blob: a7de33b60728b9e84c5985ae0dc4f3c157f096c6 [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;
Andy Huang12b3ee42013-04-24 22:49:43 -070053import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080054import android.view.KeyEvent;
55import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080056import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080057import android.view.MenuItem;
58import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070059import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070060import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070061import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080062
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080063import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080064import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070065import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070066import com.android.mail.analytics.Analytics;
Jin Cao779dd602014-04-22 16:16:28 -070067import com.android.mail.analytics.AnalyticsTimer;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080068import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070069import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080070import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070071import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070072import com.android.mail.browse.ConversationMessage;
Andy Huang87a89822014-08-14 16:44:24 -070073import com.android.mail.browse.ConversationPagerAdapter;
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 */
Andy Huang87a89822014-08-14 16:44:24 -0700468 protected 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
Andy Huang87a89822014-08-14 16:44:24 -0700541 @Override
542 public final String toString() {
543 final StringBuilder sb = new StringBuilder(super.toString());
544 sb.append("{");
545 sb.append("mCurrentConversation=");
546 sb.append(mCurrentConversation);
547 appendToString(sb);
548 sb.append("}");
549 return sb.toString();
550 }
551
552 protected void appendToString(StringBuilder sb) {}
553
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800554 public Account getCurrentAccount() {
555 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800556 }
557
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800558 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800559 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800560 }
561
562 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700563 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700564 return mConversationListCursor;
565 }
566
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700567 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700568 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800569 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700570 * @return true if the fragment is valid, false otherwise
571 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800572 private static boolean isValidFragment(Fragment in) {
573 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700574 }
575
576 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700577 * Get the conversation list fragment for this activity. If the conversation list fragment is
578 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700579 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700580 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
581 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
582 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
583 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
584 * need the fragment immediately after adding it, consider making the fragment an observer of
585 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700586 */
587 protected ConversationListFragment getConversationListFragment() {
588 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700589 if (isValidFragment(fragment)) {
590 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700591 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700592 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700593 }
594
595 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700596 * Returns the folder list fragment attached with this activity. If no such fragment is attached
597 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700598 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700599 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
600 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
601 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
602 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
603 * need the fragment immediately after adding it, consider making the fragment an observer of
604 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700605 */
606 protected FolderListFragment getFolderListFragment() {
James Lemieux10fcd642014-03-03 13:01:04 -0800607 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
608 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700609 if (isValidFragment(fragment)) {
610 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700611 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700612 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700613 }
614
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800615 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800616 * Initialize the action bar. This is not visible to OnePaneController and
617 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800618 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700619 private void initializeActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700620 final ActionBar actionBar = mActivity.getSupportActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700621 if (actionBar == null) {
622 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700623 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700624
Jin Caoc6801eb2014-08-12 18:16:57 -0700625 mActionBarController = new ActionBarController(mContext);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700626 mActionBarController.initialize(mActivity, this, actionBar);
Jin Caoc6801eb2014-08-12 18:16:57 -0700627 actionBar.setShowHideAnimationEnabled(false);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700628
Andy Huang12b3ee42013-04-24 22:49:43 -0700629 // init the action bar to allow the 'up' affordance.
630 // any configurations that disallow 'up' should do that later.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700631 mActionBarController.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700632 }
633
634 /**
635 * Attach the action bar to the activity.
636 */
637 private void attachActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700638 final ActionBar actionBar = mActivity.getSupportActionBar();
Tony Mantlerbd091502013-09-16 13:59:47 -0700639 if (actionBar != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700640 // Show a title
641 final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700642 actionBar.setDisplayOptions(mask, mask);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700643 mActionBarController.setViewModeController(mViewMode);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800644 }
645 }
646
647 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800648 * Returns whether the conversation list fragment is visible or not.
649 * Different layouts will have their own notion on the visibility of
650 * fragments, so this method needs to be overriden.
651 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800652 */
653 protected abstract boolean isConversationListVisible();
654
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700655 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700656 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700657 */
658 final void perhapsEnterWaitMode() {
659 // If the account is not initialized, then show the wait fragment, since nothing can be
660 // shown.
661 if (mAccount.isAccountInitializationRequired()) {
662 showWaitForInitialization();
663 return;
664 }
665
666 final boolean inWaitingMode = inWaitMode();
667 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
668 if (isSyncRequired) {
669 if (inWaitingMode) {
670 // Update the WaitFragment's account object
671 updateWaitMode();
672 } else {
673 // Transition to waiting mode
674 showWaitForInitialization();
675 }
676 } else if (inWaitingMode) {
677 // Dismiss waiting mode
678 hideWaitForInitialization();
679 }
680 }
681
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800682 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700683 public void switchToDefaultInboxOrChangeAccount(Account account) {
684 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Andy Huange764cfd2014-02-26 11:55:03 -0800685 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -0800686 // We are in an activity on top of the main navigation activity.
687 // We need to return to it with a result code that indicates it should navigate to
688 // a different folder.
689 final Intent intent = new Intent();
690 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
691 mActivity.setResult(Activity.RESULT_OK, intent);
692 mActivity.finish();
693 return;
694 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700695 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700696 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700697 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700698 if (switchToDefaultInbox) {
699 loadAccountInbox();
700 return;
701 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700702 changeAccount(account);
703 }
704
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700705 public void changeAccount(Account account) {
706 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
707 // Is the account or account settings different from the existing account?
708 final boolean firstLoad = mAccount == null;
709 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
710
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800711 // If nothing has changed, return early without wasting any more time.
712 if (!accountChanged && !account.settingsDiffer(mAccount)) {
713 return;
714 }
715 // We also don't want to do anything if the new account is null
716 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700717 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800718 return;
719 }
Tony Mantler79b11562013-10-09 15:31:50 -0700720 final String emailAddress = account.getEmailAddress();
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800721 mHandler.post(new Runnable() {
722 @Override
723 public void run() {
Tony Mantler79b11562013-10-09 15:31:50 -0700724 MailActivity.setNfcMessage(emailAddress);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700725 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800726 });
727 if (accountChanged) {
728 commitDestructiveActions(false);
729 }
Régis Décamps2168cbc2014-08-22 15:00:37 +0200730
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800731 // Change the account here
732 setAccount(account);
733 // And carry out associated actions.
734 cancelRefreshTask();
735 if (accountChanged) {
736 loadAccountInbox();
737 }
738 // Check if we need to force setting up an account before proceeding.
739 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
740 // Launch the intent!
741 final Intent intent = new Intent(Intent.ACTION_EDIT);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700742
743 intent.setPackage(mContext.getPackageName());
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800744 intent.setData(mAccount.settings.setupIntentUri);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700745
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800746 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800747 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800748 }
749
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700750 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700751 * Adds a listener interested in change in the current account. If a class is storing a
752 * reference to the current account, it should listen on changes, so it can receive updates to
753 * settings. Must happen in the UI thread.
754 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800755 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700756 public void registerAccountObserver(DataSetObserver obs) {
757 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800758 }
759
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700760 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700761 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700762 * Must happen in the UI thread.
763 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700764 @Override
765 public void unregisterAccountObserver(DataSetObserver obs) {
766 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700767 }
768
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700769 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700770 public void registerAllAccountObserver(DataSetObserver observer) {
771 mAllAccountObservers.registerObserver(observer);
772 }
773
774 @Override
775 public void unregisterAllAccountObserver(DataSetObserver observer) {
776 mAllAccountObservers.unregisterObserver(observer);
777 }
778
779 @Override
780 public Account[] getAllAccounts() {
781 return mAllAccounts;
782 }
783
784 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700785 public Account getAccount() {
786 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700787 }
788
Rohan Shah0f73d902013-04-19 17:06:37 -0700789 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700790 public void registerFolderOrAccountChangedObserver(final DataSetObserver observer) {
791 mFolderOrAccountObservers.registerObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700792 }
793
794 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700795 public void unregisterFolderOrAccountChangedObserver(final DataSetObserver observer) {
796 mFolderOrAccountObservers.unregisterObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700797 }
798
799 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700800 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
801 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
802 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700803 */
804 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700805 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
806 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700807 if (!isDrawerEnabled()) {
Tony Mantler54022ee2014-07-07 13:43:35 -0700808 if (hasNewFolderOrAccount) {
809 mFolderOrAccountObservers.notifyChanged();
810 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700811 return;
812 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700813 // If there are no new folders or accounts to switch to, just close the drawer
814 if (!hasNewFolderOrAccount) {
815 mDrawerContainer.closeDrawers();
816 return;
817 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700818 // Otherwise, start preloading the conversation list for the new folder.
819 if (nextFolder != null) {
820 preloadConvList(nextAccount, nextFolder);
821 }
822 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700823 final ConversationListFragment conversationList = getConversationListFragment();
824 if (conversationList != null) {
825 mListViewForAnimating = conversationList.getListView();
826 } else {
827 // There is no conversation list to animate, so just set it to null
828 mListViewForAnimating = null;
829 }
830
831 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
832 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
833 mHasNewAccountOrFolder = true;
834 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
835 } else {
836 // Drawer is already closed, notify observers that is the case.
Tony Mantler54022ee2014-07-07 13:43:35 -0700837 if (hasNewFolderOrAccount) {
838 mFolderOrAccountObservers.notifyChanged();
839 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700840 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700841 }
842
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700843 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700844 * Load the conversation list early for the given folder. This happens when some UI element
845 * (usually the drawer) instructs the controller that an account change or folder change is
846 * imminent. While the UI element is animating, the controller can preload the conversation
847 * list for the default inbox of the account provided here or to the folder provided here.
848 *
849 * @param nextAccount The account which the app will switch to shortly, possibly null.
850 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700851 */
852 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
853 // Fire off the conversation list loader for this account already with a fake
854 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700855 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700856 if (nextAccount != null) {
857 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
858 } else {
859 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
860 }
861 if (nextFolder != null) {
862 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700863 } else {
864 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700865 }
866 mFolder = null;
867 final LoaderManager lm = mActivity.getLoaderManager();
868 lm.destroyLoader(LOADER_CONVERSATION_LIST);
869 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
870 }
871
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700872 /**
873 * Initiates the async request to create a fake search folder, which returns conversations that
874 * match the query term provided by the user. Returns immediately.
875 * @param intent Intent that the app was started with. This intent contains the search query.
876 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800877 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700878 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700879 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800880 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800881 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800882 }
883
Jin Cao405a3442014-08-25 13:49:33 -0700884 protected void onFolderChanged(Folder folder, final boolean force) {
Andy Huangf58e4c32014-07-09 16:58:18 -0700885 if (isDrawerEnabled()) {
886 /** If the folder doesn't exist, or its parent URI is empty,
887 * this is not a child folder */
888 final boolean isTopLevel = Folder.isRoot(folder);
889 final int mode = mViewMode.getMode();
Rohan Shah207e6622014-09-29 14:47:31 -0700890 updateDrawerIndicator(mode, isTopLevel);
Andy Huangf58e4c32014-07-09 16:58:18 -0700891 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700892
Andy Huangf58e4c32014-07-09 16:58:18 -0700893 mDrawerContainer.closeDrawers();
894 }
Alice Yang7dd0e1c2013-09-04 06:43:16 +0000895
896 if (mFolder == null || !mFolder.equals(folder)) {
897 // We are actually changing the folder, so exit cab mode
898 exitCabMode();
899 }
900
Scott Kennedya158ac82013-09-04 13:48:13 -0700901 final String query;
902 if (folder != null && folder.isType(FolderType.SEARCH)) {
903 query = mConvListContext.searchQuery;
904 } else {
905 query = null;
906 }
907
908 changeFolder(folder, query, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700909 }
910
911 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700912 * Sets the folder state without changing view mode and without creating a list fragment, if
913 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800914 * @param folder the folder whose list of conversations are to be shown
915 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700916 */
917 private void setListContext(Folder folder, String query) {
918 updateFolder(folder);
919 if (query != null) {
920 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
921 } else {
922 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
923 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700924 cancelRefreshTask();
925 }
926
927 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700928 * Changes the folder to the value provided here. This causes the view mode to change.
929 * @param folder the folder to change to
930 * @param query if non-null, this represents the search string that the folder represents.
Alice Yangebeef1b2013-09-04 06:41:10 +0000931 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
932 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700933 */
Alice Yangebeef1b2013-09-04 06:41:10 +0000934 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700935 if (!Objects.equal(mFolder, folder)) {
936 commitDestructiveActions(false);
937 }
Alice Yangebeef1b2013-09-04 06:41:10 +0000938 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700939 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700940 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800941 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700942 // Touch the current folder: it is different, and it has been accessed.
Tony Mantler46b5b332014-10-16 15:36:50 -0700943 if (mFolder != null) {
944 mRecentFolderList.touchFolder(mFolder, mAccount);
945 }
Mindy Pereira28e0c342012-02-17 15:05:13 -0800946 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800947 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800948 }
949
Mindy Pereira13c12a62012-05-31 15:41:08 -0700950 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700951 public void onFolderSelected(Folder folder) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000952 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700953 }
954
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700955 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700956 * Adds a listener interested in change in the recent folders. If a class is storing a
957 * reference to the recent folders, it should listen on changes, so it can receive updates.
958 * Must happen in the UI thread.
959 */
960 @Override
961 public void registerRecentFolderObserver(DataSetObserver obs) {
962 mRecentFolderObservers.registerObserver(obs);
963 }
964
965 /**
966 * Removes a listener from receiving recent folder changes.
967 * Must happen in the UI thread.
968 */
969 @Override
970 public void unregisterRecentFolderObserver(DataSetObserver obs) {
971 mRecentFolderObservers.unregisterObserver(obs);
972 }
973
974 @Override
975 public RecentFolderList getRecentFolders() {
976 return mRecentFolderList;
977 }
978
Jin Cao405a3442014-08-25 13:49:33 -0700979 /**
980 * Load the default inbox associated with the current account.
981 */
982 protected void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700983 boolean handled = false;
984 if (mFolderWatcher != null) {
985 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
986 if (inbox != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000987 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700988 handled = true;
989 }
990 }
991 if (!handled) {
Paul Westbrook1bf20e02014-02-26 12:48:54 -0800992 LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700993 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
994 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700995 final int mode = mViewMode.getMode();
996 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700997 mViewMode.enterConversationListMode();
998 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700999 }
1000
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -07001001 @Override
1002 public void setFolderWatcher(FolderWatcher watcher) {
1003 mFolderWatcher = watcher;
1004 }
1005
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001006 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001007 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
1008 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
1009 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001010 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001011 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001012 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001013 // We should never try to assign a null folder. But in the rare event that we do, we should
1014 // only set the bit when we have a valid folder, and null is not valid.
1015 if (newFolder == null) {
1016 return;
1017 }
1018 // If the previous folder was null, or if the two folders represent different data, then we
1019 // consider that the folder has changed.
Scott Kennedy259df5b2013-07-11 13:24:01 -07001020 if (mFolder == null || !newFolder.equals(mFolder)) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001021 mFolderChanged = true;
1022 }
1023 }
1024
1025 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001026 * Sets the current folder if it is different from the object provided here. This method does
1027 * NOT notify the folder observers that a change has happened. Observers are notified when we
1028 * get an updated folder from the loaders, which will happen as a consequence of this method
1029 * (since this method starts/restarts the loaders).
1030 * @param folder The folder to assign
1031 */
Mindy Pereira11e35962012-06-01 14:49:46 -07001032 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001033 if (folder == null || !folder.isInitialized()) {
1034 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
1035 return;
1036 }
1037 if (folder.equals(mFolder)) {
1038 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
1039 return;
1040 }
1041 final boolean wasNull = mFolder == null;
1042 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
1043 final LoaderManager lm = mActivity.getLoaderManager();
1044 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
1045 // ensure that the folder is different from the previous folder before marking the
1046 // folder changed.
1047 setHasFolderChanged(folder);
1048 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001049
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001050 // We do not need to notify folder observers yet. Instead we start the loaders and
1051 // when the load finishes, we will get an updated folder. Then, we notify the
1052 // folderObservers in onLoadFinished.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001053 mActionBarController.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001054
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001055 // Only when we switch from one folder to another do we want to restart the
1056 // folder and conversation list loaders (to trigger onCreateLoader).
1057 // The first time this runs when the activity is [re-]initialized, we want to re-use the
1058 // previous loader's instance and data upon configuration change (e.g. rotation).
1059 // If there was not already an instance of the loader, init it.
1060 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001061 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001062 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001063 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001064 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001065 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
1066 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001067 // folders, we want to restart the loader to get the information
1068 // for the newly selected folder
1069 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001070 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001071 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -07001072 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001073 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07001074 args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY,
1075 mIgnoreInitialConversationLimit);
1076 mIgnoreInitialConversationLimit = false;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001077 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001078 }
1079
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001080 @Override
Andy Huang090db1e2012-07-25 13:25:28 -07001081 public Folder getFolder() {
1082 return mFolder;
1083 }
1084
1085 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -07001086 public Folder getHierarchyFolder() {
1087 return mFolderListFolder;
1088 }
1089
Jin Cao405a3442014-08-25 13:49:33 -07001090 /**
1091 * Set the folder currently selected in the folder selection hierarchy fragments.
1092 */
1093 protected void setHierarchyFolder(Folder folder) {
Mindy Pereirac975e842012-07-16 09:15:00 -07001094 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001095 }
1096
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001097 /**
1098 * The mail activity calls other activities for two specific reasons:
1099 * <ul>
1100 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1101 * <li>To update the password on a current account. The result {@link
1102 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1103 * </ul>
1104 * @param requestCode
1105 * @param resultCode
1106 * @param data
1107 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001108 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001109 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001110 switch (requestCode) {
1111 case ADD_ACCOUNT_REQUEST_CODE:
1112 // We were waiting for the user to create an account
1113 if (resultCode == Activity.RESULT_OK) {
1114 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001115 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1116 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001117 } else {
1118 // The user failed to create an account, just exit the app
1119 mActivity.finish();
1120 }
1121 break;
1122 case REAUTHENTICATE_REQUEST_CODE:
1123 if (resultCode == Activity.RESULT_OK) {
1124 // The user successfully authenticated, attempt to refresh the list
1125 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1126 if (refreshUri != null) {
1127 startAsyncRefreshTask(refreshUri);
1128 }
1129 }
1130 break;
Martin Hibdon371a71c2014-02-19 13:55:28 -08001131 case CHANGE_NAVIGATION_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001132 if (ViewMode.isSearchMode(mViewMode.getMode())) {
1133 mActivity.setResult(resultCode, data);
1134 mActivity.finish();
1135 } else if (resultCode == Activity.RESULT_OK && data != null) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08001136 // We have have received a result that indicates we need to navigate to a
1137 // different folder or account. This happens if someone navigates using the
1138 // drawer on the search results activity.
1139 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER);
1140 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT);
1141 if (folder != null) {
1142 onFolderSelected(folder);
1143 mViewMode.enterConversationListMode();
1144 } else if (account != null) {
1145 switchToDefaultInboxOrChangeAccount(account);
1146 mViewMode.enterConversationListMode();
1147 }
1148 }
1149 break;
Greg Bullock406ae082014-08-27 17:03:16 +02001150 case VOICE_SEARCH_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001151 if (resultCode == Activity.RESULT_OK) {
1152 final ArrayList<String> matches = data.getStringArrayListExtra(
1153 RecognizerIntent.EXTRA_RESULTS);
1154 if (!matches.isEmpty()) {
1155 // not sure how dependable the API is, but it's all we have.
1156 // take the top choice.
1157 mSearchViewController.onSearchPerformed(matches.get(0));
1158 }
1159 }
1160 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001161 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001162 }
1163
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001164 /**
1165 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001166 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001167 */
1168 protected synchronized void informCursorVisiblity(boolean visible) {
1169 if (mConversationListCursor != null) {
1170 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1171 // We have informed the cursor. Subsequent visibility changes should not tell it that
1172 // the folder has changed.
1173 mFolderChanged = false;
1174 }
1175 }
1176
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001177 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001178 public void onConversationListVisibilityChanged(boolean visible) {
Jin Cao4a0456b2014-09-18 15:12:07 -07001179 mFloatingComposeButton.setVisibility(
1180 !ViewMode.isSearchMode(mViewMode.getMode()) && visible ? View.VISIBLE : View.GONE);
Andy Huang92ae7662014-09-11 17:00:47 -07001181
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001182 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001183 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001184
1185 // Notify special views
1186 final ConversationListFragment convListFragment = getConversationListFragment();
1187 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1188 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1189 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001190 }
1191
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001192 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001193 * Called when a conversation is visible. Child classes must call the super class implementation
1194 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001195 */
1196 @Override
1197 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001198 commitAutoAdvanceOperation();
1199 }
1200
1201 /**
1202 * Commits any pending destructive action that was earlier deferred by an auto-advance
1203 * mode-change transition.
1204 */
1205 private void commitAutoAdvanceOperation() {
1206 if (mAutoAdvanceOp != null) {
1207 mAutoAdvanceOp.run();
1208 mAutoAdvanceOp = null;
1209 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001210 }
1211
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001212 /**
1213 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1214 * to turn it on for shipped versions.
1215 */
1216 private void initializeDevLoggingService() {
1217 if (!MailLogService.DEBUG_ENABLED) {
1218 return;
1219 }
1220 // Check every 5 minutes.
1221 final int WAIT_TIME = 5 * 60 * 1000;
1222 // Start a runnable that periodically checks the log level and starts/stops the service.
1223 mLogServiceChecker = new Runnable() {
1224 /** True if currently logging. */
1225 private boolean mCurrentlyLogging = false;
1226
1227 /**
1228 * If the logging level has been changed since the previous run, start or stop the
1229 * service.
1230 */
1231 private void startOrStopService() {
1232 // If the log level is already high, start the service.
1233 final Intent i = new Intent(mContext, MailLogService.class);
1234 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1235 if (mCurrentlyLogging == loggingEnabled) {
1236 // No change since previous run, just return;
1237 return;
1238 }
1239 if (loggingEnabled) {
1240 LogUtils.e(LOG_TAG, "Starting MailLogService");
1241 mContext.startService(i);
1242 } else {
1243 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1244 mContext.stopService(i);
1245 }
1246 mCurrentlyLogging = loggingEnabled;
1247 }
1248
1249 @Override
1250 public void run() {
1251 startOrStopService();
1252 mHandler.postDelayed(this, WAIT_TIME);
1253 }
1254 };
1255 // Start the runnable right away.
1256 mHandler.post(mLogServiceChecker);
1257 }
1258
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001259 /**
1260 * The application can be started from the following entry points:
1261 * <ul>
1262 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1263 * as “Starting the app”.</li>
1264 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1265 * <li>Widget: Shows the contents of a synced label, and allows:
1266 * <ul>
1267 * <li>Viewing the list (tapping on the title)</li>
1268 * <li>Composing a new message (tapping on the new message icon in the title. This
1269 * launches the {@link ComposeActivity}.
1270 * </li>
1271 * <li>Viewing a single message (tapping on a list element)</li>
1272 * </ul>
1273 *
1274 * </li>
1275 * <li>Tapping on a notification:
1276 * <ul>
1277 * <li>Shows message list if more than one message</li>
1278 * <li>Shows the conversation if the notification is for a single message</li>
1279 * </ul>
1280 * </li>
1281 * <li>...and most importantly, the activity life cycle can tear down the application and
1282 * restart it:
1283 * <ul>
1284 * <li>Rotate the application: it is destroyed and recreated.</li>
1285 * <li>Navigate away, and return from recent applications.</li>
1286 * </ul>
1287 * </li>
1288 * <li>Add a new account: fires off an intent to add an account,
1289 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1290 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1291 * <li>Composing can happen from many entry points: third party applications fire off an
1292 * intent to compose email, and launch directly into the {@link ComposeActivity}
1293 * .</li>
1294 * </ul>
1295 * {@inheritDoc}
1296 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001297 @Override
Andy Huang87a89822014-08-14 16:44:24 -07001298 public void onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001299 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001300 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001301 // Allow shortcut keys to function for the ActionBar and menus.
1302 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001303 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001304 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001305 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001306 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001307
James Lemieux1a1e9752014-07-18 13:50:40 -07001308 mFloatingComposeButton = mActivity.findViewById(R.id.compose_button);
1309 mFloatingComposeButton.setOnClickListener(this);
James Lemieux5d79a912014-07-16 14:22:26 -07001310
Andy Huangf58e4c32014-07-09 16:58:18 -07001311 if (isDrawerEnabled()) {
Andrew Sapperstein355dd902014-09-10 12:46:44 -07001312 mDrawerToggle = new ActionBarDrawerToggle(mActivity, mDrawerContainer,
1313 R.string.drawer_open, R.string.drawer_close);
Andy Huangf58e4c32014-07-09 16:58:18 -07001314 mDrawerContainer.setDrawerListener(mDrawerListener);
1315 mDrawerContainer.setDrawerShadow(
1316 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
Andy Huang12b3ee42013-04-24 22:49:43 -07001317
Rohan Shah207e6622014-09-29 14:47:31 -07001318 // Disable default drawer indicator as we are setting the drawer indicator icons.
1319 // TODO(shahrk): Once we can disable/enable drawer animation, go back to using
1320 // drawer indicators.
1321 mDrawerToggle.setDrawerIndicatorEnabled(false);
Rohan Shah375faf02014-10-09 15:34:36 -07001322 mDrawerToggle.setHomeAsUpIndicator(R.drawable.ic_menu_wht_24dp);
Andy Huangf58e4c32014-07-09 16:58:18 -07001323 } else {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001324 final ActionBar ab = mActivity.getSupportActionBar();
Rohan Shah375faf02014-10-09 15:34:36 -07001325 ab.setHomeAsUpIndicator(R.drawable.ic_menu_wht_24dp);
Andy Huangf58e4c32014-07-09 16:58:18 -07001326 ab.setHomeActionContentDescription(R.string.drawer_open);
1327 ab.setDisplayHomeAsUpEnabled(true);
1328 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001329
Mindy Pereira161f50d2012-02-28 15:47:19 -08001330 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001331 // simplifies the amount of logic in the AbstractActivityController, but increases the
1332 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001333 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001334 mPagerController = new ConversationPagerController(mActivity, this);
James Lemieux9a110112014-08-07 15:23:13 -07001335 mToastBar = findActionableToastBar(mActivity);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001336 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -07001337
Andy Huang144bfe72013-06-11 13:27:52 -07001338 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1339
Andy Huang632721e2012-04-11 16:57:26 -07001340 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001341
Jin Caoc6801eb2014-08-12 18:16:57 -07001342 mSearchViewController = new MaterialSearchViewController(mActivity, this, intent,
1343 savedState);
Jin Cao524ded52014-09-05 13:44:58 -07001344 addConversationListLayoutListener(mSearchViewController);
Jin Caoc6801eb2014-08-12 18:16:57 -07001345
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001346 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001347 // that does not rely on restored fragments or loader data
1348 // any state restoration that relies on those can be done later in
1349 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1350 if (savedState != null) {
1351 if (savedState.containsKey(SAVED_ACCOUNT)) {
1352 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001353 }
1354 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001355 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001356 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001357 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001358 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001359 if (savedState.containsKey(SAVED_ACTION)) {
1360 mDialogAction = savedState.getInt(SAVED_ACTION);
1361 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001362 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001363 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001364 } else if (intent != null) {
1365 handleIntent(intent);
1366 }
Andy Huang632721e2012-04-11 16:57:26 -07001367 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001368 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1369 mAccountCallbacks);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001370 }
1371
James Lemieux9a110112014-08-07 15:23:13 -07001372 /**
1373 * @param activity the activity that has been inflated
1374 * @return the Actionable Toast Bar defined within the activity
1375 */
1376 protected ActionableToastBar findActionableToastBar(MailActivity activity) {
1377 return (ActionableToastBar) activity.findViewById(R.id.toast_bar);
1378 }
1379
Andy Huangb1c34dc2012-04-17 16:36:19 -07001380 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001381 public void onPostCreate(Bundle savedState) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001382 if (!isDrawerEnabled()) {
1383 return;
1384 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001385 // Sync the toggle state after onRestoreInstanceState has occurred.
1386 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001387
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001388 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001389 }
1390
1391 @Override
1392 public void onConfigurationChanged(Configuration newConfig) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001393 if (isDrawerEnabled()) {
1394 mDrawerToggle.onConfigurationChanged(newConfig);
1395 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001396 }
1397
1398 /**
James Lemieux5d79a912014-07-16 14:22:26 -07001399 * This controller listens for clicks on items in the floating action bar.
1400 *
1401 * @param view the item that was clicked in the floating action bar
1402 */
1403 @Override
1404 public void onClick(View view) {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001405 final int viewId = view.getId();
1406 if (viewId == R.id.compose_button) {
1407 ComposeActivity.compose(mActivity.getActivityContext(), getAccount());
1408 } else if (viewId == android.R.id.home) {
1409 // TODO: b/16627877
Jin Cao405a3442014-08-25 13:49:33 -07001410 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001411 }
James Lemieux5d79a912014-07-16 14:22:26 -07001412 }
1413
1414 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001415 * If drawer is open/visible (even partially), close it.
1416 */
1417 protected void closeDrawerIfOpen() {
1418 if (!isDrawerEnabled()) {
1419 return;
1420 }
1421 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1422 mDrawerContainer.closeDrawers();
1423 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001424 }
1425
1426 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001427 public void onStart() {
1428 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001429
1430 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001431
1432 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1433 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1434 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001435 }
1436
1437 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001438 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001439 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001440 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1441 if (fragment != null) {
1442 fragment.dismiss();
1443 }
mindypea04f932012-08-27 14:17:59 -07001444 // When the user places the app in the background by pressing "home",
1445 // dismiss the toast bar. However, since there is no way to determine if
1446 // home was pressed, just dismiss any existing toast bar when restarting
1447 // the app.
1448 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001449 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001450 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001451 }
1452
1453 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001454 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001455 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001456 }
1457
1458 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001459 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001460 if (mViewMode.isAdMode()) {
1461 return false;
1462 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001463 final MenuInflater inflater = mActivity.getMenuInflater();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001464 inflater.inflate(mActionBarController.getOptionsMenuId(), menu);
1465 mActionBarController.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001466 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001467 }
1468
1469 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001470 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001471 return false;
1472 }
1473
mindyp17a8e782012-11-29 14:56:17 -08001474 public abstract boolean doesActionChangeConversationListVisibility(int action);
1475
Jin Cao30c881a2014-04-08 14:28:36 -07001476 /**
1477 * Helper function that determines if we should associate an undo callback with
1478 * the current menu action item
1479 * @param actionId the id of the action
1480 * @return the appropriate callback handler, or null if not applicable
1481 */
1482 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(
1483 int actionId, final Conversation conv) {
1484 // We associated the undoCallback if the user is going to perform an action on the current
1485 // conversation, causing the current conversation to be removed from view and replacing it
1486 // with another (via Auto Advance). The undoCallback will bring the removed conversation
1487 // back into the view if the action is undone.
1488 final Collection<Conversation> convCol = Conversation.listOf(conv);
1489 final boolean isApplicableForReshow = mAccount != null &&
1490 mAccount.settings != null &&
1491 mTracker != null &&
1492 // ensure that we will show another conversation due to Auto Advance
1493 mTracker.getNextConversation(
1494 mAccount.settings.getAutoAdvanceSetting(), convCol) != null &&
1495 // ensure that we are performing the action from conversation view
1496 isCurrentConversationInView(convCol) &&
1497 // check for the appropriate destructive actions
1498 doesActionRemoveCurrentConversationFromView(actionId);
1499 return (isApplicableForReshow) ?
1500 new UndoCallback() {
1501 @Override
1502 public void performUndoCallback() {
1503 showConversation(conv);
1504 }
1505 } : null;
1506 }
1507
1508 /**
1509 * Check if the provided action will remove the active conversation from view
1510 * @param actionId the applied action
1511 * @return true if it will remove the conversation from view, false otherwise
1512 */
1513 private boolean doesActionRemoveCurrentConversationFromView(int actionId) {
1514 return actionId == R.id.archive ||
1515 actionId == R.id.delete ||
Jin Cao512821c2014-05-30 15:54:04 -07001516 actionId == R.id.discard_outbox ||
Jin Cao30c881a2014-04-08 14:28:36 -07001517 actionId == R.id.remove_folder ||
1518 actionId == R.id.report_spam ||
1519 actionId == R.id.report_phishing ||
1520 actionId == R.id.move_to;
1521 }
1522
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001523 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001524 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001525
Andy Huang12b3ee42013-04-24 22:49:43 -07001526 /*
1527 * The action bar home/up action should open or close the drawer.
1528 * mDrawerToggle will take care of this.
1529 */
Andy Huangf58e4c32014-07-09 16:58:18 -07001530 if (isDrawerEnabled() && mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001531 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1532 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001533 return true;
1534 }
1535
Andy Huang2b555492013-08-14 21:06:21 -07001536 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
Andy Huangf8c59b02014-03-19 20:00:53 -07001537 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0);
Andy Huang042a5302013-08-13 12:39:08 -07001538
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001539 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001540 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001541 /** This is NOT a batch action. */
1542 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001543 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001544 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001545 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001546 // doing before. Don't animate if we are launching a new screen.
1547 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Jin Cao30c881a2014-04-08 14:28:36 -07001548 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(
1549 id, mCurrentConversation);
1550
Jin Cao2d7d1ee2014-09-29 13:18:22 -07001551 // Menu items that are targetted, only perform if there actually is a target and the
1552 // cursor is showing the target in the list.
Jin Caoa6b45212014-09-18 13:46:54 -07001553 boolean handled = false;
Jin Cao2d7d1ee2014-09-29 13:18:22 -07001554 if (target.size() > 0 &&
1555 ConversationCursor.isCursorReadyToShow(getConversationListCursor())) {
Jin Caoa6b45212014-09-18 13:46:54 -07001556 handled = true;
1557 if (id == R.id.archive) {
1558 final boolean showDialog = (settings != null && settings.confirmArchive);
1559 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation,
1560 undoCallback);
1561 } else if (id == R.id.remove_folder) {
1562 delete(R.id.remove_folder, target,
1563 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback),
Jin Cao30c881a2014-04-08 14:28:36 -07001564 isBatch);
Jin Caoa6b45212014-09-18 13:46:54 -07001565 } else if (id == R.id.delete) {
1566 final boolean showDialog = (settings != null && settings.confirmDelete);
1567 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation,
1568 undoCallback);
1569 } else if (id == R.id.discard_drafts) {
1570 // drafts are lost forever, so always confirm
1571 confirmAndDelete(id, target, true /* showDialog */,
1572 R.plurals.confirm_discard_drafts_conversation, undoCallback);
1573 } else if (id == R.id.discard_outbox) {
1574 // discard in outbox means we discard the failed message and save them in drafts
1575 delete(id, target, getDeferredAction(id, target, isBatch, undoCallback), isBatch);
1576 } else if (id == R.id.mark_important) {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001577 updateConversation(Conversation.listOf(mCurrentConversation),
Jin Caoa6b45212014-09-18 13:46:54 -07001578 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1579 } else if (id == R.id.mark_not_important) {
1580 if (mFolder != null && mFolder.isImportantOnly()) {
1581 delete(R.id.mark_not_important, target,
1582 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback),
1583 isBatch);
1584 } else {
1585 updateConversation(target, ConversationColumns.PRIORITY,
1586 UIProvider.ConversationPriority.LOW);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001587 }
Jin Caoa6b45212014-09-18 13:46:54 -07001588 } else if (id == R.id.mute) {
1589 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback),
1590 isBatch);
1591 } else if (id == R.id.report_spam) {
1592 delete(R.id.report_spam, target,
1593 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback),
1594 isBatch);
1595 } else if (id == R.id.mark_not_spam) {
1596 // Currently, since spam messages are only shown in list with
1597 // other spam messages,
1598 // marking a message not as spam is a destructive action
1599 delete(R.id.mark_not_spam, target,
1600 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback),
1601 isBatch);
1602 } else if (id == R.id.report_phishing) {
1603 delete(R.id.report_phishing, target,
1604 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback),
1605 isBatch);
1606 } else if (id == R.id.move_to || id == R.id.change_folders) {
1607 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount,
1608 target, isBatch, mFolder, id == R.id.move_to);
1609 if (dialog != null) {
1610 dialog.show(mActivity.getFragmentManager(), null);
1611 }
1612 } else if (id == R.id.move_to_inbox) {
1613 new AsyncTask<Void, Void, Folder>() {
1614 @Override
1615 protected Folder doInBackground(final Void... params) {
1616 // Get the "move to" inbox
1617 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1618 true /* allowHidden */);
1619 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001620
Jin Caoa6b45212014-09-18 13:46:54 -07001621 @Override
1622 protected void onPostExecute(final Folder moveToInbox) {
1623 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1624 // Add inbox
1625 ops.add(new FolderOperation(moveToInbox, true));
1626 assignFolder(ops, target, true, true /* showUndo */, false /* isMoveTo */);
1627 }
1628 }.execute((Void[]) null);
1629 } else {
1630 handled = false;
1631 }
Mindy Pereira28d5f722012-02-15 12:32:40 -08001632 }
Jin Caoa6b45212014-09-18 13:46:54 -07001633
1634 // Not handled by the targetted menu items, check the general ones.
1635 if (!handled) {
1636 handled = true;
1637 if (id == android.R.id.home) {
1638 handleUpPress();
1639 } else if (id == R.id.compose) {
1640 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1641 } else if (id == R.id.refresh) {
1642 requestFolderRefresh();
1643 } else if (id == R.id.toggle_drawer) {
1644 toggleDrawerState();
1645 } else if (id == R.id.settings) {
1646 Utils.showSettings(mActivity.getActivityContext(), mAccount);
1647 } else if (id == R.id.help_info_menu_item) {
1648 mActivity.showHelp(mAccount, mViewMode.getMode());
1649 } else if (id == R.id.empty_trash) {
1650 showEmptyDialog();
1651 } else if (id == R.id.empty_spam) {
1652 showEmptyDialog();
1653 } else if (id == R.id.search) {
1654 mSearchViewController.showSearchActionBar(
1655 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
1656 } else {
1657 handled = false;
1658 }
1659 }
1660
1661 // If the controller didn't handle this event, check the CAB menu if it's active.
1662 // This is necessary because keyboard shortcuts don't seem to check CAB menus.
1663 if (!handled && mCabActionMenu != null && mCabActionMenu.isActivated() &&
1664 mCabActionMenu.onActionItemClicked(item)) {
1665 handled = true;
1666 }
1667
Mindy Pereira9b875682012-02-15 18:10:54 -08001668 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001669 }
1670
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001671 /**
1672 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1673 */
1674 private void showEmptyDialog() {
1675 if (mFolder != null) {
1676 final EmptyFolderDialogFragment fragment =
1677 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1678 fragment.setListener(this);
1679 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1680 }
1681 }
1682
1683 @Override
1684 public void onFolderEmptied() {
1685 emptyFolder();
1686 }
1687
1688 /**
1689 * Performs the work of emptying the currently visible folder.
1690 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001691 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001692 if (mConversationListCursor != null) {
1693 mConversationListCursor.emptyFolder();
1694 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001695 }
1696
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001697 private void attachEmptyFolderDialogFragmentListener() {
1698 final EmptyFolderDialogFragment fragment =
1699 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1700 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1701
1702 if (fragment != null) {
1703 fragment.setListener(this);
1704 }
1705 }
1706
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001707 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001708 * Toggles the drawer pullout. If it was open (Fully extended), the
1709 * drawer will be closed. Otherwise, the drawer will be opened. This should
1710 * only be called when used with a toggle item. Other cases should be handled
1711 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001712 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001713 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001714 if (!isDrawerEnabled()) {
1715 return;
1716 }
1717 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1718 mDrawerContainer.closeDrawers();
1719 } else {
1720 mDrawerContainer.openDrawer(mDrawerPullout);
1721 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001722 }
1723
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001724 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001725 public final boolean onBackPressed() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001726 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1727 mDrawerContainer.closeDrawers();
1728 return true;
Jin Caoc6801eb2014-08-12 18:16:57 -07001729 } else if (mSearchViewController.handleBackPress()) {
1730 return true;
Andrew Sapperstein4c928742014-08-29 15:34:23 -07001731 // If we're in CAB mode, let the activity handle onBackPressed.
1732 // It will handle closing CAB mode for us.
1733 } else if (mCabActionMenu != null && mCabActionMenu.isActivated()) {
1734 return false;
Andy Huang12b3ee42013-04-24 22:49:43 -07001735 }
1736
Andy Huangc1fb9a92013-02-11 13:09:12 -08001737 return handleBackPress();
1738 }
1739
1740 protected abstract boolean handleBackPress();
Greg Bullockede2e522014-05-30 14:11:35 +02001741
Andy Huangc1fb9a92013-02-11 13:09:12 -08001742 protected abstract boolean handleUpPress();
1743
1744 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001745 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001746 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001747 refreshConversationList();
1748 }
1749
1750 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001751 public void updateConversation(Collection <Conversation> target, String columnName,
1752 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001753 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001754 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001755 }
1756
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001757 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001758 public void updateConversation(Collection <Conversation> target, String columnName,
1759 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001760 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001761 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001762 }
1763
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001764 @Override
1765 public void updateConversation(Collection <Conversation> target, String columnName,
1766 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001767 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001768 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001769 }
1770
Andy Huang839ada22012-07-20 15:48:40 -07001771 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001772 public void markConversationMessagesUnread(final Conversation conv,
1773 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang87a89822014-08-14 16:44:24 -07001774 onPreMarkUnread();
Andy Huang8f6b0062012-07-31 15:36:31 -07001775
Andy Huang839ada22012-07-20 15:48:40 -07001776 // locally mark conversation unread (the provider is supposed to propagate message unread
1777 // to conversation unread)
1778 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001779 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001780 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001781
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001782 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1783 @Override
1784 public void onLoadFinished() {
1785 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1786 originalConversationInfo);
1787 }
1788 });
1789 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001790 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001791 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1792 }
1793 }
1794
Andy Huang87a89822014-08-14 16:44:24 -07001795 /**
1796 * Hook to do stuff before actually marking a conversation unread (only called from within
1797 * conversation view). Most configurations do the default behavior of popping out of
1798 * CV to go back to TL.
1799 *
1800 */
1801 protected void onPreMarkUnread() {
1802 // The only caller of this method is the conversation view, from where marking unread should
1803 // take you back to list mode in most cases. Two-pane view is the exception.
1804 showConversation(null);
1805 }
1806
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001807 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1808 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001809 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001810 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001811 final int numMessages = conv.getNumMessages();
1812 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1813 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001814
Andy Huang9e4ca792013-02-28 14:33:43 -08001815 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001816 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001817 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001818 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001819 // Conversations are neither marked read, nor viewed, and we don't want to show
1820 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001821 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001822 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001823 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001824 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1825 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1826 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1827 info);
1828 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001829 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001830
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001831 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001832 if (originalConversationInfo != null) {
1833 mConversationListCursor.setConversationColumn(conv.uri,
1834 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1835 }
Andy Huang839ada22012-07-20 15:48:40 -07001836
1837 // applyBatch with each CPO as an UPDATE op on each affected message uri
1838 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1839 String authority = null;
1840 for (Uri messageUri : unreadMessageUris) {
1841 if (authority == null) {
1842 authority = messageUri.getAuthority();
1843 }
1844 ops.add(ContentProviderOperation.newUpdate(messageUri)
1845 .withValue(UIProvider.MessageColumns.READ, 0)
1846 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001847 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001848 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001849 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001850 new ContentProviderTask() {
1851 @Override
1852 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001853 if (result.exception != null) {
1854 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1855 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001856 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1857 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001858 }
Andy Huang839ada22012-07-20 15:48:40 -07001859 }
1860 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001861 }
Andy Huang839ada22012-07-20 15:48:40 -07001862 }
1863
Andy Huang87a89822014-08-14 16:44:24 -07001864 /**
1865 * Mark a single conversation 'seen', which is a combination of 'viewed' and 'read'. In some
1866 * configurations (peek mode), this operation may be prevented and the method will return false.
1867 *
1868 * @param conv the conversation to mark seen
1869 * @return true if the operation was a success
1870 */
1871 @Override
1872 public boolean markConversationSeen(Conversation conv) {
1873 if (isCurrentConversationJustPeeking()) {
1874 LogUtils.i(LOG_TAG, "AAC is in peek mode, not marking seen. conv=%s", conv);
1875 return false;
1876 } else {
1877 markConversationsRead(Arrays.asList(conv), true /* read */, true /* viewed */);
1878 return true;
1879 }
1880 }
1881
Andy Huang839ada22012-07-20 15:48:40 -07001882 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001883 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1884 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001885 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1886
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001887 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001888 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1889 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1890 targets.toArray());
1891 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001892 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1893 @Override
1894 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001895 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001896 }
1897 });
1898 } else {
1899 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001900 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001901 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001902 }
1903
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001904 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001905 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001906 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001907 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001908 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001909 final Runnable operation = new Runnable() {
1910 @Override
1911 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001912 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001913 }
1914 };
1915
Scott Kennedycaaeed32013-06-12 13:39:16 -07001916 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001917 // This method will be called again if the user selects an autoadvance option
1918 return;
1919 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001920 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001921
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001922 final int size = targets.size();
1923 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1924 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001925 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001926 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001927
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001928 // We never want to mark unseen here, but we do want to mark it seen
1929 if (read || markViewed) {
1930 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1931 }
1932
Paul Westbrook5109c512012-11-05 11:00:30 -08001933 // The mark read/unread/viewed operations do not show an undo bar
1934 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001935 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001936 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001937 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001938 final ConversationInfo info = target.conversationInfo;
Tony Mantleredd6c1a2013-10-08 14:47:43 -07001939 final boolean changed = info.markRead(read);
1940 if (changed) {
1941 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
Andy Huang839ada22012-07-20 15:48:40 -07001942 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001943 opList.add(mConversationListCursor.getOperationForConversation(
1944 target, ConversationOperation.UPDATE, value));
1945 // Update the local conversation objects so they immediately change state.
1946 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001947 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001948 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001949 }
Andy Huang839ada22012-07-20 15:48:40 -07001950 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001951 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001952 }
1953
Andy Huang8f6b0062012-07-31 15:36:31 -07001954 /**
1955 * Auto-advance to a different conversation if the currently visible conversation in
1956 * conversation mode is affected (deleted, marked unread, etc.).
1957 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001958 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001959 *
1960 * @param target the set of conversations being deleted/marked unread
1961 */
mindyp9365a822012-09-12 09:09:09 -07001962 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001963 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001964 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001965 }
1966
1967 /**
Jin Cao30c881a2014-04-08 14:28:36 -07001968 * Helper function to determine if the provided set of conversations is in view
1969 * @param target set of conversations that we are interested in
1970 * @return true if they are in view, false otherwise
1971 */
1972 private boolean isCurrentConversationInView(final Collection<Conversation> target) {
1973 final int viewMode = mViewMode.getMode();
1974 return (viewMode == ViewMode.CONVERSATION
1975 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1976 && Conversation.contains(target, mCurrentConversation);
1977 }
1978
1979 /**
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001980 * Auto-advance to a different conversation if the currently visible conversation in
1981 * conversation mode is affected (deleted, marked unread, etc.).
1982 *
1983 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001984 * <p>
1985 * Clients may pass an operation to execute on the target that this method will run after
1986 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1987 * later, or not at all. Reasons it may run later include:
1988 * <ul>
1989 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1990 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1991 * mode change transition to finish</li>
1992 * </ul>
1993 * <p>If the current conversation is not in the target collection, this method will do nothing,
1994 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001995 *
1996 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001997 * @param operation (optional) the operation to execute after advancing
1998 * @return <code>false</code> if this method handled or will execute the operation,
1999 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002000 */
2001 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002002 final Runnable operation) {
Jin Cao30c881a2014-04-08 14:28:36 -07002003 if (isCurrentConversationInView(target)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002004 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
2005
Tony Mantler93e64572014-07-11 14:35:56 -07002006 // If we don't have one set, but we're here, just take the default
2007 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
2008 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002009
Tony Mantler93e64572014-07-11 14:35:56 -07002010 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
2011 // transition doesn't run (i.e. it "completes" immediately).
2012 mAutoAdvanceOp = operation;
Andy Huang87a89822014-08-14 16:44:24 -07002013 doShowNextConversation(target, autoAdvance);
Tony Mantler93e64572014-07-11 14:35:56 -07002014 return (mAutoAdvanceOp == null);
Andy Huang8f6b0062012-07-31 15:36:31 -07002015 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002016
2017 return true;
2018 }
2019
Andy Huang87a89822014-08-14 16:44:24 -07002020 /**
2021 * Do the actual work of selecting a next conversation to show and showing it. Two-pane
2022 * overrides this in landscape to prefer peeking rather than staring at an empty CV pane when
2023 * auto-advance=LIST.
2024 *
2025 * @param target conversations being destroyed, of which the current convo is one
2026 * @param autoAdvance auto-advance pref value
2027 */
2028 protected void doShowNextConversation(Collection<Conversation> target, int autoAdvance) {
2029 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
2030 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
2031 showConversation(next);
2032 }
2033
Andy Huang839ada22012-07-20 15:48:40 -07002034 @Override
2035 public void starMessage(ConversationMessage msg, boolean starred) {
2036 if (msg.starred == starred) {
2037 return;
2038 }
2039
Tony Mantlerb5371672014-10-02 13:55:31 -07002040 msg.setStarredInConversation(starred);
Andy Huang839ada22012-07-20 15:48:40 -07002041
2042 // locally propagate the change to the owning conversation
2043 // (figure the provider will properly propagate the change when it commits it)
2044 //
2045 // when unstarring, only propagate the change if this was the only message starred
2046 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08002047 final Conversation conv = msg.getConversation();
2048 if (conversationStarred != conv.starred) {
2049 conv.starred = conversationStarred;
2050 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07002051 ConversationColumns.STARRED, conversationStarred);
2052 }
2053
2054 final ContentValues values = new ContentValues(1);
2055 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
2056
2057 new ContentProviderTask.UpdateTask() {
2058 @Override
2059 protected void onPostExecute(Result result) {
2060 // TODO: handle errors?
2061 }
2062 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
2063 }
2064
Andy Huang12b3ee42013-04-24 22:49:43 -07002065 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07002066 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07002067 if (mFolder == null) {
2068 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08002069 }
Alice Yang37dda442013-03-26 22:48:53 -07002070 final ConversationListFragment convList = getConversationListFragment();
2071 if (convList == null) {
2072 // This could happen if this account is in initial sync (user
2073 // is seeing the "your mail will appear shortly" message)
2074 return;
2075 }
2076 convList.showSyncStatusBar();
2077
2078 if (mAsyncRefreshTask != null) {
2079 mAsyncRefreshTask.cancel(true);
2080 }
2081 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
2082 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08002083 }
2084
Mindy Pereirafbe40192012-03-20 10:40:45 -07002085 /**
2086 * Confirm (based on user's settings) and delete a conversation from the conversation list and
2087 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002088 * @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 -07002089 * @param target the conversations to act upon
2090 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
2091 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07002092 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002093 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07002094 boolean showDialog, int confirmResource, UndoCallback undoCallback) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002095 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002096 if (showDialog) {
Jin Cao30c881a2014-04-08 14:28:36 -07002097 makeDialogListener(actionId, isBatch, undoCallback);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002098 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
2099 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002100 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
2101 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07002102 } else {
Jin Cao30c881a2014-04-08 14:28:36 -07002103 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002104 }
2105 }
2106
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002107 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002108 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002109 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07002110 // Order of events is critical! The Conversation View Fragment must be
2111 // notified of the next conversation with showConversation(next) *before* the
2112 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002113 // fragment has a chance to delete the conversation, animating it away.
2114
mindyp84f7d322012-10-01 17:14:40 -07002115 // Update the conversation fragment if the current conversation is
2116 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002117 final Runnable operation = new Runnable() {
2118 @Override
2119 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07002120 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002121 }
2122 };
2123
Rohan Shah52bab6f2014-08-26 18:52:14 -07002124 showNextConversation(target, operation);
2125
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002126 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002127 // Batch selections are cleared in the end of the action, so not done for batch actions.
2128 if (!isBatch) {
2129 for (final Conversation conv : target) {
Jin Caoec0fa482014-08-28 16:38:08 -07002130 if (mCheckedSet.contains(conv)) {
2131 mCheckedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002132 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002133 }
2134 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002135 // The conversation list deletes and performs the action if it exists.
2136 final ConversationListFragment convListFragment = getConversationListFragment();
2137 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07002138 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002139 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002140 return;
2141 }
mindyp84f7d322012-10-01 17:14:40 -07002142 // No visible UI element handled it on our behalf. Perform the action
2143 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07002144 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07002145 action.performAction();
2146 }
2147
2148 /**
2149 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002150 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07002151 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002152 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002153 action.performAction();
2154 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002155 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07002156
2157 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002158 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
2159 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002160 }
2161
2162 @Override
Andy Huang87a89822014-08-14 16:44:24 -07002163 public void onPrepareOptionsMenu(Menu menu) {
2164 mActionBarController.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002165 }
2166
Mindy Pereira68f2e222012-03-07 10:36:54 -08002167 @Override
2168 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002169 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002170 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08002171 }
2172
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002173 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002174 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002175 // Register the receiver that will prevent the status receiver from
2176 // displaying its notification icon as long as we're running.
2177 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2178 // that the notification was received for.
2179 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002180
2181 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002182
2183 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002184
2185 // Invalidating the options menu so that when we make changes in settings,
2186 // the changes will always be updated in the action bar/options menu/
2187 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002188 }
2189
2190 @Override
2191 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002192 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002193 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002194 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2195 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002196 if (mFolder != null) {
2197 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002198 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002199 // If this is a search activity, let's store the search query term as well.
2200 if (ConversationListContext.isSearchResult(mConvListContext)) {
2201 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2202 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002203 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002204 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2205 }
Jin Caoec0fa482014-08-28 16:38:08 -07002206 if (!mCheckedSet.isEmpty()) {
2207 outState.putParcelable(SAVED_SELECTED_SET, mCheckedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002208 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002209 if (mToastBar.getVisibility() == View.VISIBLE) {
2210 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2211 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002212 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002213 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002214 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002215 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002216 // 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 -08002217 if (mDialogAction != -1) {
2218 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002219 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002220 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002221 if (mDetachedConvUri != null) {
2222 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2223 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002224
Scott Kennedyb10212e2013-02-22 16:27:00 -08002225 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002226 mSafeToModifyFragments = false;
Alice Yangebeef1b2013-09-04 06:41:10 +00002227
2228 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002229
2230 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2231 mConversationListScrollPositions);
Jin Caoc6801eb2014-08-12 18:16:57 -07002232
2233 mSearchViewController.saveState(outState);
Andy Huang1ee96b22012-08-24 20:19:53 -07002234 }
2235
2236 /**
2237 * @see #mSafeToModifyFragments
2238 */
2239 protected boolean safeToModifyFragments() {
2240 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002241 }
2242
2243 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002244 public void executeSearch(String query) {
Jin Cao779dd602014-04-22 16:16:28 -07002245 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST);
Mindy Pereira68f2e222012-03-07 10:36:54 -08002246 Intent intent = new Intent();
2247 intent.setAction(Intent.ACTION_SEARCH);
2248 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2249 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2250 intent.setComponent(mActivity.getComponentName());
Jin Caoc6801eb2014-08-12 18:16:57 -07002251 mSearchViewController.showSearchActionBar(
2252 MaterialSearchViewController.SEARCH_VIEW_STATE_GONE);
Martin Hibdon371a71c2014-02-19 13:55:28 -08002253 // Call startActivityForResult here so we can tell if we have navigated to a different folder
2254 // or account from search results.
2255 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002256 }
2257
2258 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002259 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002260 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002261 }
2262
Andy Huang632721e2012-04-11 16:57:26 -07002263 @Override
2264 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002265 // stop listening to the cursor on e.g. configuration changes
2266 if (mConversationListCursor != null) {
2267 mConversationListCursor.removeListener(this);
2268 }
Andy Huang144bfe72013-06-11 13:27:52 -07002269 mDrawIdler.setListener(null);
2270 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002271 // unregister the ViewPager's observer on the conversation cursor
2272 mPagerController.onDestroy();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002273 mActionBarController.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002274 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002275 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002276 mHandler.removeCallbacks(mLogServiceChecker);
2277 mLogServiceChecker = null;
Jin Caoc6801eb2014-08-12 18:16:57 -07002278 mSearchViewController.onDestroy();
Andy Huang632721e2012-04-11 16:57:26 -07002279 }
2280
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002281 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002282 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2283 * or not. The individual controller is responsible for changing the icon based on the mode.
2284 */
2285 protected abstract void resetActionBarIcon();
2286
2287 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002288 * {@inheritDoc} Subclasses must override this to listen to mode changes
2289 * from the ViewMode. Subclasses <b>must</b> call the parent's
2290 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002291 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002292 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002293 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002294 // When we step away from the conversation mode, we don't have a current conversation
2295 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2296 if (!ViewMode.isConversationMode(newMode)) {
2297 setCurrentConversation(null);
2298 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002299
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002300 // If the viewmode is not set, preserve existing icon.
2301 if (newMode != ViewMode.UNKNOWN) {
2302 resetActionBarIcon();
2303 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002304
2305 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002306 /** If the folder doesn't exist, or its parent URI is empty,
2307 * this is not a child folder */
Jin Cao9695e002014-05-29 11:56:44 -07002308 final boolean isTopLevel = Folder.isRoot(mFolder);
Rohan Shah207e6622014-09-29 14:47:31 -07002309 updateDrawerIndicator(newMode, isTopLevel);
Martin Hibdon371a71c2014-02-19 13:55:28 -08002310 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002311 closeDrawerIfOpen();
2312 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002313 }
2314
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002315 /**
Rohan Shah207e6622014-09-29 14:47:31 -07002316 * Update the drawer indicator to either be the burger or the back arrow.
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002317 * @param viewMode the current view mode
2318 * @param isTopLevel true if the current folder is not a child
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002319 */
Rohan Shah207e6622014-09-29 14:47:31 -07002320 private void updateDrawerIndicator(final int viewMode, final boolean isTopLevel) {
2321 // Show burger if we're either in conversation list or folder list mode.
2322 if (isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
2323 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel)) {
Rohan Shah375faf02014-10-09 15:34:36 -07002324 mDrawerToggle.setHomeAsUpIndicator(R.drawable.ic_menu_wht_24dp);
Rohan Shah207e6622014-09-29 14:47:31 -07002325
2326 // Otherwise, show the back arrow for the indicator.
2327 } else {
2328 mDrawerToggle.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp_with_rtl);
2329 }
Scott Kennedy8a72b852013-05-02 14:18:50 -07002330 }
2331
Andy Huang3825f3d2012-08-29 16:44:12 -07002332 public void disablePagerUpdates() {
2333 mPagerController.stopListening();
2334 }
2335
Andy Huang4e0158f2012-08-07 21:06:01 -07002336 public boolean isDestroyed() {
2337 return mDestroyed;
2338 }
2339
mindyp54f120f2012-08-28 13:10:33 -07002340 @Override
2341 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002342 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002343 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002344 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002345 }
2346 }
2347
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002348 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002349 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002350 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002351 // hasFocus already ensures that the window is in focus, so we don't need to call
2352 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002353 if (hasFocus && convList != null && convList.isVisible()) {
2354 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002355 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002356 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002357 }
2358
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002359 /**
2360 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002361 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002362 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002363 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002364 if (account == null) {
2365 LogUtils.w(LOG_TAG, new Error(),
2366 "AAC ignoring null (presumably invalid) account restoration");
2367 return;
2368 }
Andy Huangb1148412012-05-19 00:16:30 -07002369 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002370 mAccount = account;
Régis Décamps2168cbc2014-08-22 15:00:37 +02002371
2372 Analytics.getInstance().setEmail(account.getEmailAddress(), account.getType());
2373
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002374 // Only change AAC state here. Do *not* modify any other object's state. The object
2375 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002376 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002377 mActivity.invalidateOptionsMenu();
2378 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002379 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002380 // The Mail instance can be null during test runs.
2381 final MailAppProvider instance = MailAppProvider.getInstance();
2382 if (instance != null) {
2383 instance.setLastViewedAccount(mAccount.uri.toString());
2384 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002385 if (account.settings == null) {
2386 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2387 return;
2388 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002389 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002390 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002391 }
2392
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002393 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002394 * Restore the state from the previous bundle. Subclasses should call this
2395 * method from the parent class, since it performs important UI
2396 * initialization.
2397 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002398 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002399 */
Andy Huang632721e2012-04-11 16:57:26 -07002400 @Override
2401 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002402 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002403 if (savedState.containsKey(SAVED_CONVERSATION)) {
2404 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002405 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Andy Huang87a89822014-08-14 16:44:24 -07002406 restoreConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002407 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002408
Mindy Pereirad33674992012-06-25 16:26:30 -07002409 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002410 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002411 if (op != null) {
2412 if (op.getType() == ToastBarOperation.UNDO) {
2413 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002414 } else if (op.getType() == ToastBarOperation.ERROR) {
2415 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002416 }
2417 }
2418 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002419 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002420 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002421 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002422 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002423 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002424 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002425 * Restore the state of selected conversations. This needs to be done after the correct mode
2426 * is set and the action bar is fully initialized. If not, several key pieces of state
2427 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002428 */
Andy Huang4556a442012-03-30 16:42:05 -07002429 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002430 // Order is important!!!
2431 // The dialog listener needs to happen *after* the selected set is restored.
2432
2433 // If there has been an orientation change, and we need to recreate the listener for the
2434 // confirm dialog fragment (delete/archive/...), then do it here.
2435 if (mDialogAction != -1) {
Jin Cao30c881a2014-04-08 14:28:36 -07002436 makeDialogListener(mDialogAction, mDialogFromSelectedSet,
2437 getUndoCallbackForDestructiveActionsWithAutoAdvance(
2438 mDialogAction, mCurrentConversation));
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002439 }
Alice Yangebeef1b2013-09-04 06:41:10 +00002440
2441 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002442
2443 mConversationListScrollPositions.clear();
2444 mConversationListScrollPositions.putAll(
2445 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002446 }
2447
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002448 /**
2449 * Handle an intent to open the app. This method is called only when there is no saved state,
2450 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2451 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002452 *
2453 * This method is called for a subset of the reasons mentioned in
2454 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2455 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002456 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002457 */
Andy Huang632721e2012-04-11 16:57:26 -07002458 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002459 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002460 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2461 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Tony Mantler26a20752014-02-28 16:44:24 -08002462 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002463 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002464 if (mAccount == null) {
2465 return;
Andy Huang632721e2012-04-11 16:57:26 -07002466 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002467 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002468
2469 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
Régis Décamps2168cbc2014-08-22 15:00:37 +02002470 Analytics.getInstance().setEmail(mAccount.getEmailAddress(), mAccount.getType());
Andy Huang4fe0af82013-08-20 17:24:51 -07002471 Analytics.getInstance().sendEvent("notification_click",
2472 isConversationMode ? "conversation" : "conversation_list", null, 0);
2473 }
2474
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002475 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002476 mViewMode.enterConversationMode();
2477 } else {
2478 mViewMode.enterConversationListMode();
2479 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002480 // Put the folder and conversation, and ask the loader to create this folder.
2481 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002482
2483 final Uri folderUri;
2484 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002485 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
Scott Kennedy48cfe462013-04-10 11:32:02 -07002486 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2487 final Folder folder =
2488 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002489 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002490 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002491 final Bundle extras = intent.getExtras();
2492 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2493 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002494 folderUri = mAccount.settings.defaultInbox;
2495 }
2496
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002497 // Check if we should load all conversations instead of using
2498 // the default behavior which loads an initial subset.
2499 mIgnoreInitialConversationLimit =
2500 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false);
2501
Scott Kennedy60593352013-03-13 13:45:30 -07002502 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002503 args.putParcelable(Utils.EXTRA_CONVERSATION,
2504 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2505 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002506 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2507 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002508 mHaveSearchResults = false;
Jin Caoc6801eb2014-08-12 18:16:57 -07002509 // Save this search query for future suggestions
Andy Huang632721e2012-04-11 16:57:26 -07002510 final String query = intent.getStringExtra(SearchManager.QUERY);
Jin Caoc6801eb2014-08-12 18:16:57 -07002511 mSearchViewController.saveRecentQuery(query);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002512 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2513 fetchSearchFolder(intent);
2514 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002515 mViewMode.enterSearchResultsConversationMode();
2516 } else {
2517 mViewMode.enterSearchResultsListMode();
2518 }
Andy Huang632721e2012-04-11 16:57:26 -07002519 } else {
2520 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2521 mActivity.finish();
2522 }
2523 }
2524 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002525 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002526 }
2527 }
2528
Andy Huang4556a442012-03-30 16:42:05 -07002529 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002530 * Returns true if we should enter conversation mode with search.
2531 */
2532 protected final boolean shouldEnterSearchConvMode() {
Andy Huang87a89822014-08-14 16:44:24 -07002533 return mHaveSearchResults && shouldShowFirstConversation();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002534 }
2535
2536 /**
Andy Huang4556a442012-03-30 16:42:05 -07002537 * Copy any selected conversations stored in the saved bundle into our selection set,
2538 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2539 *
2540 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002541 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002542 if (savedState == null) {
Jin Caoec0fa482014-08-28 16:38:08 -07002543 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002544 return;
2545 }
Jin Caoec0fa482014-08-28 16:38:08 -07002546 final ConversationCheckedSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002547 if (selectedSet == null || selectedSet.isEmpty()) {
Jin Caoec0fa482014-08-28 16:38:08 -07002548 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002549 return;
2550 }
Andy Huang632721e2012-04-11 16:57:26 -07002551
2552 // putAll will take care of calling our registered onSetPopulated method
Jin Caoec0fa482014-08-28 16:38:08 -07002553 mCheckedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002554 }
2555
Andy Huang87a89822014-08-14 16:44:24 -07002556 protected void restoreConversation(Conversation conversation) {
2557 if (conversation != null && conversation.position < 0) {
2558 // Set the position to 0 on this conversation, as we don't know where it is
2559 // in the list
2560 conversation.position = 0;
2561 }
2562 showConversation(conversation);
2563 }
2564
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002565 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002566 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2567 * object, which is a signal to back out of conversation view mode.
2568 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002569 * @param conversation the conversation to be shown, or null if we want to back out to list
2570 * mode.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002571 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002572 */
Jin Cao30c881a2014-04-08 14:28:36 -07002573 protected void showConversation(Conversation conversation) {
Rohan Shah18512bd2014-10-03 15:56:46 -07002574 showConversation(conversation, false /* shouldAnimate */);
Jin Caod23f6d12014-08-08 15:23:20 -07002575 }
2576
Rohan Shah18512bd2014-10-03 15:56:46 -07002577 /**
2578 * Helper method to allow for conversation view animation control. Implementing classes should
2579 * directly override this to handle the animation.
2580 * @param conversation
2581 * @param shouldAnimate true if we want to animate the conversation in, false otherwise
2582 */
2583 protected void showConversation(Conversation conversation, boolean shouldAnimate) {
2584 showConversationWithPeek(conversation, false /* peek */);
2585 }
2586
2587 protected void showConversationWithPeek(Conversation conversation, boolean peek) {
Andy Huang243c2362013-03-01 17:50:35 -08002588 if (conversation != null) {
2589 Utils.sConvLoadTimer.start();
2590 }
2591
Andy Huang54e925e2013-03-14 13:24:18 -07002592 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002593 // Set the current conversation just in case it wasn't already set.
2594 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002595 }
2596
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002597 /**
Jin Cao405a3442014-08-25 13:49:33 -07002598 * Show the wait for account initialization mode.
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002599 * Children can override this method, but they must call super.showWaitForInitialization().
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002600 */
Jin Cao405a3442014-08-25 13:49:33 -07002601 protected void showWaitForInitialization() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002602 mViewMode.enterWaitingForInitializationMode();
Andy Huangc96efcc2014-04-09 15:30:42 -07002603 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002604 }
2605
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002606 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002607 final FragmentManager manager = mActivity.getFragmentManager();
2608 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002609 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002610 if (waitFragment != null) {
2611 waitFragment.updateAccount(mAccount);
2612 }
2613 }
2614
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002615 /**
2616 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2617 * method, though they must call the parent implementation <b>after</b> they do anything.
2618 */
2619 protected void hideWaitForInitialization() {
2620 mWaitFragment = null;
2621 }
2622
2623 /**
2624 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2625 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2626 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002627 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002628 */
2629 protected final WaitFragment getWaitFragment() {
2630 final FragmentManager manager = mActivity.getFragmentManager();
2631 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2632 if (waitFrag != null) {
2633 // The Fragment Manager knows better, so use its instance.
2634 mWaitFragment = waitFrag;
2635 }
2636 return mWaitFragment;
2637 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002638
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002639 /**
2640 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2641 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002642 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002643 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002644 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002645 if (waitFragment != null) {
2646 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002647 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002648 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2649 }
2650 return false;
2651 }
2652
2653 /**
Jin Cao405a3442014-08-25 13:49:33 -07002654 * Show the conversation List with the list context provided here. On certain layouts, this
2655 * might show more than just the conversation list. For instance, on tablets this might show
2656 * the conversations along with the conversation list.
2657 * @param listContext context providing information on what conversation list to display.
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002658 */
Jin Cao405a3442014-08-25 13:49:33 -07002659 protected abstract void showConversationList(ConversationListContext listContext);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002660
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002661 @Override
Jin Cao0b693382014-08-11 10:46:12 -07002662 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002663 final ConversationListFragment convListFragment = getConversationListFragment();
2664 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2665 convListFragment.getAnimatedAdapter().onConversationSelected();
2666 }
mindypaa55bc92012-08-24 09:49:56 -07002667 // Only animate destructive actions if we are going to be showing the
2668 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002669 commitDestructiveActions(mIsTablet);
Rohan Shah18512bd2014-10-03 15:56:46 -07002670 showConversation(conversation, true /* shouldAnimate */);
Andy Huang1ee96b22012-08-24 20:19:53 -07002671 }
2672
2673 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002674 public final void onCabModeEntered() {
2675 final ConversationListFragment convListFragment = getConversationListFragment();
2676 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2677 convListFragment.getAnimatedAdapter().onCabModeEntered();
2678 }
2679 }
2680
2681 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002682 public final void onCabModeExited() {
2683 final ConversationListFragment convListFragment = getConversationListFragment();
2684 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2685 convListFragment.getAnimatedAdapter().onCabModeExited();
2686 }
2687 }
2688
2689 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002690 public Conversation getCurrentConversation() {
2691 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002692 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002693
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002694 /**
2695 * Set the current conversation. This is the conversation on which all actions are performed.
2696 * Do not modify mCurrentConversation except through this method, which makes it easy to
2697 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002698 * @param conversation new conversation to view. Passing null indicates that we are backing
2699 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002700 */
Andy Huang632721e2012-04-11 16:57:26 -07002701 @Override
2702 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002703 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002704 // we are going back to conversation list mode.
2705 if (mDetachedConvUri != null && (conversation == null
2706 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002707 clearDetachedMode();
2708 }
2709
2710 // Must happen *before* setting mCurrentConversation because this sets
2711 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002712 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002713 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002714
2715 if (mCurrentConversation != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002716 mActionBarController.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002717 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002718 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002719 }
2720
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002721 /**
Andy Huang87a89822014-08-14 16:44:24 -07002722 * Invoked by {@link ConversationPagerAdapter} when a new page in the ViewPager is selected.
2723 *
2724 * @param conversation the conversation of the now currently visible fragment
2725 *
2726 */
2727 @Override
2728 public void onConversationViewSwitched(Conversation conversation) {
2729 setCurrentConversation(conversation);
2730 }
2731
2732 @Override
2733 public boolean isCurrentConversationJustPeeking() {
2734 return false;
2735 }
2736
2737 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002738 * {@link LoaderManager} currently has a bug in
2739 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2740 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2741 * this bug by destroying any loaders that may have been created as null (essentially because
2742 * they are optional loads, and may not apply to a particular account).
2743 * <p>
2744 * A simple null check before restarting a loader will not work, because that would not
2745 * give the controller a chance to invalidate UI corresponding the prior loader result.
2746 *
2747 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002748 * @param handler the LoaderCallback which will handle this loader ID.
2749 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2750 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002751 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002752 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002753 final LoaderManager lm = mActivity.getLoaderManager();
2754 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002755 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002756 }
2757
Andy Huang632721e2012-04-11 16:57:26 -07002758 @Override
2759 public void registerConversationListObserver(DataSetObserver observer) {
2760 mConversationListObservable.registerObserver(observer);
2761 }
2762
2763 @Override
2764 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002765 try {
2766 mConversationListObservable.unregisterObserver(observer);
2767 } catch (IllegalStateException e) {
2768 // Log instead of crash
2769 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2770 + "hasn't been registered");
2771 }
Andy Huang632721e2012-04-11 16:57:26 -07002772 }
2773
Andy Huang090db1e2012-07-25 13:25:28 -07002774 @Override
2775 public void registerFolderObserver(DataSetObserver observer) {
2776 mFolderObservable.registerObserver(observer);
2777 }
2778
2779 @Override
2780 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002781 try {
2782 mFolderObservable.unregisterObserver(observer);
2783 } catch (IllegalStateException e) {
2784 // Log instead of crash
2785 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2786 + "hasn't been registered");
2787 }
Andy Huang090db1e2012-07-25 13:25:28 -07002788 }
2789
Andy Huang9d3fd922012-09-26 22:23:58 -07002790 @Override
2791 public void registerConversationLoadedObserver(DataSetObserver observer) {
2792 mPagerController.registerConversationLoadedObserver(observer);
2793 }
2794
2795 @Override
2796 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002797 try {
2798 mPagerController.unregisterConversationLoadedObserver(observer);
2799 } catch (IllegalStateException e) {
2800 // Log instead of crash
2801 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2802 + "that hasn't been registered");
2803 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002804 }
2805
Vikram Aggarwal60069912012-07-24 14:26:09 -07002806 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002807 * Returns true if the number of accounts is different, or if the current account has
2808 * changed. This method is meant to filter frequent changes to the list of
2809 * accounts, and only return true if the new list is substantially different from the existing
2810 * list. Returning true is safe here, it leads to more work in creating the
2811 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002812 * @param accountCursor the cursor which points to all the accounts.
2813 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002814 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002815 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002816 // Check to see if the current account hasn't been set, or the account cursor is empty
2817 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002818 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002819 }
2820
2821 // Check to see if the number of accounts are different, from the number we saw on the last
2822 // updated
2823 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2824 return true;
2825 }
2826
2827 // Check to see if the account list is different or if the current account is not found in
2828 // the cursor.
2829 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002830 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002831 final Account account = accountCursor.getModel();
2832 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2833 if (mAccount.settingsDiffer(account)) {
2834 // Settings changed, and we don't need to look any further.
2835 return true;
2836 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002837 foundCurrentAccount = true;
2838 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002839 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002840 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002841 return true;
2842 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002843 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002844
2845 // As long as we found the current account, the list hasn't been updated
2846 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002847 }
2848
2849 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002850 * Updates accounts for the app. If the current account is missing, the first
2851 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002852 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002853 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002854 * @return true if the update was successful, false otherwise
2855 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002856 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002857 if (accounts == null || !accounts.moveToFirst()) {
2858 return false;
2859 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002860
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002861 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002862 // A match for the current account's URI in the list of accounts.
2863 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002864
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002865 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002866 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002867 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002868 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002869 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002870 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2871 currentFromList = account;
2872 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002873 }
2874
Vikram Aggarwal60069912012-07-24 14:26:09 -07002875 // 1. current account is already set and is in allAccounts:
2876 // 1a. It has changed -> load the updated account.
Greg Bullock406ae082014-08-27 17:03:16 +02002877 // 1b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002878 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002879 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002880 // 4. otherwise just pick first
2881
Vikram Aggarwal60069912012-07-24 14:26:09 -07002882 boolean accountChanged = false;
2883 /// Assume case 4, initialize to first account, and see if we can find anything better.
2884 Account newAccount = allAccounts[0];
2885 if (currentFromList != null) {
2886 // Case 1: Current account exists but has changed
2887 if (!currentFromList.equals(mAccount)) {
2888 newAccount = currentFromList;
2889 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002890 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002891 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002892 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002893 // Case 2: Current account is not in allAccounts, the account needs to change.
2894 accountChanged = true;
2895 if (mAccount == null) {
2896 // Case 3: Check for last viewed account, and check if it exists in the list.
2897 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2898 if (lastAccountUri != null) {
2899 for (final Account account : allAccounts) {
2900 if (lastAccountUri.equals(account.uri.toString())) {
2901 newAccount = account;
2902 break;
2903 }
Andy Huang0d647352012-03-21 21:48:16 -07002904 }
2905 }
2906 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002907 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002908 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002909 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002910 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002911
Vikram Aggarwal60069912012-07-24 14:26:09 -07002912 // Whether we have updated the current account or not, we need to update the list of
2913 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002914 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002915 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002916 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002917 }
2918
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002919 private void disableNotifications() {
2920 mNewEmailReceiver.activate(mContext, this);
2921 }
2922
2923 private void enableNotifications() {
2924 mNewEmailReceiver.deactivate();
2925 }
2926
2927 private void disableNotificationsOnAccountChange(Account account) {
2928 // If the new mail suppression receiver is activated for a different account, we want to
2929 // activate it for the new account.
2930 if (mNewEmailReceiver.activated() &&
2931 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2932 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2933 mNewEmailReceiver.deactivate();
2934 mNewEmailReceiver.activate(mContext, this);
2935 }
2936 }
2937
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002938 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002939 * Destructive actions on Conversations. This class should only be created by controllers, and
2940 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2941 * Only the controllers should know what kind of destructive actions are being created.
2942 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002943 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002944 /**
2945 * The action to be performed. This is specified as the resource ID of the menu item
2946 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2947 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002948 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002949 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002950 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002951 /** Whether this destructive action has already been performed */
2952 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002953 /** Whether this is an action on the currently selected set. */
2954 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002955
Jin Cao30c881a2014-04-08 14:28:36 -07002956 private UndoCallback mCallback;
2957
Mindy Pereirafbe40192012-03-20 10:40:45 -07002958 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002959 * Create a listener object.
2960 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002961 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002962 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002963 * @param isBatch whether the conversations are in the currently selected batch set.
2964 */
2965 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002966 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002967 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002968 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002969 }
2970
Jin Cao30c881a2014-04-08 14:28:36 -07002971 @Override
2972 public void setUndoCallback(UndoCallback undoCallback) {
2973 mCallback = undoCallback;
2974 }
2975
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002976 /**
2977 * The action common to child classes. This performs the action specified in the constructor
2978 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002979 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002980 @Override
2981 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002982 if (isPerformed()) {
2983 return;
2984 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002985 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002986
2987 // Are we destroying the currently shown conversation? Show the next one.
2988 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002989 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2990 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002991 Conversation.toString(mTarget), mCurrentConversation);
2992 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002993
Paul Westbrooke1221d22012-08-19 11:09:07 -07002994 if (mConversationListCursor == null) {
2995 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2996 + "\nmTarget=%s\nCurrent=%s",
2997 Conversation.toString(mTarget), mCurrentConversation);
2998 return;
2999 }
3000
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003001 if (mAction == R.id.archive) {
3002 LogUtils.d(LOG_TAG, "Archiving");
Jin Cao30c881a2014-04-08 14:28:36 -07003003 mConversationListCursor.archive(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003004 } else if (mAction == R.id.delete) {
3005 LogUtils.d(LOG_TAG, "Deleting");
Jin Cao30c881a2014-04-08 14:28:36 -07003006 mConversationListCursor.delete(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003007 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07003008 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003009 }
3010 } else if (mAction == R.id.mute) {
3011 LogUtils.d(LOG_TAG, "Muting");
3012 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
3013 for (Conversation c : mTarget) {
3014 c.localDeleteOnUpdate = true;
3015 }
3016 }
Jin Cao30c881a2014-04-08 14:28:36 -07003017 mConversationListCursor.mute(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003018 } else if (mAction == R.id.report_spam) {
3019 LogUtils.d(LOG_TAG, "Reporting spam");
Jin Cao30c881a2014-04-08 14:28:36 -07003020 mConversationListCursor.reportSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003021 } else if (mAction == R.id.mark_not_spam) {
3022 LogUtils.d(LOG_TAG, "Marking not spam");
Jin Cao30c881a2014-04-08 14:28:36 -07003023 mConversationListCursor.reportNotSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003024 } else if (mAction == R.id.report_phishing) {
3025 LogUtils.d(LOG_TAG, "Reporting phishing");
Jin Cao30c881a2014-04-08 14:28:36 -07003026 mConversationListCursor.reportPhishing(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07003027 } else if (mAction == R.id.remove_star) {
3028 LogUtils.d(LOG_TAG, "Removing star");
3029 // Star removal is destructive in the Starred folder.
3030 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
3031 false);
3032 } else if (mAction == R.id.mark_not_important) {
3033 LogUtils.d(LOG_TAG, "Marking not-important");
3034 // Marking not important is destructive in a mailbox
3035 // containing only important messages
3036 if (mFolder != null && mFolder.isImportantOnly()) {
3037 for (Conversation conv : mTarget) {
3038 conv.localDeleteOnUpdate = true;
3039 }
3040 }
3041 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
3042 UIProvider.ConversationPriority.LOW);
3043 } else if (mAction == R.id.discard_drafts) {
3044 LogUtils.d(LOG_TAG, "Discarding draft messages");
3045 // Discarding draft messages is destructive in a "draft" mailbox
3046 if (mFolder != null && mFolder.isDraft()) {
3047 for (Conversation conv : mTarget) {
3048 conv.localDeleteOnUpdate = true;
3049 }
3050 }
3051 mConversationListCursor.discardDrafts(mTarget);
3052 // We don't support undoing discarding drafts
3053 undoEnabled = false;
Jin Cao512821c2014-05-30 15:54:04 -07003054 } else if (mAction == R.id.discard_outbox) {
3055 LogUtils.d(LOG_TAG, "Discarding failed messages in Outbox");
3056 mConversationListCursor.moveFailedIntoDrafts(mTarget);
3057 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003058 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07003059 if (undoEnabled && mTarget.size() > 0) {
mindypead50392012-08-23 11:03:53 -07003060 mHandler.postDelayed(new Runnable() {
3061 @Override
3062 public void run() {
3063 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003064 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07003065 }
3066 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003067 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003068 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003069 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07003070 mCheckedSet.clear();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003071 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07003072 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003073
3074 /**
3075 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003076 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003077 */
3078 private synchronized boolean isPerformed() {
3079 if (mCompleted) {
3080 return true;
3081 }
3082 mCompleted = true;
3083 return false;
3084 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07003085 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003086
Vikram Aggarwald503df42012-05-11 10:13:35 -07003087 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
3088 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003089 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07003090 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003091 Collection<Conversation> target, boolean batch, boolean showUndo,
3092 final boolean isMoveTo) {
Jin Cao479753f2014-07-31 14:31:01 -07003093 // Actions are destructive only when the current folder can be un-assigned from and
Mindy Pereira8db7e402012-07-13 10:32:47 -07003094 // when the list of folders contains the current folder.
3095 final boolean isDestructive = mFolder
Jin Cao479753f2014-07-31 14:31:01 -07003096 .supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION)
Mindy Pereira8db7e402012-07-13 10:32:47 -07003097 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07003098 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
3099 if (isDestructive) {
3100 for (final Conversation c : target) {
3101 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07003102 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003103 }
mindypc84759c2012-08-29 09:51:53 -07003104 final DestructiveAction folderChange;
Jin Cao30c881a2014-04-08 14:28:36 -07003105 final UndoCallback undoCallback = isMoveTo ?
3106 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to,
3107 mCurrentConversation)
3108 : null;
Vikram Aggarwald503df42012-05-11 10:13:35 -07003109 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003110 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07003111 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003112 /*
3113 * If this is a MOVE operation, we want the action folder to be the destination folder.
3114 * Otherwise, we want it to be the current folder.
3115 *
3116 * A set of folder operations is a move if there are exactly two operations: an add and
3117 * a remove.
3118 */
3119 final Folder actionFolder;
3120 if (folderOps.size() != 2) {
3121 actionFolder = mFolder;
3122 } else {
3123 Folder addedFolder = null;
3124 boolean hasRemove = false;
3125 for (final FolderOperation folderOperation : folderOps) {
3126 if (folderOperation.mAdd) {
3127 addedFolder = folderOperation.mFolder;
3128 } else {
3129 hasRemove = true;
3130 }
3131 }
3132
3133 if (hasRemove && addedFolder != null) {
3134 actionFolder = addedFolder;
3135 } else {
3136 actionFolder = mFolder;
3137 }
3138 }
3139
mindypc84759c2012-08-29 09:51:53 -07003140 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003141 batch, showUndo, isMoveTo, actionFolder, undoCallback);
Scott Kennedycaaeed32013-06-12 13:39:16 -07003142 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003143 } else {
mindypc84759c2012-08-29 09:51:53 -07003144 folderChange = getFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003145 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003146 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003147 }
3148 }
3149
Mindy Pereira967ede62012-03-22 09:29:09 -07003150 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003151 public final void onRefreshRequired() {
Andy Huang12a05d22014-08-28 21:36:18 -07003152 if (isAnimating()) {
Andy Huangf21787a2014-05-02 14:22:01 +02003153 final ConversationListFragment f = getConversationListFragment();
3154 LogUtils.w(ConversationCursor.LOG_TAG,
3155 "onRefreshRequired: delay until animating done. cursor=%s adapter=%s",
3156 mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null);
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003157 return;
3158 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003159 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003160 if (mConversationListCursor.isRefreshRequired()) {
3161 mConversationListCursor.refresh();
3162 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003163 }
3164
mindyp5390fca2012-08-22 12:12:25 -07003165 @Override
mindyp6f54e1b2012-10-09 09:54:08 -07003166 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003167 boolean isAnimating = false;
3168 ConversationListFragment convListFragment = getConversationListFragment();
3169 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07003170 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003171 }
3172 return isAnimating;
3173 }
3174
Marc Blankbf128eb2012-04-18 15:58:45 -07003175 /**
3176 * Called when the {@link ConversationCursor} is changed or has new data in it.
3177 * <p>
3178 * {@inheritDoc}
3179 */
3180 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003181 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08003182 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
3183 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06003184
3185 if (mDestroyed) {
3186 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
3187 return;
3188 }
3189
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003190 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07003191 // Swap cursors
3192 mConversationListCursor.sync();
Andy Huangf21787a2014-05-02 14:22:01 +02003193 } else {
3194 // (CLF guaranteed to be non-null due to check in isAnimating)
3195 LogUtils.w(LOG_TAG,
3196 "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s",
3197 mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
Marc Blankbf128eb2012-04-18 15:58:45 -07003198 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07003199 mTracker.onCursorUpdated();
Andy Huang87a89822014-08-14 16:44:24 -07003200 perhapsShowFirstConversation();
Marc Blankbf128eb2012-04-18 15:58:45 -07003201 }
3202
3203 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003204 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003205 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003206 mConversationListObservable.notifyChanged();
Jin Caoec0fa482014-08-28 16:38:08 -07003207 mCheckedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003208 }
3209
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003210 /**
3211 * If the Conversation List Fragment is visible, updates the fragment.
3212 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003213 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003214 final ConversationListFragment convList = getConversationListFragment();
3215 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003216 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003217 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003218 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003219 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003220 }
3221 }
3222
3223 /**
3224 * This class handles throttled refresh of the conversation list
3225 */
3226 static class RefreshTimerTask extends TimerTask {
3227 final Handler mHandler;
3228 final AbstractActivityController mController;
3229
3230 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3231 mHandler = handler;
3232 mController = controller;
3233 }
3234
3235 @Override
3236 public void run() {
3237 mHandler.post(new Runnable() {
3238 @Override
3239 public void run() {
3240 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3241 mController.onRefreshRequired();
3242 }});
3243 }
3244 }
3245
3246 /**
3247 * Cancel the refresh task, if it's running
3248 */
3249 private void cancelRefreshTask () {
3250 if (mConversationListRefreshTask != null) {
3251 mConversationListRefreshTask.cancel();
3252 mConversationListRefreshTask = null;
3253 }
3254 }
3255
3256 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003257 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Andy Huangf21787a2014-05-02 14:22:01 +02003258 if (animatedAdapter != null) {
3259 LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor,
3260 animatedAdapter);
3261 }
Paul Westbrook026139c2012-09-19 22:35:37 -07003262 if (mConversationListCursor == null) {
3263 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3264 return;
3265 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003266 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003267 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003268 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003269 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003270
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003271 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003272 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003273 mConversationListCursor.refresh();
3274 }
mindyp6f54e1b2012-10-09 09:54:08 -07003275 if (mRecentsDataUpdated) {
3276 mRecentsDataUpdated = false;
3277 mRecentFolderObservers.notifyChanged();
3278 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003279 }
3280
3281 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003282 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003283 // There are no selected conversations. Ensure that the listener and its associated actions
3284 // are blanked out.
3285 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003286 }
3287
3288 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003289 public void onSetPopulated(ConversationCheckedSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003290 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003291 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003292 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003293 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003294 }
3295
Mindy Pereira967ede62012-03-22 09:29:09 -07003296 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003297 public void onSetChanged(ConversationCheckedSet set) {
Mindy Pereira967ede62012-03-22 09:29:09 -07003298 // Do nothing. We don't care about changes to the set.
3299 }
3300
3301 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003302 public ConversationCheckedSet getCheckedSet() {
3303 return mCheckedSet;
Mindy Pereira967ede62012-03-22 09:29:09 -07003304 }
3305
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003306 /**
3307 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3308 */
3309 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003310 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003311 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003312 if (mCabActionMenu != null) {
3313 mCabActionMenu.deactivate();
3314 }
3315 }
3316
3317 /**
3318 * Re-enable the CAB menu if required. The selection set is not changed.
3319 */
3320 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003321 if (mCabActionMenu != null &&
3322 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003323 mCabActionMenu.activate();
3324 }
3325 }
3326
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003327 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003328 * Re-enable CAB mode only if we have an active selection
3329 */
3330 protected void maybeEnableCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003331 if (!mCheckedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003332 if (mCabActionMenu != null) {
3333 mCabActionMenu.activate();
3334 }
Tony Mantler43fab322013-07-26 16:38:35 -07003335 }
3336 }
3337
3338 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003339 * Unselect conversations and exit CAB mode.
3340 */
3341 protected final void exitCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003342 mCheckedSet.clear();
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003343 }
3344
Mindy Pereira967ede62012-03-22 09:29:09 -07003345 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003346 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003347 if (mAccount == null) {
3348 // We cannot search if there is no account. Drop the request to the floor.
3349 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3350 return;
3351 }
James Lemieux3531d7e2014-01-28 11:10:05 -08003352 if (mAccount.supportsSearch()) {
Jin Caoc6801eb2014-08-12 18:16:57 -07003353 mSearchViewController.showSearchActionBar(
3354 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003355 } else {
3356 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003357 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003358 }
3359 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003360
Mindy Pereira0963ef82012-04-10 11:43:01 -07003361 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003362 public void onTouchEvent(MotionEvent event) {
3363 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003364 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
James Lemieux9a110112014-08-07 15:23:13 -07003365 // if the toast bar is still animating, ignore this attempt to hide it
3366 if (mToastBar.isAnimating()) {
3367 return;
3368 }
3369
3370 // if the toast bar has not been seen long enough, ignore this attempt to hide it
3371 if (mToastBar.cannotBeHidden()) {
3372 return;
3373 }
3374
3375 // hide the toast bar
3376 mToastBar.hide(true /* animated */, false /* actionClicked */);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003377 }
3378 }
3379 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003380
Andy Huang632721e2012-04-11 16:57:26 -07003381 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003382 public void onConversationSeen() {
3383 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003384 }
3385
Andy Huang9d3fd922012-09-26 22:23:58 -07003386 @Override
3387 public boolean isInitialConversationLoading() {
3388 return mPagerController.isInitialConversationLoading();
3389 }
3390
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003391 /**
3392 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3393 * insufficient because that doesn't check if the window is currently in focus or not.
3394 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003395 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003396 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3397 }
3398
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003399 /**
3400 * This class handles callbacks that create a {@link ConversationCursor}.
3401 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003402 private class ConversationListLoaderCallbacks implements
3403 LoaderManager.LoaderCallbacks<ConversationCursor> {
3404
3405 @Override
3406 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003407 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3408 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003409 final boolean ignoreInitialConversationLimit =
3410 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003411 if (account == null || folder == null) {
3412 return null;
3413 }
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003414 return new ConversationCursorLoader(mActivity, account,
Andy Huangf21787a2014-05-02 14:22:01 +02003415 folder.conversationListUri, folder.getTypeDescription(),
3416 ignoreInitialConversationLimit);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003417 }
3418
3419 @Override
3420 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003421 LogUtils.d(LOG_TAG,
3422 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3423 data, loader, this);
Tony Mantler0c532ef2014-09-24 10:47:53 -07003424 if (isDestroyed()) {
3425 return;
3426 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003427 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003428 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003429 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003430 return;
3431 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003432 // Clear our all pending destructive actions before swapping the conversation cursor
3433 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003434 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003435 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003436 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003437 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003438 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003439 // Handle actions that were deferred until after the conversation list was loaded.
3440 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3441 callback.onLoadFinished();
3442 }
3443 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003444
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003445 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003446 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003447 // The conversation list is already listening to list changes and gets notified
3448 // in the mConversationListObservable.notifyChanged() line above. We only need to
3449 // check and inform the cursor of the change in visibility here.
3450 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003451 }
Andy Huang87a89822014-08-14 16:44:24 -07003452 perhapsShowFirstConversation();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003453 }
3454
3455 @Override
3456 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003457 LogUtils.d(LOG_TAG,
3458 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3459 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003460
3461 if (mConversationListCursor != null) {
3462 // Unregister the listener
3463 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003464 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003465 mConversationListCursor = null;
3466
3467 // Inform anyone who is interested about the change
3468 mTracker.onCursorUpdated();
3469 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003470 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003471 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003472 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003473
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003474 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003475 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3476 */
3477 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3478 @Override
3479 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3480 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3481 switch (id) {
3482 case LOADER_FOLDER_CURSOR:
3483 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3484 final ObjectCursorLoader<Folder> loader = new
3485 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003486 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003487 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3488 return loader;
3489 case LOADER_RECENT_FOLDERS:
3490 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003491 if (mAccount != null && mAccount.recentFolderListUri != null
3492 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003493 return new ObjectCursorLoader<Folder>(mContext,
3494 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3495 }
3496 break;
3497 case LOADER_ACCOUNT_INBOX:
3498 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3499 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3500 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3501 mAccount.folderListUri : defaultInbox;
3502 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3503 if (inboxUri != null) {
3504 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3505 everything, Folder.FACTORY);
3506 }
3507 break;
3508 case LOADER_SEARCH:
3509 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3510 return Folder.forSearchResults(mAccount,
3511 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
Jin Cao1a864cc2014-05-21 11:16:39 -07003512 // We can just use current time as a unique identifier for this search
3513 Long.toString(SystemClock.uptimeMillis()),
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003514 mActivity.getActivityContext());
3515 case LOADER_FIRST_FOLDER:
3516 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3517 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3518 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3519 if (mConversationToShow != null && mConversationToShow.position < 0){
3520 mConversationToShow.position = 0;
3521 }
3522 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3523 everything, Folder.FACTORY);
3524 default:
3525 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3526 return null;
3527 }
3528 return null;
3529 }
3530
3531 @Override
3532 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3533 if (data == null) {
3534 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3535 }
Tony Mantler0c532ef2014-09-24 10:47:53 -07003536 if (isDestroyed()) {
3537 return;
3538 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003539 switch (loader.getId()) {
3540 case LOADER_FOLDER_CURSOR:
3541 if (data != null && data.moveToFirst()) {
3542 final Folder folder = data.getModel();
3543 setHasFolderChanged(folder);
3544 mFolder = folder;
3545 mFolderObservable.notifyChanged();
3546 } else {
3547 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003548 mFolder != null ? mFolder.name : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003549 }
3550 break;
3551 case LOADER_RECENT_FOLDERS:
3552 // Few recent folders and we are running on a phone? Populate the default
3553 // recents. The number of default recent folders is at least 2: every provider
3554 // has at least two folders, and the recent folder count never decreases.
3555 // Having a single recent folder is an erroneous case, and we can gracefully
3556 // recover by populating default recents. The default recents will not stomp on
3557 // the existing value: it will be shown in addition to the default folders:
3558 // the max number of recent folders is more than 1+num(defaultRecents).
3559 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3560 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3561 @Override
3562 protected Void doInBackground(Uri... uri) {
3563 // Asking for an update on the URI and ignore the result.
3564 final ContentResolver resolver = mContext.getContentResolver();
3565 resolver.update(uri[0], null, null, null);
3566 return null;
3567 }
3568 }
3569 final Uri uri = mAccount.defaultRecentFolderListUri;
3570 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3571 new PopulateDefault().execute(uri);
3572 break;
3573 }
3574 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3575 mRecentFolderList.loadFromUiProvider(data);
3576 if (isAnimating()) {
3577 mRecentsDataUpdated = true;
3578 } else {
3579 mRecentFolderObservers.notifyChanged();
3580 }
3581 break;
3582 case LOADER_ACCOUNT_INBOX:
3583 if (data != null && !data.isClosed() && data.moveToFirst()) {
3584 final Folder inbox = data.getModel();
Alice Yangebeef1b2013-09-04 06:41:10 +00003585 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003586 // Just want to get the inbox, don't care about updates to it
3587 // as this will be tracked by the folder change listener.
3588 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3589 } else {
3590 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003591 mAccount != null ? mAccount.getEmailAddress() : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003592 }
3593 break;
3594 case LOADER_SEARCH:
3595 if (data != null && data.getCount() > 0) {
3596 data.moveToFirst();
3597 final Folder search = data.getModel();
3598 updateFolder(search);
3599 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3600 mActivity.getIntent()
3601 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3602 showConversationList(mConvListContext);
3603 mActivity.invalidateOptionsMenu();
3604 mHaveSearchResults = search.totalCount > 0;
3605 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3606 } else {
3607 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3608 }
3609 break;
3610 case LOADER_FIRST_FOLDER:
3611 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3612 return;
3613 }
3614 final Folder folder = data.getModel();
3615 boolean handled = false;
3616 if (folder != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +00003617 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003618 handled = true;
3619 }
3620 if (mConversationToShow != null) {
3621 // Open the conversation.
3622 showConversation(mConversationToShow);
3623 handled = true;
3624 }
3625 if (!handled) {
3626 // We have an account, but nothing else: load the default inbox.
3627 loadAccountInbox();
3628 }
3629 mConversationToShow = null;
3630 // And don't run this anymore.
3631 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3632 break;
3633 }
3634 }
3635
3636 @Override
3637 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3638 }
3639 }
3640
3641 /**
3642 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3643 */
3644 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3645 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3646 final CursorCreator<Account> mFactory = Account.FACTORY;
3647
3648 @Override
3649 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3650 switch (id) {
3651 case LOADER_ACCOUNT_CURSOR:
3652 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3653 return new ObjectCursorLoader<Account>(mContext,
3654 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3655 case LOADER_ACCOUNT_UPDATE_CURSOR:
3656 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3657 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3658 mFactory);
3659 default:
3660 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3661 break;
3662 }
3663 return null;
3664 }
3665
3666 @Override
3667 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3668 ObjectCursor<Account> data) {
3669 if (data == null) {
3670 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3671 }
Tony Mantler0c532ef2014-09-24 10:47:53 -07003672 if (isDestroyed()) {
3673 return;
3674 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003675 switch (loader.getId()) {
3676 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003677 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003678 if (data == null) {
3679 // Nothing useful to do if we have no valid data.
3680 break;
3681 }
Andy Huang761522c2013-08-08 13:09:11 -07003682 final long count = data.getCount();
3683 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003684 // If an empty cursor is returned, the MailAppProvider is indicating that
3685 // no accounts have been specified. We want to navigate to the
3686 // "add account" activity that will handle the intent returned by the
3687 // MailAppProvider
3688
3689 // If the MailAppProvider believes that all accounts have been loaded,
3690 // and the account list is still empty, we want to prompt the user to add
3691 // an account.
3692 final Bundle extras = data.getExtras();
3693 final boolean accountsLoaded =
3694 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3695
3696 if (accountsLoaded) {
3697 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3698 (mContext);
3699 if (noAccountIntent != null) {
3700 mActivity.startActivityForResult(noAccountIntent,
3701 ADD_ACCOUNT_REQUEST_CODE);
3702 }
3703 }
3704 } else {
3705 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003706 if (!mHaveAccountList || accountListUpdated) {
3707 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003708 }
Andy Huang761522c2013-08-08 13:09:11 -07003709 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3710 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003711 }
3712 break;
3713 case LOADER_ACCOUNT_UPDATE_CURSOR:
3714 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003715 if (data != null && data.moveToFirst()) {
3716 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003717 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003718 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003719 final Settings previousSettings = mAccount.settings;
3720
3721 // Update the controller's reference to the current account
3722 mAccount = updatedAccount;
3723 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3724 + "mAccount = %s", mAccount.uri);
3725
3726 // Only notify about a settings change if something differs
3727 if (!Objects.equal(mAccount.settings, previousSettings)) {
3728 mAccountObservers.notifyChanged();
3729 }
3730 perhapsEnterWaitMode();
3731 } else {
3732 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3733 + " %s", updatedAccount.uri, mAccount.uri);
3734 // We need to restart the loader, so the correct account information
3735 // will be returned.
3736 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3737 }
3738 }
3739 break;
3740 }
3741 }
3742
3743 @Override
3744 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003745 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003746 }
3747 }
3748
3749 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003750 * Updates controller state based on search results and shows first conversation if required.
Andy Huang87a89822014-08-14 16:44:24 -07003751 * Be sure to call the super-implementation if overriding.
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003752 */
Andy Huang87a89822014-08-14 16:44:24 -07003753 protected void perhapsShowFirstConversation() {
3754 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3755 && mConversationListCursor.getCount() > 0;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003756 }
3757
3758 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003759 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3760 * next destructive action..
3761 * @param nextAction the next destructive action to be performed. This can be null.
3762 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003763 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003764 // If there is a pending action, perform that first.
3765 if (mPendingDestruction != null) {
3766 mPendingDestruction.performAction();
3767 }
3768 mPendingDestruction = nextAction;
3769 }
3770
3771 /**
3772 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003773 * 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 -07003774 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003775 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003776 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003777 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003778 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3779 // destructive action from the controller and for the controller to own the action. This is
3780 // a half-way solution while refactoring DestructiveAction.
3781 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003782 }
3783
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003784 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003785 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003786 final DestructiveAction da = new ConversationAction(action, mCheckedSet.values(), true);
Jin Cao30c881a2014-04-08 14:28:36 -07003787 da.setUndoCallback(undoCallback);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003788 registerDestructiveAction(da);
3789 return da;
3790 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003791
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003792 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003793 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003794 return getDeferredAction(action, mCheckedSet.values(), true, undoCallback);
mindypf0656a12012-10-01 08:30:57 -07003795 }
3796
3797 /**
3798 * Get a destructive action for a menu action. This is a temporary method,
3799 * to control the profusion of {@link DestructiveAction} classes that are
3800 * created. Please do not copy this paradigm.
3801 * @param action the resource ID of the menu action: R.id.delete, for
3802 * example
3803 * @param target the conversations to act upon.
3804 * @return a {@link DestructiveAction} that performs the specified action.
3805 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003806 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07003807 boolean batch, UndoCallback callback) {
3808 ConversationAction cAction = new ConversationAction(action, target, batch);
3809 cAction.setUndoCallback(callback);
3810 return cAction;
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003811 }
3812
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003813 /**
3814 * Class to change the folders that are assigned to a set of conversations. This is destructive
3815 * because the user can remove the current folder from the conversation, in which case it has
3816 * to be animated away from the current folder.
3817 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003818 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003819 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003820 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003821 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003822 /** Whether this destructive action has already been performed */
3823 private boolean mCompleted;
Martin Hibdone78c40f2013-10-10 18:29:25 -07003824 private final boolean mIsSelectedSet;
3825 private final boolean mShowUndo;
3826 private final int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003827 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003828
Jin Cao30c881a2014-04-08 14:28:36 -07003829 private UndoCallback mUndoCallback;
3830
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003831 /**
3832 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003833 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003834 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003835 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003836 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003837 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003838 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003839 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003840 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003841 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003842 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003843 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003844 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003845 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003846 }
3847
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003848 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003849 public void setUndoCallback(UndoCallback undoCallback) {
3850 mUndoCallback = undoCallback;
3851 }
3852
3853 @Override
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003854 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003855 if (isPerformed()) {
3856 return;
3857 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07003858 if (mIsDestructive && mShowUndo && mTarget.size() > 0) {
mindypcb0b30e2012-11-30 10:16:35 -08003859 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003860 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003861 onUndoAvailable(undoOp);
3862 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003863 // For each conversation, for each operation, add/ remove the
3864 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003865 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3866 ArrayList<Uri> folderUris;
3867 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003868 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003869 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3870 .getRawFolders());
3871 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003872 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003873 if (mIsDestructive) {
3874 target.localDeleteOnUpdate = true;
3875 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003876 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003877 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003878 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003879 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003880 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003881 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003882 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003883 }
3884 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003885 ops.add(mConversationListCursor.getConversationFolderOperation(target,
Jin Cao30c881a2014-04-08 14:28:36 -07003886 folderUris, adds, targetFolders.values(), mUndoCallback));
mindyp389f0b22012-08-29 11:12:54 -07003887 }
3888 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003889 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003890 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003891 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003892 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07003893 mCheckedSet.clear();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003894 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003895 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003896
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003897 /**
3898 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003899 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003900 */
3901 private synchronized boolean isPerformed() {
3902 if (mCompleted) {
3903 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003904 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003905 mCompleted = true;
3906 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003907 }
3908 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003909
mindypc84759c2012-08-29 09:51:53 -07003910 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3911 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003912 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3913 UndoCallback undoCallback) {
mindypc84759c2012-08-29 09:51:53 -07003914 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003915 isBatch, showUndo, isMoveTo, actionFolder, undoCallback);
mindypc84759c2012-08-29 09:51:53 -07003916 registerDestructiveAction(da);
3917 return da;
3918 }
3919
3920 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003921 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003922 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3923 UndoCallback undoCallback) {
3924 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch,
3925 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
3926 fd.setUndoCallback(undoCallback);
3927 return fd;
Mindy Pereira01f30502012-08-14 10:30:51 -07003928 }
3929
3930 @Override
3931 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3932 Folder toRemove, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003933 boolean showUndo, UndoCallback undoCallback) {
Mindy Pereira01f30502012-08-14 10:30:51 -07003934 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3935 folderOps.add(new FolderOperation(toRemove, false));
Jin Cao30c881a2014-04-08 14:28:36 -07003936 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003937 showUndo, R.id.remove_folder, mFolder);
Jin Cao30c881a2014-04-08 14:28:36 -07003938 da.setUndoCallback(undoCallback);
3939 return da;
Mindy Pereira01f30502012-08-14 10:30:51 -07003940 }
3941
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003942 @Override
3943 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003944 final ConversationListFragment convList = getConversationListFragment();
3945 if (convList == null) {
3946 return;
3947 }
3948 convList.requestListRefresh();
3949 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003950
3951 protected final ActionClickedListener getUndoClickedListener(
3952 final AnimatedAdapter listAdapter) {
3953 return new ActionClickedListener() {
3954 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003955 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003956 if (mAccount.undoUri != null) {
3957 // NOTE: We might want undo to return the messages affected, in which case
3958 // the resulting cursor might be interesting...
3959 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3960 // commands to undo
3961 if (mConversationListCursor != null) {
3962 mConversationListCursor.undo(
3963 mActivity.getActivityContext(), mAccount.undoUri);
3964 }
3965 if (listAdapter != null) {
3966 listAdapter.setUndo(true);
3967 }
3968 }
3969 }
3970 };
3971 }
3972
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003973 /**
3974 * Shows an error toast in the bottom when a folder was not fetched successfully.
3975 * @param folder the folder which could not be fetched.
3976 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3977 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003978 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003979
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003980 final ActionClickedListener listener;
3981 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003982 final int lastSyncResult = folder.lastSyncResult;
James Lemieuxdeee5be2014-09-29 17:51:28 -07003983 switch (UIProvider.getResultFromLastSyncResult(lastSyncResult)) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003984 case UIProvider.LastSyncResult.CONNECTION_ERROR:
James Lemieuxdeee5be2014-09-29 17:51:28 -07003985 // The sync status that caused this failure.
3986 final int syncStatus = UIProvider.getStatusFromLastSyncResult(lastSyncResult);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003987 // Show: User explicitly pressed the refresh button and there is no connection
3988 // Show: The first time the user enters the app and there is no connection
3989 // TODO(viki): Implement this.
3990 // Reference: http://b/7202801
James Lemieuxdeee5be2014-09-29 17:51:28 -07003991 final boolean showToast = (syncStatus & UIProvider.SyncStatus.USER_REFRESH) != 0;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003992 // Don't show: Already in the app; user switches to a synced label
3993 // Don't show: In a live label and a background sync fails
3994 final boolean avoidToast = !showToast && (folder.syncWindow > 0
James Lemieuxdeee5be2014-09-29 17:51:28 -07003995 || (syncStatus & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003996 if (avoidToast) {
3997 return;
3998 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003999 listener = getRetryClickedListener(folder);
4000 actionTextResourceId = R.string.retry;
4001 break;
4002 case UIProvider.LastSyncResult.AUTH_ERROR:
4003 listener = getSignInClickedListener();
4004 actionTextResourceId = R.string.signin;
4005 break;
4006 case UIProvider.LastSyncResult.SECURITY_ERROR:
4007 return; // Currently we do nothing for security errors.
4008 case UIProvider.LastSyncResult.STORAGE_ERROR:
4009 listener = getStorageErrorClickedListener();
4010 actionTextResourceId = R.string.info;
4011 break;
4012 case UIProvider.LastSyncResult.INTERNAL_ERROR:
4013 listener = getInternalErrorClickedListener();
4014 actionTextResourceId = R.string.report;
4015 break;
4016 default:
4017 return;
4018 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004019 mToastBar.show(listener,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07004020 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004021 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07004022 replaceVisibleToast,
James Lemieux0ec03e82014-09-03 14:01:53 -07004023 true /* autohide */,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07004024 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004025 }
4026
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004027 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004028 return new ActionClickedListener() {
4029 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004030 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004031 final Uri uri = folder.refreshUri;
4032
4033 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004034 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07004035 }
4036 }
4037 };
4038 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004039
4040 private ActionClickedListener getSignInClickedListener() {
4041 return new ActionClickedListener() {
4042 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004043 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004044 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004045 }
4046 };
4047 }
4048
4049 private ActionClickedListener getStorageErrorClickedListener() {
4050 return new ActionClickedListener() {
4051 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004052 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004053 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004054 }
4055 };
4056 }
4057
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004058 private void showStorageErrorDialog() {
4059 DialogFragment fragment = (DialogFragment)
4060 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
4061 if (fragment == null) {
4062 fragment = SyncErrorDialogFragment.newInstance();
4063 }
4064 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
4065 }
4066
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004067 private ActionClickedListener getInternalErrorClickedListener() {
4068 return new ActionClickedListener() {
4069 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07004070 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08004071 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07004072 }
4073 };
4074 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004075
4076 @Override
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004077 public void onFooterViewLoadMoreClick(Folder folder) {
4078 if (folder != null && folder.loadMoreUri != null) {
4079 startAsyncRefreshTask(folder.loadMoreUri);
4080 }
4081 }
4082
4083 private void startAsyncRefreshTask(Uri uri) {
4084 if (mFolderSyncTask != null) {
4085 mFolderSyncTask.cancel(true);
4086 }
4087 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4088 mFolderSyncTask.execute();
4089 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004090
4091 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004092 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004093 final Intent authenticationIntent =
4094 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4095 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4096 }
4097 }
mindypca87de42012-09-28 15:02:39 -07004098
4099 @Override
4100 public void onAccessibilityStateChanged() {
4101 // Clear the cache of objects.
4102 ConversationItemViewModel.onAccessibilityUpdated();
4103 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004104 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004105 if (frag != null) {
4106 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4107 if (adapter != null) {
4108 adapter.notifyDataSetInvalidated();
4109 }
4110 }
4111 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004112
4113 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07004114 public void makeDialogListener (final int action, final boolean isBatch,
4115 UndoCallback undoCallback) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004116 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004117 if (isBatch) {
Jin Caoec0fa482014-08-28 16:38:08 -07004118 target = mCheckedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004119 } else {
4120 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4121 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004122 }
Jin Cao30c881a2014-04-08 14:28:36 -07004123 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch,
4124 undoCallback);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004125 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004126 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004127 mDialogListener = new AlertDialog.OnClickListener() {
4128 @Override
4129 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004130 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004131 // Afterwards, let's remove references to the listener and the action.
4132 setListener(null, -1);
4133 }
4134 };
4135 }
4136
4137 @Override
4138 public AlertDialog.OnClickListener getListener() {
4139 return mDialogListener;
4140 }
4141
4142 /**
4143 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4144 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4145 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004146 * @param listener the listener that will perform the task for this dialog's positive action.
4147 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004148 */
4149 private void setListener(AlertDialog.OnClickListener listener, final int action){
4150 mDialogListener = listener;
4151 mDialogAction = action;
4152 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004153
4154 @Override
4155 public VeiledAddressMatcher getVeiledAddressMatcher() {
4156 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004157 }
4158
4159 @Override
4160 public void setDetachedMode() {
4161 // Tell the conversation list not to select anything.
4162 final ConversationListFragment frag = getConversationListFragment();
4163 if (frag != null) {
4164 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004165 } else if (mIsTablet) {
4166 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004167 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4168 }
4169 mDetachedConvUri = mCurrentConversation.uri;
4170 }
4171
4172 private void clearDetachedMode() {
4173 // Tell the conversation list to go back to its usual selection behavior.
4174 final ConversationListFragment frag = getConversationListFragment();
4175 if (frag != null) {
4176 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004177 } else if (mIsTablet) {
4178 // How did we ever land here? Detached mode, and no CLF on tablet???
4179 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004180 }
4181 mDetachedConvUri = null;
4182 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004183
Andy Huang61f26c22014-03-13 18:24:52 -07004184 @Override
Andy Huang8712f412014-08-21 23:10:41 -07004185 public boolean shouldPreventListSwipesEntirely() {
4186 return false;
4187 }
4188
4189 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004190 public DrawerController getDrawerController() {
4191 return mDrawerListener;
4192 }
4193
4194 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener>
4195 implements DrawerLayout.DrawerListener, DrawerController {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004196 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004197 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004198
4199 public MailDrawerListener() {
4200 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004201 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004202 }
4203
Andy Huang12b3ee42013-04-24 22:49:43 -07004204 @Override
Andy Huang3e95fe92014-07-25 17:46:33 -07004205 public boolean isDrawerEnabled() {
4206 return AbstractActivityController.this.isDrawerEnabled();
4207 }
4208
4209 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004210 public void registerDrawerListener(DrawerLayout.DrawerListener l) {
4211 registerObserver(l);
4212 }
4213
4214 @Override
4215 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) {
4216 unregisterObserver(l);
4217 }
4218
4219 @Override
4220 public boolean isDrawerOpen() {
4221 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
4222 }
4223
4224 @Override
4225 public boolean isDrawerVisible() {
4226 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout);
4227 }
4228
4229 @Override
Andy Huangf58e4c32014-07-09 16:58:18 -07004230 public void toggleDrawerState() {
4231 AbstractActivityController.this.toggleDrawerState();
4232 }
4233
4234 @Override
Andy Huang12b3ee42013-04-24 22:49:43 -07004235 public void onDrawerOpened(View drawerView) {
4236 mDrawerToggle.onDrawerOpened(drawerView);
Andy Huang61f26c22014-03-13 18:24:52 -07004237
4238 for (DrawerLayout.DrawerListener l : mObservers) {
4239 l.onDrawerOpened(drawerView);
4240 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004241 }
4242
4243 @Override
4244 public void onDrawerClosed(View drawerView) {
4245 mDrawerToggle.onDrawerClosed(drawerView);
4246 if (mHasNewAccountOrFolder) {
4247 refreshDrawer();
4248 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004249
4250 // When closed, we want to use either the burger, or up, based on where we are
4251 final int mode = mViewMode.getMode();
Jin Cao9695e002014-05-29 11:56:44 -07004252 final boolean isTopLevel = Folder.isRoot(mFolder);
Rohan Shah207e6622014-09-29 14:47:31 -07004253 updateDrawerIndicator(mode, isTopLevel);
Andy Huang61f26c22014-03-13 18:24:52 -07004254
4255 for (DrawerLayout.DrawerListener l : mObservers) {
4256 l.onDrawerClosed(drawerView);
4257 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004258 }
4259
4260 /**
4261 * As part of the overriden function, it will animate the alpha of the conversation list
4262 * view along with the drawer sliding when we're in the process of switching accounts or
4263 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4264 */
4265 @Override
4266 public void onDrawerSlide(View drawerView, float slideOffset) {
4267 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4268 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4269 mListViewForAnimating.setAlpha(slideOffset);
4270 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004271
4272 // This code handles when to change the visibility of action items
4273 // based on drawer state. The basic logic is that right when we
4274 // open the drawer, we hide the action items. We show the action items
4275 // when the drawer closes. However, due to the animation of the drawer closing,
4276 // to make the reshowing of the action items feel right, we make the items visible
4277 // slightly sooner.
4278 //
4279 // However, to make the animating behavior work properly, we have to know whether
4280 // we're animating open or closed. Only if we're animating closed do we want to
4281 // show the action items early. We save the last slide offset so that we can compare
4282 // the current slide offset to it to determine if we're opening or closing.
4283 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4284 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4285 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004286 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004287 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004288 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4289 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004290 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004291 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004292 }
4293 } else {
4294 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4295 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004296 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004297 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004298 } else if (!mHideMenuItems && slideOffset > 0.f) {
4299 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004300 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004301 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004302 }
4303 }
4304
4305 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004306
Andy Huang61f26c22014-03-13 18:24:52 -07004307 for (DrawerLayout.DrawerListener l : mObservers) {
4308 l.onDrawerSlide(drawerView, slideOffset);
4309 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004310 }
4311
4312 /**
4313 * This condition here should only be called when the drawer is stuck in a weird state
4314 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4315 * and, more importantly, unlock the drawer when this is the case.
4316 */
4317 @Override
4318 public void onDrawerStateChanged(int newState) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004319 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004320 mDrawerState = newState;
4321 mDrawerToggle.onDrawerStateChanged(mDrawerState);
Andy Huang61f26c22014-03-13 18:24:52 -07004322
4323 for (DrawerLayout.DrawerListener l : mObservers) {
4324 l.onDrawerStateChanged(newState);
4325 }
4326
Andy Huange764cfd2014-02-26 11:55:03 -08004327 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004328 return;
4329 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004330 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4331 if (mHasNewAccountOrFolder) {
4332 refreshDrawer();
4333 }
4334 if (mConversationListLoadFinishedIgnored) {
4335 mConversationListLoadFinishedIgnored = false;
4336 final Bundle args = new Bundle();
4337 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4338 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4339 mActivity.getLoaderManager().initLoader(
4340 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4341 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004342 }
4343 }
4344
4345 /**
4346 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4347 * conversation list, and finish end actions. Also, make
4348 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4349 */
4350 public void refreshDrawer() {
4351 mHasNewAccountOrFolder = false;
4352 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4353 ConversationListFragment conversationList = getConversationListFragment();
4354 if (conversationList != null) {
4355 conversationList.clear();
4356 }
Tony Mantler54022ee2014-07-07 13:43:35 -07004357 mFolderOrAccountObservers.notifyChanged();
Andy Huang12b3ee42013-04-24 22:49:43 -07004358 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004359
4360 /**
4361 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4362 * by {@link #onDrawerStateChanged(int)}.
4363 * @return The {@link DrawerLayout}'s current state. One of
4364 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4365 * or {@link DrawerLayout#STATE_SETTLING}.
4366 */
4367 public int getDrawerState() {
4368 return mDrawerState;
4369 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004370 }
4371
Scott Kennedy8a72b852013-05-02 14:18:50 -07004372 @Override
4373 public boolean isDrawerPullEnabled() {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004374 return true;
Scott Kennedy8a72b852013-05-02 14:18:50 -07004375 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004376
4377 @Override
4378 public boolean shouldHideMenuItems() {
4379 return mHideMenuItems;
4380 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004381
4382 protected void navigateUpFolderHierarchy() {
4383 new AsyncTask<Void, Void, Folder>() {
4384 @Override
4385 protected Folder doInBackground(final Void... params) {
4386 if (mInbox == null) {
4387 // We don't have an inbox, but we need it
4388 final Cursor cursor = mContext.getContentResolver().query(
4389 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4390 null, null);
4391
4392 if (cursor != null) {
4393 try {
4394 if (cursor.moveToFirst()) {
4395 mInbox = new Folder(cursor);
4396 }
4397 } finally {
4398 cursor.close();
4399 }
4400 }
4401 }
4402
4403 // Now try to load our parent
4404 final Folder folder;
4405
Alice Yang5ac10032013-09-04 06:41:43 +00004406 if (mFolder != null) {
Martin Hibdone78c40f2013-10-10 18:29:25 -07004407 Cursor cursor = null;
4408 try {
4409 cursor = mContext.getContentResolver().query(mFolder.parent,
4410 UIProvider.FOLDERS_PROJECTION, null, null, null);
Alice Yangebeef1b2013-09-04 06:41:10 +00004411
Martin Hibdone78c40f2013-10-10 18:29:25 -07004412 if (cursor == null || !cursor.moveToFirst()) {
4413 // We couldn't load the parent, so use the inbox
4414 folder = mInbox;
4415 } else {
Alice Yang5ac10032013-09-04 06:41:43 +00004416 folder = new Folder(cursor);
Martin Hibdone78c40f2013-10-10 18:29:25 -07004417 }
4418 } finally {
4419 if (cursor != null) {
Alice Yang5ac10032013-09-04 06:41:43 +00004420 cursor.close();
4421 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004422 }
Alice Yang5ac10032013-09-04 06:41:43 +00004423 } else {
4424 folder = mInbox;
Alice Yangebeef1b2013-09-04 06:41:10 +00004425 }
4426
4427 return folder;
4428 }
4429
4430 @Override
4431 protected void onPostExecute(final Folder result) {
4432 onFolderSelected(result);
4433 }
4434 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4435 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004436
4437 @Override
4438 public Parcelable getConversationListScrollPosition(final String folderUri) {
4439 return mConversationListScrollPositions.getParcelable(folderUri);
4440 }
4441
4442 @Override
4443 public void setConversationListScrollPosition(final String folderUri,
4444 final Parcelable savedPosition) {
4445 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4446 }
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004447
4448 @Override
Jin Cao84662ce2014-09-24 13:09:42 -07004449 public boolean setupEmptyIconView(Folder folder, boolean isEmpty) {
4450 return false;
4451 }
4452
4453 @Override
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004454 public View.OnClickListener getNavigationViewClickListener() {
4455 return mHomeButtonListener;
4456 }
4457
4458 // TODO: Fold this into the outer class when b/16627877 is fixed
4459 private class HomeButtonListener implements View.OnClickListener {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004460 @Override
4461 public void onClick(View v) {
Jin Cao405a3442014-08-25 13:49:33 -07004462 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004463 }
4464 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004465}