blob: 1c905db12f6b656dbfd91b92afefe199bb1fa8c0 [file] [log] [blame]
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001/*******************************************************************************
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
Andy Huang12b3ee42013-04-24 22:49:43 -070020import android.animation.ValueAnimator;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080021import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070022import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080023import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070024import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070025import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070026import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070027import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070028import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070029import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080030import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070031import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080032import android.content.Context;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070033import android.content.DialogInterface;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080034import android.content.DialogInterface.OnClickListener;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080035import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080036import android.content.Loader;
Paul Westbrook57246a42013-04-21 09:40:22 -070037import android.content.res.Configuration;
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -080038import android.content.res.Resources;
Alice Yangebeef1b2013-09-04 06:41:10 +000039import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070040import android.database.DataSetObservable;
41import android.database.DataSetObserver;
Andy Huang61f26c22014-03-13 18:24:52 -070042import android.database.Observable;
Paul Westbrook23b74b92012-02-29 11:36:12 -080043import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070044import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080045import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070046import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070047import android.os.Parcelable;
Jin Cao1a864cc2014-05-21 11:16:39 -070048import android.os.SystemClock;
Jin Caoc6801eb2014-08-12 18:16:57 -070049import android.speech.RecognizerIntent;
Andy Huang12b3ee42013-04-24 22:49:43 -070050import android.support.v4.widget.DrawerLayout;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070051import android.support.v7.app.ActionBar;
Andrew Sapperstein355dd902014-09-10 12:46:44 -070052import android.support.v7.app.ActionBarDrawerToggle;
Jin Cao41733e32014-09-15 12:02:34 -070053import android.text.TextUtils;
Andy Huang12b3ee42013-04-24 22:49:43 -070054import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080055import android.view.KeyEvent;
56import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080057import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080058import android.view.MenuItem;
59import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070060import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070061import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070062import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080063
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080064import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080065import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070066import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070067import com.android.mail.analytics.Analytics;
Jin Cao779dd602014-04-22 16:16:28 -070068import com.android.mail.analytics.AnalyticsTimer;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080069import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070070import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080071import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070072import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070073import com.android.mail.browse.ConversationMessage;
Paul Westbrookbf232c32012-04-18 03:17:41 -070074import com.android.mail.browse.ConversationPagerController;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070075import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070076import com.android.mail.browse.SyncErrorDialogFragment;
Jin Cao30c881a2014-04-08 14:28:36 -070077import com.android.mail.browse.UndoCallback;
Mindy Pereira9b875682012-02-15 18:10:54 -080078import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080079import com.android.mail.content.CursorCreator;
80import com.android.mail.content.ObjectCursor;
81import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080082import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080083import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070084import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080085import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070086import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070087import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080088import com.android.mail.providers.Settings;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080089import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070090import com.android.mail.providers.UIProvider.AccountCapabilities;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070091import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070092import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070093import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080094import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070095import com.android.mail.providers.UIProvider.FolderCapabilities;
Scott Kennedya158ac82013-09-04 13:48:13 -070096import com.android.mail.providers.UIProvider.FolderType;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070097import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070098import com.android.mail.utils.ContentProviderTask;
Andy Huang144bfe72013-06-11 13:27:52 -070099import com.android.mail.utils.DrawIdler;
Paul Westbrookb334c902012-06-25 11:42:46 -0700100import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800101import com.android.mail.utils.LogUtils;
Andy Huang61f26c22014-03-13 18:24:52 -0700102import com.android.mail.utils.MailObservable;
Scott Kennedycb85aea2013-02-25 13:08:32 -0800103import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800104import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800105import com.android.mail.utils.VeiledAddressMatcher;
Paul Westbrookca08fc12012-07-31 12:01:15 -0700106import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700107import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700108import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700109import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800110
Marc Blank167faa82012-03-21 13:11:53 -0700111import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800112import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700113import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700114import java.util.Collections;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700115import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700116import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800117import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700118import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800119
120
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800121/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800122 * This is an abstract implementation of the Activity Controller. This class
123 * knows how to respond to menu items, state changes, layout changes, etc. It
124 * weaves together the views and listeners, dispatching actions to the
125 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800126 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800127 * Even though this class is abstract, it should provide default implementations
128 * for most, if not all the methods in the ActivityController interface. This
129 * makes the task of the subclasses easier: OnePaneActivityController and
130 * TwoPaneActivityController can be concise when the common functionality is in
131 * AbstractActivityController.
132 * </p>
133 * <p>
134 * In the Gmail codebase, this was called BaseActivityController
135 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800136 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700137public abstract class AbstractActivityController implements ActivityController,
James Lemieux5d79a912014-07-16 14:22:26 -0700138 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener, View.OnClickListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800139 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700140 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800141 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700142 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700143 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700144 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700145 private static final String SAVED_CONVERSATION = "saved-conversation";
Jin Caoec0fa482014-08-28 16:38:08 -0700146 /** Tag for {@link #mCheckedSet} */
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700147 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700148 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700149 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700150 /** Tag for {@link #mFolderListFolder} */
151 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700152 /** Tag for {@link ConversationListContext#searchQuery} */
153 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800154 /** Tag for {@link #mDialogAction} */
155 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800156 /** Tag for {@link #mDialogFromSelectedSet} */
157 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800158 /** Tag for {@link #mDetachedConvUri} */
159 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Alice Yangebeef1b2013-09-04 06:41:10 +0000160 /** Key to store {@link #mInbox}. */
Scott Kennedyf77806e2013-08-30 11:38:15 -0700161 private static final String SAVED_INBOX_KEY = "m-inbox";
162 /** Key to store {@link #mConversationListScrollPositions} */
163 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS =
164 "saved-conversation-list-scroll-positions";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800165
Greg Bullockede2e522014-05-30 14:11:35 +0200166 /** Tag used when loading a wait fragment */
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700167 protected static final String TAG_WAIT = "wait-fragment";
168 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700169 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Scott Kennedy103319a2013-07-26 13:35:35 -0700170 /** Tag used when loading a custom fragment. */
171 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700172
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700173 /** Key to store an account in a bundle */
174 private final String BUNDLE_ACCOUNT_KEY = "account";
175 /** Key to store a folder in a bundle */
176 private final String BUNDLE_FOLDER_KEY = "folder";
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700177 /**
178 * Key to set a flag for the ConversationCursorLoader to ignore any
179 * initial load limit that may be set by the Account. Instead,
180 * perform a full load instead of the full-stage load.
181 */
182 private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY =
183 "ignore-initial-conversation-limit";
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700184
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800185 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700186 protected Folder mFolder;
Alice Yangebeef1b2013-09-04 06:41:10 +0000187 protected Folder mInbox;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700188 /** True when {@link #mFolder} is first shown to the user. */
189 private boolean mFolderChanged = false;
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700190 protected ActionBarController mActionBarController;
James Lemieux10fcd642014-03-03 13:01:04 -0800191 protected final MailActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800192 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700193 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800194 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800195 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800196 protected Conversation mCurrentConversation;
Jin Caoc6801eb2014-08-12 18:16:57 -0700197 protected MaterialSearchViewController mSearchViewController;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800198 /**
199 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
200 */
201 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800202
Scott Kennedyf77806e2013-08-30 11:38:15 -0700203 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */
204 private final Bundle mConversationListScrollPositions = new Bundle();
205
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700206 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
207 private SuppressNotificationReceiver mNewEmailReceiver = null;
208
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800209 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700210 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700211
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800212 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800213 * The current mode of the application. All changes in mode are initiated by
214 * the activity controller. View mode changes are propagated to classes that
215 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800216 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800217 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800218 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700219 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800220 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800221
Andy Huang4e0158f2012-08-07 21:06:01 -0700222 private boolean mDestroyed;
223
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800224 /** True if running on tablet */
225 private final boolean mIsTablet;
226
Andy Huang1ee96b22012-08-24 20:19:53 -0700227 /**
228 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
229 * transactions? (including back stack manipulation)
230 * <p>
231 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
232 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
233 * and onResume.
234 */
235 private boolean mSafeToModifyFragments = true;
236
Paul Westbrook23b74b92012-02-29 11:36:12 -0800237 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700238 protected ConversationCursor mConversationListCursor;
Andy Huang61f26c22014-03-13 18:24:52 -0700239 private final DataSetObservable mConversationListObservable = new MailObservable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700240
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800241 /** Runnable that checks the logging level to enable/disable the logging service. */
242 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700243 /** List of all accounts currently known to the controller. This is never null. */
244 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800245
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700246 private FolderWatcher mFolderWatcher;
247
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -0700248 private boolean mIgnoreInitialConversationLimit;
249
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800250 /**
251 * Interface for actions that are deferred until after a load completes. This is for handling
252 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
253 * that cursor is loaded.
254 */
255 private interface LoadFinishedCallback {
256 void onLoadFinished();
257 }
258
259 /** The deferred actions to execute when mConversationListCursor load completes. */
260 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
261 new ArrayList<LoadFinishedCallback>();
262
Marc Blankbf128eb2012-04-18 15:58:45 -0700263 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700264
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700265 /** Listeners that are interested in changes to the current account. */
Andy Huang61f26c22014-03-13 18:24:52 -0700266 private final DataSetObservable mAccountObservers = new MailObservable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700267 /** Listeners that are interested in changes to the recent folders. */
Andy Huang61f26c22014-03-13 18:24:52 -0700268 private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder");
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700269 /** Listeners that are interested in changes to the list of all accounts. */
Andy Huang61f26c22014-03-13 18:24:52 -0700270 private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700271 /** Listeners that are interested in changes to the current folder. */
Andy Huang61f26c22014-03-13 18:24:52 -0700272 private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder");
Tony Mantler54022ee2014-07-07 13:43:35 -0700273 /** Listeners that are interested in changes to the Folder or Account selection */
274 private final DataSetObservable mFolderOrAccountObservers =
275 new MailObservable("FolderOrAccount");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700276
Mindy Pereira967ede62012-03-22 09:29:09 -0700277 /**
278 * Selected conversations, if any.
279 */
Jin Caoec0fa482014-08-28 16:38:08 -0700280 private final ConversationCheckedSet mCheckedSet = new ConversationCheckedSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800281
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700282 private final int mFolderItemUpdateDelayMs;
283
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700284 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700285 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700286
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700287 /**
288 * Action menu associated with the selected set.
289 */
290 SelectedConversationsActionMenu mCabActionMenu;
James Lemieux1a1e9752014-07-18 13:50:40 -0700291
292 /** The compose button floating over the conversation/search lists */
293 protected View mFloatingComposeButton;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700294 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700295 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700296
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700297 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700298 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700299 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700300 private final ConversationListLoaderCallbacks mListCursorCallbacks =
301 new ConversationListLoaderCallbacks();
302
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800303 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
304 private final FolderLoads mFolderCallbacks = new FolderLoads();
305 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
306 private final AccountLoads mAccountCallbacks = new AccountLoads();
307
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800308 /**
309 * Matched addresses that must be shielded from users because they are temporary. Even though
310 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
311 * to live past the life of an account.
312 */
313 private final VeiledAddressMatcher mVeiledMatcher;
314
Paul Westbrookb334c902012-06-25 11:42:46 -0700315 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700316
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700317 // Loader constants: Accounts
318 /**
319 * The list of accounts. This loader is started early in the application life-cycle since
320 * the list of accounts is central to all other data the application needs: unread counts for
321 * folders, critical UI settings like show/hide checkboxes, ...
322 * The loader is started when the application is created: both in
323 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
324 * destroyed since the cursor is needed through the life of the application. When the list of
325 * accounts changes, we notify {@link #mAllAccountObservers}.
326 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800327 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700328
329 /**
330 * The current account. This loader is started when we have an account. The mail application
331 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
332 * we start a loader to observe for changes on the current account.
333 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
334 * When the current account object changes, we notify {@link #mAccountObservers}.
335 * A possible performance improvement would be to listen purely on
336 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
337 * and would avoid two updates when a single setting on the current account changes.
338 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200339 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 1;
340
341 // Loader constants: Conversations
342
343 /** The conversation cursor over the current conversation list. This loader provides
344 * a cursor over conversation entries from a folder to display a conversation
345 * list.
346 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
347 * or when the controller is told that a folder/account change is imminent
348 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
349 * the current folder. When the user switches folders, the old loader is destroyed and a new
350 * one is created.
351 *
352 * When the conversation list changes, we notify {@link #mConversationListObservable}.
353 */
354 private static final int LOADER_CONVERSATION_LIST = 10;
355
356 // Loader constants: misc
357 /**
358 * The loader that determines whether the Warm welcome tour should be displayed for the user.
359 */
360 public static final int LOADER_WELCOME_TOUR = 20;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700361
Greg Bullock944ef3b2014-07-17 12:15:07 +0200362 /**
363 * The load which loads accounts for the welcome tour.
364 */
365 public static final int LOADER_WELCOME_TOUR_ACCOUNTS = 21;
366
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700367 // Loader constants: Folders
Régis Décamps004b46f2014-06-23 15:13:23 +0200368
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700369 /** The current folder. This loader watches for updates to the current folder in a manner
370 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
371 * might be due to server-side changes (unread count), or local changes (sync window or sync
372 * status change).
373 * The change of current folder calls {@link #updateFolder(Folder)}.
374 * This is responsible for restarting a loader using the URI of the provided folder. When the
375 * loader returns, the current folder is updated and consumers, if any, are notified.
376 * When the current folder changes, we notify {@link #mFolderObservable}
377 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200378 private static final int LOADER_FOLDER_CURSOR = 30;
379
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700380 /**
381 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
382 * folders are tied to the current account being viewed. When the account is changed,
383 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
384 * phones historically, when they were displayed in the spinner. On the tablet,
385 * they showed in the {@link FolderListFragment} and were not-populated. The code to
386 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
387 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
388 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
389 * Recent folders are needed for the life of the current account.
390 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
391 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200392 private static final int LOADER_RECENT_FOLDERS = 31;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700393 /**
394 * The primary inbox for the current account. The mechanism to load the default inbox for the
395 * current account is (sadly) different from loading other folders. The method
396 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
397 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
398 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
399 * over the current folder.
400 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
401 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200402 private static final int LOADER_ACCOUNT_INBOX = 32;
403
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700404 /**
405 * The fake folder of search results for a term. When we search for a term,
406 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
407 * we start a loader which returns conversations that match the user-provided query.
408 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
409 * a new activity.
410 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200411 private static final int LOADER_SEARCH = 33;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700412 /**
413 * The initial folder at app start. When the application is launched from an intent that
414 * specifies the initial folder (notifications/widgets/shortcuts),
415 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
416 * shortcuts and widgets persist past application update, they might have incorrect
417 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
418 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
419 * An additional complication arises if we have to view a specific conversation within this
420 * folder. This is the case when launching the app from a single conversation notification
421 * or tapping on a specific conversation in the widget. In these cases, the conversation is
422 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
423 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200424 public static final int LOADER_FIRST_FOLDER = 34;
Régis Décampsde21e892014-06-19 20:59:13 +0200425
426 /**
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700427 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
428 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
429 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
430 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
431 * other class that uses this activity's LoaderManager. If another class needs activity-level
432 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
433 * perhaps.
434 */
Régis Décamps004b46f2014-06-23 15:13:23 +0200435 public static final int LAST_LOADER_ID = 35;
436
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800437 /**
438 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
439 * fragments, and within an activity, loader IDs need to be unique. Currently,
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700440 * SectionedInboxTeaserView is the only class that uses the
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800441 * {@link ConversationListFragment}'s LoaderManager.
442 */
443 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800444
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700445 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700446 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700447 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700448 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Martin Hibdon371a71c2014-02-19 13:55:28 -0800449 /** Code returned when the previous activity needs to navigate to a different folder
450 * or account */
451 private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3;
452
Greg Bullock406ae082014-08-27 17:03:16 +0200453 /** Code returned from voice search intent */
454 public static final int VOICE_SEARCH_REQUEST_CODE = 4;
455
Martin Hibdon371a71c2014-02-19 13:55:28 -0800456 public static final String EXTRA_FOLDER = "extra-folder";
457 public static final String EXTRA_ACCOUNT = "extra-account";
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700458
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700459 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
460 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700461 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700462 private Folder mFolderListFolder;
Martin Hibdone78c40f2013-10-10 18:29:25 -0700463 private final int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700464 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700465 /** A wait fragment we added, if any. */
466 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800467 /** True if we have results from a search query */
468 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800469 /** If a confirmation dialog is being show, the listener for the positive action. */
470 private OnClickListener mDialogListener;
471 /**
472 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
473 * is used to create a new {@link #mDialogListener} on orientation changes.
474 */
475 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800476 /**
477 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
478 * and false if it acts on the currently selected conversation
479 */
480 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800481
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800482 /** Which conversation to show, if started from widget/notification. */
483 private Conversation mConversationToShow = null;
484
Andy Huangc94d07f2013-06-03 16:19:35 -0700485 /**
486 * A temporary reference to the pending destructive action that was deferred due to an
487 * auto-advance transition in progress.
488 * <p>
489 * In detail: when auto-advance triggers a mode change, we must wait until the transition
490 * completes before executing the destructive action to ensure a smooth mode change transition.
491 * This member variable houses the pending destructive action work to be run upon completion.
492 */
493 private Runnable mAutoAdvanceOp = null;
494
Andy Huang12b3ee42013-04-24 22:49:43 -0700495 protected DrawerLayout mDrawerContainer;
496 protected View mDrawerPullout;
497 protected ActionBarDrawerToggle mDrawerToggle;
Andy Huangf58e4c32014-07-09 16:58:18 -0700498
Andy Huang12b3ee42013-04-24 22:49:43 -0700499 protected ListView mListViewForAnimating;
500 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700501 private boolean mConversationListLoadFinishedIgnored;
Andy Huang61f26c22014-03-13 18:24:52 -0700502 private final MailDrawerListener mDrawerListener = new MailDrawerListener();
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700503 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700504
Andy Huang144bfe72013-06-11 13:27:52 -0700505 private final DrawIdler mDrawIdler = new DrawIdler();
506
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700507 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700508
Scott Kennedycb85aea2013-02-25 13:08:32 -0800509 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
510 @Override
511 public void onChanged() {
512 super.onChanged();
513
514 if (mConversationListCursor != null) {
515 mConversationListCursor.handleNotificationActions();
516 }
517 }
518 };
519
Andrew Sapperstein53de4482014-07-29 02:39:39 +0000520 private final HomeButtonListener mHomeButtonListener = new HomeButtonListener();
521
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800522 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
523 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700524 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800525 mViewMode = viewMode;
526 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700527 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700528 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700529 // Allow the fragment to observe changes to its own selection set. No other object is
530 // aware of the selected set.
Jin Caoec0fa482014-08-28 16:38:08 -0700531 mCheckedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700532
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800533 final Resources r = mContext.getResources();
534 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
535 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800536 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800537 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700538 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800539 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800540
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800541 public Account getCurrentAccount() {
542 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800543 }
544
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800545 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800546 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800547 }
548
549 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700550 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700551 return mConversationListCursor;
552 }
553
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700554 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700555 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800556 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700557 * @return true if the fragment is valid, false otherwise
558 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800559 private static boolean isValidFragment(Fragment in) {
560 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700561 }
562
563 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700564 * Get the conversation list fragment for this activity. If the conversation list fragment is
565 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700566 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700567 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
568 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
569 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
570 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
571 * need the fragment immediately after adding it, consider making the fragment an observer of
572 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700573 */
574 protected ConversationListFragment getConversationListFragment() {
575 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700576 if (isValidFragment(fragment)) {
577 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700578 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700579 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700580 }
581
582 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700583 * Returns the folder list fragment attached with this activity. If no such fragment is attached
584 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700585 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700586 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
587 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
588 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
589 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
590 * need the fragment immediately after adding it, consider making the fragment an observer of
591 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700592 */
593 protected FolderListFragment getFolderListFragment() {
James Lemieux10fcd642014-03-03 13:01:04 -0800594 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
595 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700596 if (isValidFragment(fragment)) {
597 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700598 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700599 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700600 }
601
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800602 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800603 * Initialize the action bar. This is not visible to OnePaneController and
604 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800605 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700606 private void initializeActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700607 final ActionBar actionBar = mActivity.getSupportActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700608 if (actionBar == null) {
609 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700610 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700611
Jin Caoc6801eb2014-08-12 18:16:57 -0700612 mActionBarController = new ActionBarController(mContext);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700613 mActionBarController.initialize(mActivity, this, actionBar);
Jin Caoc6801eb2014-08-12 18:16:57 -0700614 actionBar.setShowHideAnimationEnabled(false);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700615
Andy Huang12b3ee42013-04-24 22:49:43 -0700616 // init the action bar to allow the 'up' affordance.
617 // any configurations that disallow 'up' should do that later.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700618 mActionBarController.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700619 }
620
621 /**
622 * Attach the action bar to the activity.
623 */
624 private void attachActionBar() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700625 final ActionBar actionBar = mActivity.getSupportActionBar();
Tony Mantlerbd091502013-09-16 13:59:47 -0700626 if (actionBar != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700627 // Show a title
628 final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700629 actionBar.setDisplayOptions(mask, mask);
Andrew Sapperstein2d86d112014-07-24 20:27:37 -0700630 mActionBarController.setViewModeController(mViewMode);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800631 }
632 }
633
634 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800635 * Returns whether the conversation list fragment is visible or not.
636 * Different layouts will have their own notion on the visibility of
637 * fragments, so this method needs to be overriden.
638 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800639 */
640 protected abstract boolean isConversationListVisible();
641
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700642 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700643 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700644 */
645 final void perhapsEnterWaitMode() {
646 // If the account is not initialized, then show the wait fragment, since nothing can be
647 // shown.
648 if (mAccount.isAccountInitializationRequired()) {
649 showWaitForInitialization();
650 return;
651 }
652
653 final boolean inWaitingMode = inWaitMode();
654 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
655 if (isSyncRequired) {
656 if (inWaitingMode) {
657 // Update the WaitFragment's account object
658 updateWaitMode();
659 } else {
660 // Transition to waiting mode
661 showWaitForInitialization();
662 }
663 } else if (inWaitingMode) {
664 // Dismiss waiting mode
665 hideWaitForInitialization();
666 }
667 }
668
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800669 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700670 public void switchToDefaultInboxOrChangeAccount(Account account) {
671 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Andy Huange764cfd2014-02-26 11:55:03 -0800672 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -0800673 // We are in an activity on top of the main navigation activity.
674 // We need to return to it with a result code that indicates it should navigate to
675 // a different folder.
676 final Intent intent = new Intent();
677 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
678 mActivity.setResult(Activity.RESULT_OK, intent);
679 mActivity.finish();
680 return;
681 }
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700682 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700683 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700684 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700685 if (switchToDefaultInbox) {
686 loadAccountInbox();
687 return;
688 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700689 changeAccount(account);
690 }
691
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700692 public void changeAccount(Account account) {
693 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
694 // Is the account or account settings different from the existing account?
695 final boolean firstLoad = mAccount == null;
696 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
697
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800698 // If nothing has changed, return early without wasting any more time.
699 if (!accountChanged && !account.settingsDiffer(mAccount)) {
700 return;
701 }
702 // We also don't want to do anything if the new account is null
703 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700704 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800705 return;
706 }
Tony Mantler79b11562013-10-09 15:31:50 -0700707 final String emailAddress = account.getEmailAddress();
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800708 mHandler.post(new Runnable() {
709 @Override
710 public void run() {
Tony Mantler79b11562013-10-09 15:31:50 -0700711 MailActivity.setNfcMessage(emailAddress);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700712 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800713 });
714 if (accountChanged) {
715 commitDestructiveActions(false);
716 }
Régis Décamps2168cbc2014-08-22 15:00:37 +0200717
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800718 // Change the account here
719 setAccount(account);
720 // And carry out associated actions.
721 cancelRefreshTask();
722 if (accountChanged) {
723 loadAccountInbox();
724 }
725 // Check if we need to force setting up an account before proceeding.
726 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
727 // Launch the intent!
728 final Intent intent = new Intent(Intent.ACTION_EDIT);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700729
730 intent.setPackage(mContext.getPackageName());
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800731 intent.setData(mAccount.settings.setupIntentUri);
Tony Mantlerec6e2512014-05-22 13:31:10 -0700732
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800733 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800734 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800735 }
736
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700737 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700738 * Adds a listener interested in change in the current account. If a class is storing a
739 * reference to the current account, it should listen on changes, so it can receive updates to
740 * settings. Must happen in the UI thread.
741 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800742 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700743 public void registerAccountObserver(DataSetObserver obs) {
744 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800745 }
746
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700747 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700748 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700749 * Must happen in the UI thread.
750 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700751 @Override
752 public void unregisterAccountObserver(DataSetObserver obs) {
753 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700754 }
755
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700756 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700757 public void registerAllAccountObserver(DataSetObserver observer) {
758 mAllAccountObservers.registerObserver(observer);
759 }
760
761 @Override
762 public void unregisterAllAccountObserver(DataSetObserver observer) {
763 mAllAccountObservers.unregisterObserver(observer);
764 }
765
766 @Override
767 public Account[] getAllAccounts() {
768 return mAllAccounts;
769 }
770
771 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700772 public Account getAccount() {
773 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700774 }
775
Rohan Shah0f73d902013-04-19 17:06:37 -0700776 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700777 public void registerFolderOrAccountChangedObserver(final DataSetObserver observer) {
778 mFolderOrAccountObservers.registerObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700779 }
780
781 @Override
Tony Mantler54022ee2014-07-07 13:43:35 -0700782 public void unregisterFolderOrAccountChangedObserver(final DataSetObserver observer) {
783 mFolderOrAccountObservers.unregisterObserver(observer);
Rohan Shah0f73d902013-04-19 17:06:37 -0700784 }
785
786 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700787 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
788 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
789 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700790 */
791 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700792 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
793 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700794 if (!isDrawerEnabled()) {
Tony Mantler54022ee2014-07-07 13:43:35 -0700795 if (hasNewFolderOrAccount) {
796 mFolderOrAccountObservers.notifyChanged();
797 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700798 return;
799 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700800 // If there are no new folders or accounts to switch to, just close the drawer
801 if (!hasNewFolderOrAccount) {
802 mDrawerContainer.closeDrawers();
803 return;
804 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700805 // Otherwise, start preloading the conversation list for the new folder.
806 if (nextFolder != null) {
807 preloadConvList(nextAccount, nextFolder);
808 }
809 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700810 final ConversationListFragment conversationList = getConversationListFragment();
811 if (conversationList != null) {
812 mListViewForAnimating = conversationList.getListView();
813 } else {
814 // There is no conversation list to animate, so just set it to null
815 mListViewForAnimating = null;
816 }
817
818 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
819 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
820 mHasNewAccountOrFolder = true;
821 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
822 } else {
823 // Drawer is already closed, notify observers that is the case.
Tony Mantler54022ee2014-07-07 13:43:35 -0700824 if (hasNewFolderOrAccount) {
825 mFolderOrAccountObservers.notifyChanged();
826 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700827 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700828 }
829
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700830 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700831 * Load the conversation list early for the given folder. This happens when some UI element
832 * (usually the drawer) instructs the controller that an account change or folder change is
833 * imminent. While the UI element is animating, the controller can preload the conversation
834 * list for the default inbox of the account provided here or to the folder provided here.
835 *
836 * @param nextAccount The account which the app will switch to shortly, possibly null.
837 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700838 */
839 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
840 // Fire off the conversation list loader for this account already with a fake
841 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700842 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700843 if (nextAccount != null) {
844 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
845 } else {
846 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
847 }
848 if (nextFolder != null) {
849 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700850 } else {
851 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700852 }
853 mFolder = null;
854 final LoaderManager lm = mActivity.getLoaderManager();
855 lm.destroyLoader(LOADER_CONVERSATION_LIST);
856 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
857 }
858
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700859 /**
860 * Initiates the async request to create a fake search folder, which returns conversations that
861 * match the query term provided by the user. Returns immediately.
862 * @param intent Intent that the app was started with. This intent contains the search query.
863 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800864 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700865 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700866 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800867 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800868 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800869 }
870
Jin Cao405a3442014-08-25 13:49:33 -0700871 protected void onFolderChanged(Folder folder, final boolean force) {
Andy Huangf58e4c32014-07-09 16:58:18 -0700872 if (isDrawerEnabled()) {
873 /** If the folder doesn't exist, or its parent URI is empty,
874 * this is not a child folder */
875 final boolean isTopLevel = Folder.isRoot(folder);
876 final int mode = mViewMode.getMode();
877 mDrawerToggle.setDrawerIndicatorEnabled(
878 getShouldShowDrawerIndicator(mode, isTopLevel));
879 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700880
Andy Huangf58e4c32014-07-09 16:58:18 -0700881 mDrawerContainer.closeDrawers();
882 }
Alice Yang7dd0e1c2013-09-04 06:43:16 +0000883
884 if (mFolder == null || !mFolder.equals(folder)) {
885 // We are actually changing the folder, so exit cab mode
886 exitCabMode();
887 }
888
Scott Kennedya158ac82013-09-04 13:48:13 -0700889 final String query;
890 if (folder != null && folder.isType(FolderType.SEARCH)) {
891 query = mConvListContext.searchQuery;
892 } else {
893 query = null;
894 }
895
896 changeFolder(folder, query, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700897 }
898
899 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700900 * Sets the folder state without changing view mode and without creating a list fragment, if
901 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800902 * @param folder the folder whose list of conversations are to be shown
903 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700904 */
905 private void setListContext(Folder folder, String query) {
906 updateFolder(folder);
907 if (query != null) {
908 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
909 } else {
910 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
911 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700912 cancelRefreshTask();
913 }
914
915 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700916 * Changes the folder to the value provided here. This causes the view mode to change.
917 * @param folder the folder to change to
918 * @param query if non-null, this represents the search string that the folder represents.
Alice Yangebeef1b2013-09-04 06:41:10 +0000919 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
920 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700921 */
Alice Yangebeef1b2013-09-04 06:41:10 +0000922 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700923 if (!Objects.equal(mFolder, folder)) {
924 commitDestructiveActions(false);
925 }
Alice Yangebeef1b2013-09-04 06:41:10 +0000926 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700927 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700928 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800929 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700930 // Touch the current folder: it is different, and it has been accessed.
931 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800932 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800933 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800934 }
935
Mindy Pereira13c12a62012-05-31 15:41:08 -0700936 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700937 public void onFolderSelected(Folder folder) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000938 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700939 }
940
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700941 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700942 * Adds a listener interested in change in the recent folders. If a class is storing a
943 * reference to the recent folders, it should listen on changes, so it can receive updates.
944 * Must happen in the UI thread.
945 */
946 @Override
947 public void registerRecentFolderObserver(DataSetObserver obs) {
948 mRecentFolderObservers.registerObserver(obs);
949 }
950
951 /**
952 * Removes a listener from receiving recent folder changes.
953 * Must happen in the UI thread.
954 */
955 @Override
956 public void unregisterRecentFolderObserver(DataSetObserver obs) {
957 mRecentFolderObservers.unregisterObserver(obs);
958 }
959
960 @Override
961 public RecentFolderList getRecentFolders() {
962 return mRecentFolderList;
963 }
964
Jin Cao405a3442014-08-25 13:49:33 -0700965 /**
966 * Load the default inbox associated with the current account.
967 */
968 protected void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700969 boolean handled = false;
970 if (mFolderWatcher != null) {
971 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
972 if (inbox != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +0000973 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700974 handled = true;
975 }
976 }
977 if (!handled) {
Paul Westbrook1bf20e02014-02-26 12:48:54 -0800978 LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700979 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
980 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700981 final int mode = mViewMode.getMode();
982 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700983 mViewMode.enterConversationListMode();
984 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700985 }
986
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700987 @Override
988 public void setFolderWatcher(FolderWatcher watcher) {
989 mFolderWatcher = watcher;
990 }
991
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700992 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700993 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
994 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
995 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800996 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700997 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800998 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700999 // We should never try to assign a null folder. But in the rare event that we do, we should
1000 // only set the bit when we have a valid folder, and null is not valid.
1001 if (newFolder == null) {
1002 return;
1003 }
1004 // If the previous folder was null, or if the two folders represent different data, then we
1005 // consider that the folder has changed.
Scott Kennedy259df5b2013-07-11 13:24:01 -07001006 if (mFolder == null || !newFolder.equals(mFolder)) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -07001007 mFolderChanged = true;
1008 }
1009 }
1010
1011 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001012 * Sets the current folder if it is different from the object provided here. This method does
1013 * NOT notify the folder observers that a change has happened. Observers are notified when we
1014 * get an updated folder from the loaders, which will happen as a consequence of this method
1015 * (since this method starts/restarts the loaders).
1016 * @param folder The folder to assign
1017 */
Mindy Pereira11e35962012-06-01 14:49:46 -07001018 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001019 if (folder == null || !folder.isInitialized()) {
1020 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
1021 return;
1022 }
1023 if (folder.equals(mFolder)) {
1024 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
1025 return;
1026 }
1027 final boolean wasNull = mFolder == null;
1028 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
1029 final LoaderManager lm = mActivity.getLoaderManager();
1030 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
1031 // ensure that the folder is different from the previous folder before marking the
1032 // folder changed.
1033 setHasFolderChanged(folder);
1034 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001035
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001036 // We do not need to notify folder observers yet. Instead we start the loaders and
1037 // when the load finishes, we will get an updated folder. Then, we notify the
1038 // folderObservers in onLoadFinished.
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001039 mActionBarController.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001040
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001041 // Only when we switch from one folder to another do we want to restart the
1042 // folder and conversation list loaders (to trigger onCreateLoader).
1043 // The first time this runs when the activity is [re-]initialized, we want to re-use the
1044 // previous loader's instance and data upon configuration change (e.g. rotation).
1045 // If there was not already an instance of the loader, init it.
1046 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001047 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001048 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001049 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001050 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001051 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
1052 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001053 // folders, we want to restart the loader to get the information
1054 // for the newly selected folder
1055 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001056 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001057 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -07001058 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001059 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07001060 args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY,
1061 mIgnoreInitialConversationLimit);
1062 mIgnoreInitialConversationLimit = false;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001063 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001064 }
1065
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001066 @Override
Andy Huang090db1e2012-07-25 13:25:28 -07001067 public Folder getFolder() {
1068 return mFolder;
1069 }
1070
1071 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -07001072 public Folder getHierarchyFolder() {
1073 return mFolderListFolder;
1074 }
1075
Jin Cao405a3442014-08-25 13:49:33 -07001076 /**
1077 * Set the folder currently selected in the folder selection hierarchy fragments.
1078 */
1079 protected void setHierarchyFolder(Folder folder) {
Mindy Pereirac975e842012-07-16 09:15:00 -07001080 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001081 }
1082
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001083 /**
1084 * The mail activity calls other activities for two specific reasons:
1085 * <ul>
1086 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1087 * <li>To update the password on a current account. The result {@link
1088 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1089 * </ul>
1090 * @param requestCode
1091 * @param resultCode
1092 * @param data
1093 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001094 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001095 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001096 switch (requestCode) {
1097 case ADD_ACCOUNT_REQUEST_CODE:
1098 // We were waiting for the user to create an account
1099 if (resultCode == Activity.RESULT_OK) {
1100 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001101 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1102 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001103 } else {
1104 // The user failed to create an account, just exit the app
1105 mActivity.finish();
1106 }
1107 break;
1108 case REAUTHENTICATE_REQUEST_CODE:
1109 if (resultCode == Activity.RESULT_OK) {
1110 // The user successfully authenticated, attempt to refresh the list
1111 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1112 if (refreshUri != null) {
1113 startAsyncRefreshTask(refreshUri);
1114 }
1115 }
1116 break;
Martin Hibdon371a71c2014-02-19 13:55:28 -08001117 case CHANGE_NAVIGATION_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001118 if (ViewMode.isSearchMode(mViewMode.getMode())) {
1119 mActivity.setResult(resultCode, data);
1120 mActivity.finish();
1121 } else if (resultCode == Activity.RESULT_OK && data != null) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08001122 // We have have received a result that indicates we need to navigate to a
1123 // different folder or account. This happens if someone navigates using the
1124 // drawer on the search results activity.
1125 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER);
1126 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT);
1127 if (folder != null) {
1128 onFolderSelected(folder);
1129 mViewMode.enterConversationListMode();
1130 } else if (account != null) {
1131 switchToDefaultInboxOrChangeAccount(account);
1132 mViewMode.enterConversationListMode();
1133 }
1134 }
1135 break;
Greg Bullock406ae082014-08-27 17:03:16 +02001136 case VOICE_SEARCH_REQUEST_CODE:
Jin Caoc6801eb2014-08-12 18:16:57 -07001137 if (resultCode == Activity.RESULT_OK) {
1138 final ArrayList<String> matches = data.getStringArrayListExtra(
1139 RecognizerIntent.EXTRA_RESULTS);
1140 if (!matches.isEmpty()) {
1141 // not sure how dependable the API is, but it's all we have.
1142 // take the top choice.
1143 mSearchViewController.onSearchPerformed(matches.get(0));
1144 }
1145 }
1146 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001147 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001148 }
1149
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001150 /**
1151 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001152 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001153 */
1154 protected synchronized void informCursorVisiblity(boolean visible) {
1155 if (mConversationListCursor != null) {
1156 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1157 // We have informed the cursor. Subsequent visibility changes should not tell it that
1158 // the folder has changed.
1159 mFolderChanged = false;
1160 }
1161 }
1162
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001163 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001164 public void onConversationListVisibilityChanged(boolean visible) {
Andy Huang92ae7662014-09-11 17:00:47 -07001165 mFloatingComposeButton.setVisibility(visible ? View.VISIBLE : View.GONE);
1166
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001167 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001168 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001169
1170 // Notify special views
1171 final ConversationListFragment convListFragment = getConversationListFragment();
1172 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1173 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1174 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001175 }
1176
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001177 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001178 * Called when a conversation is visible. Child classes must call the super class implementation
1179 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001180 */
1181 @Override
1182 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001183 commitAutoAdvanceOperation();
1184 }
1185
1186 /**
1187 * Commits any pending destructive action that was earlier deferred by an auto-advance
1188 * mode-change transition.
1189 */
1190 private void commitAutoAdvanceOperation() {
1191 if (mAutoAdvanceOp != null) {
1192 mAutoAdvanceOp.run();
1193 mAutoAdvanceOp = null;
1194 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001195 }
1196
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001197 /**
1198 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1199 * to turn it on for shipped versions.
1200 */
1201 private void initializeDevLoggingService() {
1202 if (!MailLogService.DEBUG_ENABLED) {
1203 return;
1204 }
1205 // Check every 5 minutes.
1206 final int WAIT_TIME = 5 * 60 * 1000;
1207 // Start a runnable that periodically checks the log level and starts/stops the service.
1208 mLogServiceChecker = new Runnable() {
1209 /** True if currently logging. */
1210 private boolean mCurrentlyLogging = false;
1211
1212 /**
1213 * If the logging level has been changed since the previous run, start or stop the
1214 * service.
1215 */
1216 private void startOrStopService() {
1217 // If the log level is already high, start the service.
1218 final Intent i = new Intent(mContext, MailLogService.class);
1219 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1220 if (mCurrentlyLogging == loggingEnabled) {
1221 // No change since previous run, just return;
1222 return;
1223 }
1224 if (loggingEnabled) {
1225 LogUtils.e(LOG_TAG, "Starting MailLogService");
1226 mContext.startService(i);
1227 } else {
1228 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1229 mContext.stopService(i);
1230 }
1231 mCurrentlyLogging = loggingEnabled;
1232 }
1233
1234 @Override
1235 public void run() {
1236 startOrStopService();
1237 mHandler.postDelayed(this, WAIT_TIME);
1238 }
1239 };
1240 // Start the runnable right away.
1241 mHandler.post(mLogServiceChecker);
1242 }
1243
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001244 /**
1245 * The application can be started from the following entry points:
1246 * <ul>
1247 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1248 * as “Starting the app”.</li>
1249 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1250 * <li>Widget: Shows the contents of a synced label, and allows:
1251 * <ul>
1252 * <li>Viewing the list (tapping on the title)</li>
1253 * <li>Composing a new message (tapping on the new message icon in the title. This
1254 * launches the {@link ComposeActivity}.
1255 * </li>
1256 * <li>Viewing a single message (tapping on a list element)</li>
1257 * </ul>
1258 *
1259 * </li>
1260 * <li>Tapping on a notification:
1261 * <ul>
1262 * <li>Shows message list if more than one message</li>
1263 * <li>Shows the conversation if the notification is for a single message</li>
1264 * </ul>
1265 * </li>
1266 * <li>...and most importantly, the activity life cycle can tear down the application and
1267 * restart it:
1268 * <ul>
1269 * <li>Rotate the application: it is destroyed and recreated.</li>
1270 * <li>Navigate away, and return from recent applications.</li>
1271 * </ul>
1272 * </li>
1273 * <li>Add a new account: fires off an intent to add an account,
1274 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1275 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1276 * <li>Composing can happen from many entry points: third party applications fire off an
1277 * intent to compose email, and launch directly into the {@link ComposeActivity}
1278 * .</li>
1279 * </ul>
1280 * {@inheritDoc}
1281 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001282 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001283 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001284 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001285 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001286 // Allow shortcut keys to function for the ActionBar and menus.
1287 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001288 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001289 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001290 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001291 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001292
James Lemieux1a1e9752014-07-18 13:50:40 -07001293 mFloatingComposeButton = mActivity.findViewById(R.id.compose_button);
1294 mFloatingComposeButton.setOnClickListener(this);
James Lemieux5d79a912014-07-16 14:22:26 -07001295
Andy Huangf58e4c32014-07-09 16:58:18 -07001296 if (isDrawerEnabled()) {
Andrew Sapperstein355dd902014-09-10 12:46:44 -07001297 mDrawerToggle = new ActionBarDrawerToggle(mActivity, mDrawerContainer,
1298 R.string.drawer_open, R.string.drawer_close);
Andy Huangf58e4c32014-07-09 16:58:18 -07001299 mDrawerContainer.setDrawerListener(mDrawerListener);
1300 mDrawerContainer.setDrawerShadow(
1301 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
Andy Huang12b3ee42013-04-24 22:49:43 -07001302
Andy Huangf58e4c32014-07-09 16:58:18 -07001303 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1304 } else {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001305 final ActionBar ab = mActivity.getSupportActionBar();
Andy Huangf58e4c32014-07-09 16:58:18 -07001306 ab.setHomeAsUpIndicator(R.drawable.ic_drawer);
1307 ab.setHomeActionContentDescription(R.string.drawer_open);
1308 ab.setDisplayHomeAsUpEnabled(true);
1309 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001310
Mindy Pereira161f50d2012-02-28 15:47:19 -08001311 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001312 // simplifies the amount of logic in the AbstractActivityController, but increases the
1313 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001314 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001315 mPagerController = new ConversationPagerController(mActivity, this);
James Lemieux9a110112014-08-07 15:23:13 -07001316 mToastBar = findActionableToastBar(mActivity);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001317 attachActionBar();
Andy Huang632721e2012-04-11 16:57:26 -07001318
Andy Huang144bfe72013-06-11 13:27:52 -07001319 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1320
Andy Huang632721e2012-04-11 16:57:26 -07001321 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001322
Jin Caoc6801eb2014-08-12 18:16:57 -07001323 mSearchViewController = new MaterialSearchViewController(mActivity, this, intent,
1324 savedState);
Jin Cao524ded52014-09-05 13:44:58 -07001325 addConversationListLayoutListener(mSearchViewController);
Jin Caoc6801eb2014-08-12 18:16:57 -07001326
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001327 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001328 // that does not rely on restored fragments or loader data
1329 // any state restoration that relies on those can be done later in
1330 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1331 if (savedState != null) {
1332 if (savedState.containsKey(SAVED_ACCOUNT)) {
1333 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001334 }
1335 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001336 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001337 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001338 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001339 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001340 if (savedState.containsKey(SAVED_ACTION)) {
1341 mDialogAction = savedState.getInt(SAVED_ACTION);
1342 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001343 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001344 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001345 } else if (intent != null) {
1346 handleIntent(intent);
1347 }
Andy Huang632721e2012-04-11 16:57:26 -07001348 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001349 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1350 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001351 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001352 }
1353
James Lemieux9a110112014-08-07 15:23:13 -07001354 /**
1355 * @param activity the activity that has been inflated
1356 * @return the Actionable Toast Bar defined within the activity
1357 */
1358 protected ActionableToastBar findActionableToastBar(MailActivity activity) {
1359 return (ActionableToastBar) activity.findViewById(R.id.toast_bar);
1360 }
1361
Andy Huangb1c34dc2012-04-17 16:36:19 -07001362 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001363 public void onPostCreate(Bundle savedState) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001364 if (!isDrawerEnabled()) {
1365 return;
1366 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001367 // Sync the toggle state after onRestoreInstanceState has occurred.
1368 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001369
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001370 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001371 }
1372
1373 @Override
1374 public void onConfigurationChanged(Configuration newConfig) {
Andy Huangf58e4c32014-07-09 16:58:18 -07001375 if (isDrawerEnabled()) {
1376 mDrawerToggle.onConfigurationChanged(newConfig);
1377 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001378 }
1379
1380 /**
James Lemieux5d79a912014-07-16 14:22:26 -07001381 * This controller listens for clicks on items in the floating action bar.
1382 *
1383 * @param view the item that was clicked in the floating action bar
1384 */
1385 @Override
1386 public void onClick(View view) {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001387 final int viewId = view.getId();
1388 if (viewId == R.id.compose_button) {
1389 ComposeActivity.compose(mActivity.getActivityContext(), getAccount());
1390 } else if (viewId == android.R.id.home) {
1391 // TODO: b/16627877
Jin Cao405a3442014-08-25 13:49:33 -07001392 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00001393 }
James Lemieux5d79a912014-07-16 14:22:26 -07001394 }
1395
1396 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001397 * If drawer is open/visible (even partially), close it.
1398 */
1399 protected void closeDrawerIfOpen() {
1400 if (!isDrawerEnabled()) {
1401 return;
1402 }
1403 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1404 mDrawerContainer.closeDrawers();
1405 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001406 }
1407
1408 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001409 public void onStart() {
1410 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001411
1412 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001413
1414 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1415 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1416 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001417 }
1418
1419 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001420 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001421 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001422 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1423 if (fragment != null) {
1424 fragment.dismiss();
1425 }
mindypea04f932012-08-27 14:17:59 -07001426 // When the user places the app in the background by pressing "home",
1427 // dismiss the toast bar. However, since there is no way to determine if
1428 // home was pressed, just dismiss any existing toast bar when restarting
1429 // the app.
1430 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001431 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001432 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001433 }
1434
1435 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001436 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001437 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001438 }
1439
1440 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001441 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001442 if (mViewMode.isAdMode()) {
1443 return false;
1444 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001445 final MenuInflater inflater = mActivity.getMenuInflater();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07001446 inflater.inflate(mActionBarController.getOptionsMenuId(), menu);
1447 mActionBarController.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001448 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001449 }
1450
1451 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001452 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001453 return false;
1454 }
1455
mindyp17a8e782012-11-29 14:56:17 -08001456 public abstract boolean doesActionChangeConversationListVisibility(int action);
1457
Jin Cao30c881a2014-04-08 14:28:36 -07001458 /**
1459 * Helper function that determines if we should associate an undo callback with
1460 * the current menu action item
1461 * @param actionId the id of the action
1462 * @return the appropriate callback handler, or null if not applicable
1463 */
1464 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(
1465 int actionId, final Conversation conv) {
1466 // We associated the undoCallback if the user is going to perform an action on the current
1467 // conversation, causing the current conversation to be removed from view and replacing it
1468 // with another (via Auto Advance). The undoCallback will bring the removed conversation
1469 // back into the view if the action is undone.
1470 final Collection<Conversation> convCol = Conversation.listOf(conv);
1471 final boolean isApplicableForReshow = mAccount != null &&
1472 mAccount.settings != null &&
1473 mTracker != null &&
1474 // ensure that we will show another conversation due to Auto Advance
1475 mTracker.getNextConversation(
1476 mAccount.settings.getAutoAdvanceSetting(), convCol) != null &&
1477 // ensure that we are performing the action from conversation view
1478 isCurrentConversationInView(convCol) &&
1479 // check for the appropriate destructive actions
1480 doesActionRemoveCurrentConversationFromView(actionId);
1481 return (isApplicableForReshow) ?
1482 new UndoCallback() {
1483 @Override
1484 public void performUndoCallback() {
1485 showConversation(conv);
1486 }
1487 } : null;
1488 }
1489
1490 /**
1491 * Check if the provided action will remove the active conversation from view
1492 * @param actionId the applied action
1493 * @return true if it will remove the conversation from view, false otherwise
1494 */
1495 private boolean doesActionRemoveCurrentConversationFromView(int actionId) {
1496 return actionId == R.id.archive ||
1497 actionId == R.id.delete ||
Jin Cao512821c2014-05-30 15:54:04 -07001498 actionId == R.id.discard_outbox ||
Jin Cao30c881a2014-04-08 14:28:36 -07001499 actionId == R.id.remove_folder ||
1500 actionId == R.id.report_spam ||
1501 actionId == R.id.report_phishing ||
1502 actionId == R.id.move_to;
1503 }
1504
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001505 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001506 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001507
Andy Huang12b3ee42013-04-24 22:49:43 -07001508 /*
1509 * The action bar home/up action should open or close the drawer.
1510 * mDrawerToggle will take care of this.
1511 */
Andy Huangf58e4c32014-07-09 16:58:18 -07001512 if (isDrawerEnabled() && mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001513 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1514 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001515 return true;
1516 }
1517
Andy Huang2b555492013-08-14 21:06:21 -07001518 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
Andy Huangf8c59b02014-03-19 20:00:53 -07001519 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0);
Andy Huang042a5302013-08-13 12:39:08 -07001520
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001521 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001522 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001523 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001524 /** This is NOT a batch action. */
1525 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001526 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001527 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001528 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001529 // doing before. Don't animate if we are launching a new screen.
1530 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Jin Cao30c881a2014-04-08 14:28:36 -07001531 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(
1532 id, mCurrentConversation);
1533
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001534 if (id == R.id.archive) {
1535 final boolean showDialog = (settings != null && settings.confirmArchive);
Jin Cao30c881a2014-04-08 14:28:36 -07001536 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001537 } else if (id == R.id.remove_folder) {
1538 delete(R.id.remove_folder, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001539 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback),
1540 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001541 } else if (id == R.id.delete) {
1542 final boolean showDialog = (settings != null && settings.confirmDelete);
Jin Cao30c881a2014-04-08 14:28:36 -07001543 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001544 } else if (id == R.id.discard_drafts) {
Andy Huang121c8b82013-08-05 11:52:55 -07001545 // drafts are lost forever, so always confirm
1546 confirmAndDelete(id, target, true /* showDialog */,
Jin Cao30c881a2014-04-08 14:28:36 -07001547 R.plurals.confirm_discard_drafts_conversation, undoCallback);
Jin Cao512821c2014-05-30 15:54:04 -07001548 } else if (id == R.id.discard_outbox) {
1549 // discard in outbox means we discard the failed message and save them in drafts
1550 delete(id, target, getDeferredAction(id, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001551 } else if (id == R.id.mark_important) {
1552 updateConversation(Conversation.listOf(mCurrentConversation),
1553 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1554 } else if (id == R.id.mark_not_important) {
1555 if (mFolder != null && mFolder.isImportantOnly()) {
1556 delete(R.id.mark_not_important, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001557 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback),
1558 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001559 } else {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001560 updateConversation(Conversation.listOf(mCurrentConversation),
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001561 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1562 }
1563 } else if (id == R.id.mute) {
Jin Cao30c881a2014-04-08 14:28:36 -07001564 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback),
1565 isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001566 } else if (id == R.id.report_spam) {
1567 delete(R.id.report_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001568 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001569 } else if (id == R.id.mark_not_spam) {
1570 // Currently, since spam messages are only shown in list with
1571 // other spam messages,
1572 // marking a message not as spam is a destructive action
1573 delete(R.id.mark_not_spam, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001574 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001575 } else if (id == R.id.report_phishing) {
1576 delete(R.id.report_phishing, target,
Jin Cao30c881a2014-04-08 14:28:36 -07001577 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001578 } else if (id == android.R.id.home) {
Jin Cao405a3442014-08-25 13:49:33 -07001579 handleUpPress();
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001580 } else if (id == R.id.compose) {
1581 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1582 } else if (id == R.id.refresh) {
1583 requestFolderRefresh();
1584 } else if (id == R.id.settings) {
1585 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001586 } else if (id == R.id.help_info_menu_item) {
Ray Chena57da3c2014-06-10 16:01:40 +02001587 mActivity.showHelp(mAccount, mViewMode.getMode());
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07001588 } else if (id == R.id.move_to || id == R.id.change_folders) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001589 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount,
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001590 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
1591 id == R.id.move_to);
1592 if (dialog != null) {
Tony Mantler2a4be242013-12-11 15:30:53 -08001593 dialog.show(mActivity.getFragmentManager(), null);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001594 }
1595 } else if (id == R.id.move_to_inbox) {
1596 new AsyncTask<Void, Void, Folder>() {
1597 @Override
1598 protected Folder doInBackground(final Void... params) {
1599 // Get the "move to" inbox
1600 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1601 true /* allowHidden */);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001602 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001603
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001604 @Override
1605 protected void onPostExecute(final Folder moveToInbox) {
1606 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1607 // Add inbox
1608 ops.add(new FolderOperation(moveToInbox, true));
1609 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1610 true /* showUndo */, false /* isMoveTo */);
1611 }
1612 }.execute((Void[]) null);
1613 } else if (id == R.id.empty_trash) {
1614 showEmptyDialog();
1615 } else if (id == R.id.empty_spam) {
1616 showEmptyDialog();
Jin Caod0334732014-09-01 22:33:14 -07001617 } else if (id == R.id.search) {
1618 mSearchViewController.showSearchActionBar(
1619 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001620 } else {
1621 handled = false;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001622 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001623 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001624 }
1625
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001626 /**
1627 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1628 */
1629 private void showEmptyDialog() {
1630 if (mFolder != null) {
1631 final EmptyFolderDialogFragment fragment =
1632 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1633 fragment.setListener(this);
1634 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1635 }
1636 }
1637
1638 @Override
1639 public void onFolderEmptied() {
1640 emptyFolder();
1641 }
1642
1643 /**
1644 * Performs the work of emptying the currently visible folder.
1645 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001646 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001647 if (mConversationListCursor != null) {
1648 mConversationListCursor.emptyFolder();
1649 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001650 }
1651
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001652 private void attachEmptyFolderDialogFragmentListener() {
1653 final EmptyFolderDialogFragment fragment =
1654 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1655 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1656
1657 if (fragment != null) {
1658 fragment.setListener(this);
1659 }
1660 }
1661
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001662 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001663 * Toggles the drawer pullout. If it was open (Fully extended), the
1664 * drawer will be closed. Otherwise, the drawer will be opened. This should
1665 * only be called when used with a toggle item. Other cases should be handled
1666 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001667 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001668 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001669 if (!isDrawerEnabled()) {
1670 return;
1671 }
1672 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1673 mDrawerContainer.closeDrawers();
1674 } else {
1675 mDrawerContainer.openDrawer(mDrawerPullout);
1676 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001677 }
1678
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001679 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001680 public final boolean onBackPressed() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001681 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1682 mDrawerContainer.closeDrawers();
1683 return true;
Jin Caoc6801eb2014-08-12 18:16:57 -07001684 } else if (mSearchViewController.handleBackPress()) {
1685 return true;
Andrew Sapperstein4c928742014-08-29 15:34:23 -07001686 // If we're in CAB mode, let the activity handle onBackPressed.
1687 // It will handle closing CAB mode for us.
1688 } else if (mCabActionMenu != null && mCabActionMenu.isActivated()) {
1689 return false;
Andy Huang12b3ee42013-04-24 22:49:43 -07001690 }
1691
Andy Huangc1fb9a92013-02-11 13:09:12 -08001692 return handleBackPress();
1693 }
1694
1695 protected abstract boolean handleBackPress();
Greg Bullockede2e522014-05-30 14:11:35 +02001696
Andy Huangc1fb9a92013-02-11 13:09:12 -08001697 protected abstract boolean handleUpPress();
1698
1699 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001700 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001701 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001702 refreshConversationList();
1703 }
1704
1705 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001706 public void updateConversation(Collection <Conversation> target, String columnName,
1707 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001708 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001709 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001710 }
1711
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001712 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001713 public void updateConversation(Collection <Conversation> target, String columnName,
1714 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001715 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001716 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001717 }
1718
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001719 @Override
1720 public void updateConversation(Collection <Conversation> target, String columnName,
1721 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001722 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001723 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001724 }
1725
Andy Huang839ada22012-07-20 15:48:40 -07001726 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001727 public void markConversationMessagesUnread(final Conversation conv,
1728 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001729 // The only caller of this method is the conversation view, from where marking unread should
1730 // *always* take you back to list mode.
1731 showConversation(null);
1732
Andy Huang839ada22012-07-20 15:48:40 -07001733 // locally mark conversation unread (the provider is supposed to propagate message unread
1734 // to conversation unread)
1735 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001736 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001737 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001738
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001739 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1740 @Override
1741 public void onLoadFinished() {
1742 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1743 originalConversationInfo);
1744 }
1745 });
1746 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001747 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001748 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1749 }
1750 }
1751
1752 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1753 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001754 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001755 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001756 final int numMessages = conv.getNumMessages();
1757 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1758 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001759
Andy Huang9e4ca792013-02-28 14:33:43 -08001760 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001761 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001762 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001763 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001764 // Conversations are neither marked read, nor viewed, and we don't want to show
1765 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001766 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001767 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001768 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001769 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1770 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1771 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1772 info);
1773 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001774 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001775
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001776 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001777 if (originalConversationInfo != null) {
1778 mConversationListCursor.setConversationColumn(conv.uri,
1779 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1780 }
Andy Huang839ada22012-07-20 15:48:40 -07001781
1782 // applyBatch with each CPO as an UPDATE op on each affected message uri
1783 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1784 String authority = null;
1785 for (Uri messageUri : unreadMessageUris) {
1786 if (authority == null) {
1787 authority = messageUri.getAuthority();
1788 }
1789 ops.add(ContentProviderOperation.newUpdate(messageUri)
1790 .withValue(UIProvider.MessageColumns.READ, 0)
1791 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001792 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001793 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001794 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001795 new ContentProviderTask() {
1796 @Override
1797 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001798 if (result.exception != null) {
1799 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1800 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001801 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1802 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001803 }
Andy Huang839ada22012-07-20 15:48:40 -07001804 }
1805 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001806 }
Andy Huang839ada22012-07-20 15:48:40 -07001807 }
1808
1809 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001810 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1811 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001812 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1813
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001814 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001815 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1816 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1817 targets.toArray());
1818 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001819 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1820 @Override
1821 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001822 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001823 }
1824 });
1825 } else {
1826 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001827 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001828 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001829 }
1830
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001831 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001832 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001833 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001834 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001835 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001836 final Runnable operation = new Runnable() {
1837 @Override
1838 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001839 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001840 }
1841 };
1842
Scott Kennedycaaeed32013-06-12 13:39:16 -07001843 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001844 // This method will be called again if the user selects an autoadvance option
1845 return;
1846 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001847 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001848
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001849 final int size = targets.size();
1850 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1851 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001852 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001853 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001854
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001855 // We never want to mark unseen here, but we do want to mark it seen
1856 if (read || markViewed) {
1857 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1858 }
1859
Paul Westbrook5109c512012-11-05 11:00:30 -08001860 // The mark read/unread/viewed operations do not show an undo bar
1861 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001862 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001863 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001864 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001865 final ConversationInfo info = target.conversationInfo;
Tony Mantleredd6c1a2013-10-08 14:47:43 -07001866 final boolean changed = info.markRead(read);
1867 if (changed) {
1868 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
Andy Huang839ada22012-07-20 15:48:40 -07001869 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001870 opList.add(mConversationListCursor.getOperationForConversation(
1871 target, ConversationOperation.UPDATE, value));
1872 // Update the local conversation objects so they immediately change state.
1873 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001874 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001875 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001876 }
Andy Huang839ada22012-07-20 15:48:40 -07001877 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001878 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001879 }
1880
Andy Huang8f6b0062012-07-31 15:36:31 -07001881 /**
1882 * Auto-advance to a different conversation if the currently visible conversation in
1883 * conversation mode is affected (deleted, marked unread, etc.).
1884 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001885 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001886 *
1887 * @param target the set of conversations being deleted/marked unread
1888 */
mindyp9365a822012-09-12 09:09:09 -07001889 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001890 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001891 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001892 }
1893
1894 /**
Jin Cao30c881a2014-04-08 14:28:36 -07001895 * Helper function to determine if the provided set of conversations is in view
1896 * @param target set of conversations that we are interested in
1897 * @return true if they are in view, false otherwise
1898 */
1899 private boolean isCurrentConversationInView(final Collection<Conversation> target) {
1900 final int viewMode = mViewMode.getMode();
1901 return (viewMode == ViewMode.CONVERSATION
1902 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1903 && Conversation.contains(target, mCurrentConversation);
1904 }
1905
1906 /**
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001907 * Auto-advance to a different conversation if the currently visible conversation in
1908 * conversation mode is affected (deleted, marked unread, etc.).
1909 *
1910 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001911 * <p>
1912 * Clients may pass an operation to execute on the target that this method will run after
1913 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1914 * later, or not at all. Reasons it may run later include:
1915 * <ul>
1916 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1917 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1918 * mode change transition to finish</li>
1919 * </ul>
1920 * <p>If the current conversation is not in the target collection, this method will do nothing,
1921 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001922 *
1923 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001924 * @param operation (optional) the operation to execute after advancing
1925 * @return <code>false</code> if this method handled or will execute the operation,
1926 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001927 */
1928 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001929 final Runnable operation) {
Jin Cao30c881a2014-04-08 14:28:36 -07001930 if (isCurrentConversationInView(target)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001931 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1932
Tony Mantler93e64572014-07-11 14:35:56 -07001933 // If we don't have one set, but we're here, just take the default
1934 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1935 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001936
Tony Mantler93e64572014-07-11 14:35:56 -07001937 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1938 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1939 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1940 // transition doesn't run (i.e. it "completes" immediately).
1941 mAutoAdvanceOp = operation;
1942 showConversation(next);
1943 return (mAutoAdvanceOp == null);
Andy Huang8f6b0062012-07-31 15:36:31 -07001944 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001945
1946 return true;
1947 }
1948
Andy Huang839ada22012-07-20 15:48:40 -07001949 @Override
1950 public void starMessage(ConversationMessage msg, boolean starred) {
1951 if (msg.starred == starred) {
1952 return;
1953 }
1954
1955 msg.starred = starred;
1956
1957 // locally propagate the change to the owning conversation
1958 // (figure the provider will properly propagate the change when it commits it)
1959 //
1960 // when unstarring, only propagate the change if this was the only message starred
1961 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001962 final Conversation conv = msg.getConversation();
1963 if (conversationStarred != conv.starred) {
1964 conv.starred = conversationStarred;
1965 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001966 ConversationColumns.STARRED, conversationStarred);
1967 }
1968
1969 final ContentValues values = new ContentValues(1);
1970 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1971
1972 new ContentProviderTask.UpdateTask() {
1973 @Override
1974 protected void onPostExecute(Result result) {
1975 // TODO: handle errors?
1976 }
1977 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1978 }
1979
Andy Huang12b3ee42013-04-24 22:49:43 -07001980 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001981 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001982 if (mFolder == null) {
1983 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001984 }
Alice Yang37dda442013-03-26 22:48:53 -07001985 final ConversationListFragment convList = getConversationListFragment();
1986 if (convList == null) {
1987 // This could happen if this account is in initial sync (user
1988 // is seeing the "your mail will appear shortly" message)
1989 return;
1990 }
Jin Cao41733e32014-09-15 12:02:34 -07001991
1992 // TODO: remove me after experiment b/17508768
1993 final String id = android.provider.Settings.Secure.getString(
1994 mActivity.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
1995 if (!TextUtils.isEmpty(id)) {
1996 final long longId = Long.parseLong(
1997 Character.toString(id.charAt(id.length() - 1)), 16 /* hex */);
1998 Analytics.getInstance().sendEvent("battery_experiment", "refresh",
1999 longId % 2 == 0 ? "even" : "odd", 1);
2000 }
2001
Alice Yang37dda442013-03-26 22:48:53 -07002002 convList.showSyncStatusBar();
2003
2004 if (mAsyncRefreshTask != null) {
2005 mAsyncRefreshTask.cancel(true);
2006 }
2007 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
2008 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08002009 }
2010
Mindy Pereirafbe40192012-03-20 10:40:45 -07002011 /**
2012 * Confirm (based on user's settings) and delete a conversation from the conversation list and
2013 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002014 * @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 -07002015 * @param target the conversations to act upon
2016 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
2017 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07002018 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002019 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07002020 boolean showDialog, int confirmResource, UndoCallback undoCallback) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002021 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002022 if (showDialog) {
Jin Cao30c881a2014-04-08 14:28:36 -07002023 makeDialogListener(actionId, isBatch, undoCallback);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002024 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
2025 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002026 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
2027 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07002028 } else {
Jin Cao30c881a2014-04-08 14:28:36 -07002029 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002030 }
2031 }
2032
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07002033 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002034 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07002035 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07002036 // Order of events is critical! The Conversation View Fragment must be
2037 // notified of the next conversation with showConversation(next) *before* the
2038 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002039 // fragment has a chance to delete the conversation, animating it away.
2040
mindyp84f7d322012-10-01 17:14:40 -07002041 // Update the conversation fragment if the current conversation is
2042 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002043 final Runnable operation = new Runnable() {
2044 @Override
2045 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07002046 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07002047 }
2048 };
2049
Rohan Shah52bab6f2014-08-26 18:52:14 -07002050 showNextConversation(target, operation);
2051
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002052 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002053 // Batch selections are cleared in the end of the action, so not done for batch actions.
2054 if (!isBatch) {
2055 for (final Conversation conv : target) {
Jin Caoec0fa482014-08-28 16:38:08 -07002056 if (mCheckedSet.contains(conv)) {
2057 mCheckedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002058 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07002059 }
2060 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002061 // The conversation list deletes and performs the action if it exists.
2062 final ConversationListFragment convListFragment = getConversationListFragment();
2063 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07002064 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002065 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07002066 return;
2067 }
mindyp84f7d322012-10-01 17:14:40 -07002068 // No visible UI element handled it on our behalf. Perform the action
2069 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07002070 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07002071 action.performAction();
2072 }
2073
2074 /**
2075 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002076 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07002077 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002078 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07002079 action.performAction();
2080 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002081 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07002082
2083 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002084 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
2085 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002086 }
2087
2088 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002089 public boolean onPrepareOptionsMenu(Menu menu) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002090 return mActionBarController.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002091 }
2092
Mindy Pereira68f2e222012-03-07 10:36:54 -08002093 @Override
2094 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002095 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002096 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08002097 }
2098
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002099 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002100 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002101 // Register the receiver that will prevent the status receiver from
2102 // displaying its notification icon as long as we're running.
2103 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2104 // that the notification was received for.
2105 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002106
2107 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002108
2109 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002110
2111 // Invalidating the options menu so that when we make changes in settings,
2112 // the changes will always be updated in the action bar/options menu/
2113 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002114 }
2115
2116 @Override
2117 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002118 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002119 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002120 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2121 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002122 if (mFolder != null) {
2123 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002124 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002125 // If this is a search activity, let's store the search query term as well.
2126 if (ConversationListContext.isSearchResult(mConvListContext)) {
2127 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2128 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002129 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002130 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2131 }
Jin Caoec0fa482014-08-28 16:38:08 -07002132 if (!mCheckedSet.isEmpty()) {
2133 outState.putParcelable(SAVED_SELECTED_SET, mCheckedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002134 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002135 if (mToastBar.getVisibility() == View.VISIBLE) {
2136 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2137 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002138 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002139 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002140 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002141 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002142 // 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 -08002143 if (mDialogAction != -1) {
2144 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002145 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002146 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002147 if (mDetachedConvUri != null) {
2148 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2149 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002150
Scott Kennedyb10212e2013-02-22 16:27:00 -08002151 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002152 mSafeToModifyFragments = false;
Alice Yangebeef1b2013-09-04 06:41:10 +00002153
2154 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002155
2156 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2157 mConversationListScrollPositions);
Jin Caoc6801eb2014-08-12 18:16:57 -07002158
2159 mSearchViewController.saveState(outState);
Andy Huang1ee96b22012-08-24 20:19:53 -07002160 }
2161
2162 /**
2163 * @see #mSafeToModifyFragments
2164 */
2165 protected boolean safeToModifyFragments() {
2166 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002167 }
2168
2169 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002170 public void executeSearch(String query) {
Jin Cao779dd602014-04-22 16:16:28 -07002171 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST);
Mindy Pereira68f2e222012-03-07 10:36:54 -08002172 Intent intent = new Intent();
2173 intent.setAction(Intent.ACTION_SEARCH);
2174 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2175 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2176 intent.setComponent(mActivity.getComponentName());
Jin Caoc6801eb2014-08-12 18:16:57 -07002177 mSearchViewController.showSearchActionBar(
2178 MaterialSearchViewController.SEARCH_VIEW_STATE_GONE);
Martin Hibdon371a71c2014-02-19 13:55:28 -08002179 // Call startActivityForResult here so we can tell if we have navigated to a different folder
2180 // or account from search results.
2181 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002182 }
2183
2184 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002185 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002186 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002187 }
2188
Andy Huang632721e2012-04-11 16:57:26 -07002189 @Override
2190 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002191 // stop listening to the cursor on e.g. configuration changes
2192 if (mConversationListCursor != null) {
2193 mConversationListCursor.removeListener(this);
2194 }
Andy Huang144bfe72013-06-11 13:27:52 -07002195 mDrawIdler.setListener(null);
2196 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002197 // unregister the ViewPager's observer on the conversation cursor
2198 mPagerController.onDestroy();
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002199 mActionBarController.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002200 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002201 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002202 mHandler.removeCallbacks(mLogServiceChecker);
2203 mLogServiceChecker = null;
Jin Caoc6801eb2014-08-12 18:16:57 -07002204 mSearchViewController.onDestroy();
Andy Huang632721e2012-04-11 16:57:26 -07002205 }
2206
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002207 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002208 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2209 * or not. The individual controller is responsible for changing the icon based on the mode.
2210 */
2211 protected abstract void resetActionBarIcon();
2212
2213 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002214 * {@inheritDoc} Subclasses must override this to listen to mode changes
2215 * from the ViewMode. Subclasses <b>must</b> call the parent's
2216 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002217 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002218 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002219 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002220 // When we step away from the conversation mode, we don't have a current conversation
2221 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2222 if (!ViewMode.isConversationMode(newMode)) {
2223 setCurrentConversation(null);
2224 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002225
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002226 // If the viewmode is not set, preserve existing icon.
2227 if (newMode != ViewMode.UNKNOWN) {
2228 resetActionBarIcon();
2229 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002230
2231 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002232 /** If the folder doesn't exist, or its parent URI is empty,
2233 * this is not a child folder */
Jin Cao9695e002014-05-29 11:56:44 -07002234 final boolean isTopLevel = Folder.isRoot(mFolder);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002235 mDrawerToggle.setDrawerIndicatorEnabled(
2236 getShouldShowDrawerIndicator(newMode, isTopLevel));
Martin Hibdon371a71c2014-02-19 13:55:28 -08002237 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002238 closeDrawerIfOpen();
2239 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002240 }
2241
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002242 /**
2243 * Returns true if the drawer icon is shown
2244 * @param viewMode the current view mode
2245 * @param isTopLevel true if the current folder is not a child
2246 * @return whether the drawer indicator is shown
2247 */
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002248 private boolean getShouldShowDrawerIndicator(final int viewMode,
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002249 final boolean isTopLevel) {
2250 // If search list/conv mode: disable indicator
2251 // Indicator is enabled either in conversation list or folder list mode.
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002252 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
Scott Kennedyaded5782013-07-16 14:21:53 -07002253 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel);
Scott Kennedy8a72b852013-05-02 14:18:50 -07002254 }
2255
Andy Huang3825f3d2012-08-29 16:44:12 -07002256 public void disablePagerUpdates() {
2257 mPagerController.stopListening();
2258 }
2259
Andy Huang4e0158f2012-08-07 21:06:01 -07002260 public boolean isDestroyed() {
2261 return mDestroyed;
2262 }
2263
mindyp54f120f2012-08-28 13:10:33 -07002264 @Override
2265 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002266 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002267 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002268 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002269 }
2270 }
2271
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002272 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002273 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002274 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002275 // hasFocus already ensures that the window is in focus, so we don't need to call
2276 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002277 if (hasFocus && convList != null && convList.isVisible()) {
2278 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002279 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002280 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002281 }
2282
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002283 /**
2284 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002285 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002286 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002287 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002288 if (account == null) {
2289 LogUtils.w(LOG_TAG, new Error(),
2290 "AAC ignoring null (presumably invalid) account restoration");
2291 return;
2292 }
Andy Huangb1148412012-05-19 00:16:30 -07002293 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002294 mAccount = account;
Régis Décamps2168cbc2014-08-22 15:00:37 +02002295
2296 Analytics.getInstance().setEmail(account.getEmailAddress(), account.getType());
2297
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002298 // Only change AAC state here. Do *not* modify any other object's state. The object
2299 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002300 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002301 mActivity.invalidateOptionsMenu();
2302 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002303 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002304 // The Mail instance can be null during test runs.
2305 final MailAppProvider instance = MailAppProvider.getInstance();
2306 if (instance != null) {
2307 instance.setLastViewedAccount(mAccount.uri.toString());
2308 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002309 if (account.settings == null) {
2310 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2311 return;
2312 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002313 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002314 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002315 }
2316
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002317 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002318 * Restore the state from the previous bundle. Subclasses should call this
2319 * method from the parent class, since it performs important UI
2320 * initialization.
2321 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002322 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002323 */
Andy Huang632721e2012-04-11 16:57:26 -07002324 @Override
2325 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002326 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002327 if (savedState.containsKey(SAVED_CONVERSATION)) {
2328 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002329 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002330 if (conversation != null && conversation.position < 0) {
2331 // Set the position to 0 on this conversation, as we don't know where it is
2332 // in the list
2333 conversation.position = 0;
2334 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002335 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002336 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002337
Mindy Pereirad33674992012-06-25 16:26:30 -07002338 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002339 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002340 if (op != null) {
2341 if (op.getType() == ToastBarOperation.UNDO) {
2342 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002343 } else if (op.getType() == ToastBarOperation.ERROR) {
2344 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002345 }
2346 }
2347 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002348 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002349 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002350 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002351 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002352 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002353 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002354 * Restore the state of selected conversations. This needs to be done after the correct mode
2355 * is set and the action bar is fully initialized. If not, several key pieces of state
2356 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002357 */
Andy Huang4556a442012-03-30 16:42:05 -07002358 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002359 // Order is important!!!
2360 // The dialog listener needs to happen *after* the selected set is restored.
2361
2362 // If there has been an orientation change, and we need to recreate the listener for the
2363 // confirm dialog fragment (delete/archive/...), then do it here.
2364 if (mDialogAction != -1) {
Jin Cao30c881a2014-04-08 14:28:36 -07002365 makeDialogListener(mDialogAction, mDialogFromSelectedSet,
2366 getUndoCallbackForDestructiveActionsWithAutoAdvance(
2367 mDialogAction, mCurrentConversation));
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002368 }
Alice Yangebeef1b2013-09-04 06:41:10 +00002369
2370 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002371
2372 mConversationListScrollPositions.clear();
2373 mConversationListScrollPositions.putAll(
2374 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002375 }
2376
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002377 /**
2378 * Handle an intent to open the app. This method is called only when there is no saved state,
2379 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2380 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002381 *
2382 * This method is called for a subset of the reasons mentioned in
2383 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2384 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002385 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002386 */
Andy Huang632721e2012-04-11 16:57:26 -07002387 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002388 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002389 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2390 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Tony Mantler26a20752014-02-28 16:44:24 -08002391 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002392 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002393 if (mAccount == null) {
2394 return;
Andy Huang632721e2012-04-11 16:57:26 -07002395 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002396 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002397
2398 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
Régis Décamps2168cbc2014-08-22 15:00:37 +02002399 Analytics.getInstance().setEmail(mAccount.getEmailAddress(), mAccount.getType());
Andy Huang4fe0af82013-08-20 17:24:51 -07002400 Analytics.getInstance().sendEvent("notification_click",
2401 isConversationMode ? "conversation" : "conversation_list", null, 0);
2402 }
2403
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002404 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002405 mViewMode.enterConversationMode();
2406 } else {
2407 mViewMode.enterConversationListMode();
2408 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002409 // Put the folder and conversation, and ask the loader to create this folder.
2410 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002411
2412 final Uri folderUri;
2413 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002414 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
Scott Kennedy48cfe462013-04-10 11:32:02 -07002415 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2416 final Folder folder =
2417 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002418 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002419 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002420 final Bundle extras = intent.getExtras();
2421 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2422 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002423 folderUri = mAccount.settings.defaultInbox;
2424 }
2425
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07002426 // Check if we should load all conversations instead of using
2427 // the default behavior which loads an initial subset.
2428 mIgnoreInitialConversationLimit =
2429 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false);
2430
Scott Kennedy60593352013-03-13 13:45:30 -07002431 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002432 args.putParcelable(Utils.EXTRA_CONVERSATION,
2433 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2434 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002435 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2436 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002437 mHaveSearchResults = false;
Jin Caoc6801eb2014-08-12 18:16:57 -07002438 // Save this search query for future suggestions
Andy Huang632721e2012-04-11 16:57:26 -07002439 final String query = intent.getStringExtra(SearchManager.QUERY);
Jin Caoc6801eb2014-08-12 18:16:57 -07002440 mSearchViewController.saveRecentQuery(query);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002441 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2442 fetchSearchFolder(intent);
2443 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002444 mViewMode.enterSearchResultsConversationMode();
2445 } else {
2446 mViewMode.enterSearchResultsListMode();
2447 }
Andy Huang632721e2012-04-11 16:57:26 -07002448 } else {
2449 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2450 mActivity.finish();
2451 }
2452 }
2453 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002454 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002455 }
2456 }
2457
Andy Huang4556a442012-03-30 16:42:05 -07002458 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002459 * Returns true if we should enter conversation mode with search.
2460 */
2461 protected final boolean shouldEnterSearchConvMode() {
2462 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2463 }
2464
2465 /**
Andy Huang4556a442012-03-30 16:42:05 -07002466 * Copy any selected conversations stored in the saved bundle into our selection set,
2467 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2468 *
2469 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002470 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002471 if (savedState == null) {
Jin Caoec0fa482014-08-28 16:38:08 -07002472 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002473 return;
2474 }
Jin Caoec0fa482014-08-28 16:38:08 -07002475 final ConversationCheckedSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002476 if (selectedSet == null || selectedSet.isEmpty()) {
Jin Caoec0fa482014-08-28 16:38:08 -07002477 mCheckedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002478 return;
2479 }
Andy Huang632721e2012-04-11 16:57:26 -07002480
2481 // putAll will take care of calling our registered onSetPopulated method
Jin Caoec0fa482014-08-28 16:38:08 -07002482 mCheckedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002483 }
2484
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002485 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002486 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2487 * object, which is a signal to back out of conversation view mode.
2488 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002489 * @param conversation the conversation to be shown, or null if we want to back out to list
2490 * mode.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002491 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002492 */
Jin Cao30c881a2014-04-08 14:28:36 -07002493 protected void showConversation(Conversation conversation) {
Jin Caof8471262014-08-14 18:51:52 -07002494 showConversation(conversation, false /* peek */);
Jin Caod23f6d12014-08-08 15:23:20 -07002495 }
2496
Jin Caof8471262014-08-14 18:51:52 -07002497 protected void showConversation(Conversation conversation, boolean peek) {
Andy Huang243c2362013-03-01 17:50:35 -08002498 if (conversation != null) {
2499 Utils.sConvLoadTimer.start();
2500 }
2501
Andy Huang54e925e2013-03-14 13:24:18 -07002502 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002503 // Set the current conversation just in case it wasn't already set.
2504 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002505 }
2506
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002507 /**
Jin Cao405a3442014-08-25 13:49:33 -07002508 * Show the wait for account initialization mode.
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002509 * Children can override this method, but they must call super.showWaitForInitialization().
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002510 */
Jin Cao405a3442014-08-25 13:49:33 -07002511 protected void showWaitForInitialization() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002512 mViewMode.enterWaitingForInitializationMode();
Andy Huangc96efcc2014-04-09 15:30:42 -07002513 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002514 }
2515
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002516 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002517 final FragmentManager manager = mActivity.getFragmentManager();
2518 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002519 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002520 if (waitFragment != null) {
2521 waitFragment.updateAccount(mAccount);
2522 }
2523 }
2524
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002525 /**
2526 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2527 * method, though they must call the parent implementation <b>after</b> they do anything.
2528 */
2529 protected void hideWaitForInitialization() {
2530 mWaitFragment = null;
2531 }
2532
2533 /**
2534 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2535 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2536 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002537 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002538 */
2539 protected final WaitFragment getWaitFragment() {
2540 final FragmentManager manager = mActivity.getFragmentManager();
2541 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2542 if (waitFrag != null) {
2543 // The Fragment Manager knows better, so use its instance.
2544 mWaitFragment = waitFrag;
2545 }
2546 return mWaitFragment;
2547 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002548
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002549 /**
2550 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2551 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002552 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002553 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002554 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002555 if (waitFragment != null) {
2556 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002557 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002558 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2559 }
2560 return false;
2561 }
2562
2563 /**
Jin Cao405a3442014-08-25 13:49:33 -07002564 * Show the conversation List with the list context provided here. On certain layouts, this
2565 * might show more than just the conversation list. For instance, on tablets this might show
2566 * the conversations along with the conversation list.
2567 * @param listContext context providing information on what conversation list to display.
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002568 */
Jin Cao405a3442014-08-25 13:49:33 -07002569 protected abstract void showConversationList(ConversationListContext listContext);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002570
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002571 @Override
Jin Cao0b693382014-08-11 10:46:12 -07002572 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002573 final ConversationListFragment convListFragment = getConversationListFragment();
2574 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2575 convListFragment.getAnimatedAdapter().onConversationSelected();
2576 }
mindypaa55bc92012-08-24 09:49:56 -07002577 // Only animate destructive actions if we are going to be showing the
2578 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002579 commitDestructiveActions(mIsTablet);
Jin Cao30c881a2014-04-08 14:28:36 -07002580 showConversation(conversation);
Andy Huang1ee96b22012-08-24 20:19:53 -07002581 }
2582
2583 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002584 public final void onCabModeEntered() {
2585 final ConversationListFragment convListFragment = getConversationListFragment();
2586 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2587 convListFragment.getAnimatedAdapter().onCabModeEntered();
2588 }
2589 }
2590
2591 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002592 public final void onCabModeExited() {
2593 final ConversationListFragment convListFragment = getConversationListFragment();
2594 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2595 convListFragment.getAnimatedAdapter().onCabModeExited();
2596 }
2597 }
2598
2599 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002600 public Conversation getCurrentConversation() {
2601 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002602 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002603
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002604 /**
2605 * Set the current conversation. This is the conversation on which all actions are performed.
2606 * Do not modify mCurrentConversation except through this method, which makes it easy to
2607 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002608 * @param conversation new conversation to view. Passing null indicates that we are backing
2609 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002610 */
Andy Huang632721e2012-04-11 16:57:26 -07002611 @Override
2612 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002613 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002614 // we are going back to conversation list mode.
2615 if (mDetachedConvUri != null && (conversation == null
2616 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002617 clearDetachedMode();
2618 }
2619
2620 // Must happen *before* setting mCurrentConversation because this sets
2621 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002622 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002623 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002624
2625 if (mCurrentConversation != null) {
Andrew Sapperstein2d86d112014-07-24 20:27:37 -07002626 mActionBarController.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002627 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002628 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002629 }
2630
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002631 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002632 * {@link LoaderManager} currently has a bug in
2633 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2634 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2635 * this bug by destroying any loaders that may have been created as null (essentially because
2636 * they are optional loads, and may not apply to a particular account).
2637 * <p>
2638 * A simple null check before restarting a loader will not work, because that would not
2639 * give the controller a chance to invalidate UI corresponding the prior loader result.
2640 *
2641 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002642 * @param handler the LoaderCallback which will handle this loader ID.
2643 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2644 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002645 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002646 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002647 final LoaderManager lm = mActivity.getLoaderManager();
2648 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002649 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002650 }
2651
Andy Huang632721e2012-04-11 16:57:26 -07002652 @Override
2653 public void registerConversationListObserver(DataSetObserver observer) {
2654 mConversationListObservable.registerObserver(observer);
2655 }
2656
2657 @Override
2658 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002659 try {
2660 mConversationListObservable.unregisterObserver(observer);
2661 } catch (IllegalStateException e) {
2662 // Log instead of crash
2663 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2664 + "hasn't been registered");
2665 }
Andy Huang632721e2012-04-11 16:57:26 -07002666 }
2667
Andy Huang090db1e2012-07-25 13:25:28 -07002668 @Override
2669 public void registerFolderObserver(DataSetObserver observer) {
2670 mFolderObservable.registerObserver(observer);
2671 }
2672
2673 @Override
2674 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002675 try {
2676 mFolderObservable.unregisterObserver(observer);
2677 } catch (IllegalStateException e) {
2678 // Log instead of crash
2679 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2680 + "hasn't been registered");
2681 }
Andy Huang090db1e2012-07-25 13:25:28 -07002682 }
2683
Andy Huang9d3fd922012-09-26 22:23:58 -07002684 @Override
2685 public void registerConversationLoadedObserver(DataSetObserver observer) {
2686 mPagerController.registerConversationLoadedObserver(observer);
2687 }
2688
2689 @Override
2690 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002691 try {
2692 mPagerController.unregisterConversationLoadedObserver(observer);
2693 } catch (IllegalStateException e) {
2694 // Log instead of crash
2695 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2696 + "that hasn't been registered");
2697 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002698 }
2699
Vikram Aggarwal60069912012-07-24 14:26:09 -07002700 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002701 * Returns true if the number of accounts is different, or if the current account has
2702 * changed. This method is meant to filter frequent changes to the list of
2703 * accounts, and only return true if the new list is substantially different from the existing
2704 * list. Returning true is safe here, it leads to more work in creating the
2705 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002706 * @param accountCursor the cursor which points to all the accounts.
2707 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002708 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002709 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002710 // Check to see if the current account hasn't been set, or the account cursor is empty
2711 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002712 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002713 }
2714
2715 // Check to see if the number of accounts are different, from the number we saw on the last
2716 // updated
2717 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2718 return true;
2719 }
2720
2721 // Check to see if the account list is different or if the current account is not found in
2722 // the cursor.
2723 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002724 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002725 final Account account = accountCursor.getModel();
2726 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2727 if (mAccount.settingsDiffer(account)) {
2728 // Settings changed, and we don't need to look any further.
2729 return true;
2730 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002731 foundCurrentAccount = true;
2732 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002733 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002734 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002735 return true;
2736 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002737 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002738
2739 // As long as we found the current account, the list hasn't been updated
2740 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002741 }
2742
2743 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002744 * Updates accounts for the app. If the current account is missing, the first
2745 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002746 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002747 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002748 * @return true if the update was successful, false otherwise
2749 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002750 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002751 if (accounts == null || !accounts.moveToFirst()) {
2752 return false;
2753 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002754
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002755 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002756 // A match for the current account's URI in the list of accounts.
2757 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002758
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002759 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002760 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002761 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002762 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002763 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002764 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2765 currentFromList = account;
2766 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002767 }
2768
Vikram Aggarwal60069912012-07-24 14:26:09 -07002769 // 1. current account is already set and is in allAccounts:
2770 // 1a. It has changed -> load the updated account.
Greg Bullock406ae082014-08-27 17:03:16 +02002771 // 1b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002772 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002773 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002774 // 4. otherwise just pick first
2775
Vikram Aggarwal60069912012-07-24 14:26:09 -07002776 boolean accountChanged = false;
2777 /// Assume case 4, initialize to first account, and see if we can find anything better.
2778 Account newAccount = allAccounts[0];
2779 if (currentFromList != null) {
2780 // Case 1: Current account exists but has changed
2781 if (!currentFromList.equals(mAccount)) {
2782 newAccount = currentFromList;
2783 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002784 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002785 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002786 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002787 // Case 2: Current account is not in allAccounts, the account needs to change.
2788 accountChanged = true;
2789 if (mAccount == null) {
2790 // Case 3: Check for last viewed account, and check if it exists in the list.
2791 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2792 if (lastAccountUri != null) {
2793 for (final Account account : allAccounts) {
2794 if (lastAccountUri.equals(account.uri.toString())) {
2795 newAccount = account;
2796 break;
2797 }
Andy Huang0d647352012-03-21 21:48:16 -07002798 }
2799 }
2800 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002801 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002802 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002803 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002804 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002805
Vikram Aggarwal60069912012-07-24 14:26:09 -07002806 // Whether we have updated the current account or not, we need to update the list of
2807 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002808 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002809 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002810 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002811 }
2812
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002813 private void disableNotifications() {
2814 mNewEmailReceiver.activate(mContext, this);
2815 }
2816
2817 private void enableNotifications() {
2818 mNewEmailReceiver.deactivate();
2819 }
2820
2821 private void disableNotificationsOnAccountChange(Account account) {
2822 // If the new mail suppression receiver is activated for a different account, we want to
2823 // activate it for the new account.
2824 if (mNewEmailReceiver.activated() &&
2825 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2826 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2827 mNewEmailReceiver.deactivate();
2828 mNewEmailReceiver.activate(mContext, this);
2829 }
2830 }
2831
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002832 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002833 * Destructive actions on Conversations. This class should only be created by controllers, and
2834 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2835 * Only the controllers should know what kind of destructive actions are being created.
2836 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002837 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002838 /**
2839 * The action to be performed. This is specified as the resource ID of the menu item
2840 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2841 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002842 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002843 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002844 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002845 /** Whether this destructive action has already been performed */
2846 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002847 /** Whether this is an action on the currently selected set. */
2848 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002849
Jin Cao30c881a2014-04-08 14:28:36 -07002850 private UndoCallback mCallback;
2851
Mindy Pereirafbe40192012-03-20 10:40:45 -07002852 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002853 * Create a listener object.
2854 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002855 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002856 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002857 * @param isBatch whether the conversations are in the currently selected batch set.
2858 */
2859 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002860 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002861 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002862 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002863 }
2864
Jin Cao30c881a2014-04-08 14:28:36 -07002865 @Override
2866 public void setUndoCallback(UndoCallback undoCallback) {
2867 mCallback = undoCallback;
2868 }
2869
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002870 /**
2871 * The action common to child classes. This performs the action specified in the constructor
2872 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002873 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002874 @Override
2875 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002876 if (isPerformed()) {
2877 return;
2878 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002879 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002880
2881 // Are we destroying the currently shown conversation? Show the next one.
2882 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002883 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2884 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002885 Conversation.toString(mTarget), mCurrentConversation);
2886 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002887
Paul Westbrooke1221d22012-08-19 11:09:07 -07002888 if (mConversationListCursor == null) {
2889 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2890 + "\nmTarget=%s\nCurrent=%s",
2891 Conversation.toString(mTarget), mCurrentConversation);
2892 return;
2893 }
2894
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002895 if (mAction == R.id.archive) {
2896 LogUtils.d(LOG_TAG, "Archiving");
Jin Cao30c881a2014-04-08 14:28:36 -07002897 mConversationListCursor.archive(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002898 } else if (mAction == R.id.delete) {
2899 LogUtils.d(LOG_TAG, "Deleting");
Jin Cao30c881a2014-04-08 14:28:36 -07002900 mConversationListCursor.delete(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002901 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07002902 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002903 }
2904 } else if (mAction == R.id.mute) {
2905 LogUtils.d(LOG_TAG, "Muting");
2906 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2907 for (Conversation c : mTarget) {
2908 c.localDeleteOnUpdate = true;
2909 }
2910 }
Jin Cao30c881a2014-04-08 14:28:36 -07002911 mConversationListCursor.mute(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002912 } else if (mAction == R.id.report_spam) {
2913 LogUtils.d(LOG_TAG, "Reporting spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002914 mConversationListCursor.reportSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002915 } else if (mAction == R.id.mark_not_spam) {
2916 LogUtils.d(LOG_TAG, "Marking not spam");
Jin Cao30c881a2014-04-08 14:28:36 -07002917 mConversationListCursor.reportNotSpam(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002918 } else if (mAction == R.id.report_phishing) {
2919 LogUtils.d(LOG_TAG, "Reporting phishing");
Jin Cao30c881a2014-04-08 14:28:36 -07002920 mConversationListCursor.reportPhishing(mTarget, mCallback);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002921 } else if (mAction == R.id.remove_star) {
2922 LogUtils.d(LOG_TAG, "Removing star");
2923 // Star removal is destructive in the Starred folder.
2924 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2925 false);
2926 } else if (mAction == R.id.mark_not_important) {
2927 LogUtils.d(LOG_TAG, "Marking not-important");
2928 // Marking not important is destructive in a mailbox
2929 // containing only important messages
2930 if (mFolder != null && mFolder.isImportantOnly()) {
2931 for (Conversation conv : mTarget) {
2932 conv.localDeleteOnUpdate = true;
2933 }
2934 }
2935 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2936 UIProvider.ConversationPriority.LOW);
2937 } else if (mAction == R.id.discard_drafts) {
2938 LogUtils.d(LOG_TAG, "Discarding draft messages");
2939 // Discarding draft messages is destructive in a "draft" mailbox
2940 if (mFolder != null && mFolder.isDraft()) {
2941 for (Conversation conv : mTarget) {
2942 conv.localDeleteOnUpdate = true;
2943 }
2944 }
2945 mConversationListCursor.discardDrafts(mTarget);
2946 // We don't support undoing discarding drafts
2947 undoEnabled = false;
Jin Cao512821c2014-05-30 15:54:04 -07002948 } else if (mAction == R.id.discard_outbox) {
2949 LogUtils.d(LOG_TAG, "Discarding failed messages in Outbox");
2950 mConversationListCursor.moveFailedIntoDrafts(mTarget);
2951 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002952 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07002953 if (undoEnabled && mTarget.size() > 0) {
mindypead50392012-08-23 11:03:53 -07002954 mHandler.postDelayed(new Runnable() {
2955 @Override
2956 public void run() {
2957 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002958 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002959 }
2960 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002961 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002962 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002963 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07002964 mCheckedSet.clear();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002965 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002966 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002967
2968 /**
2969 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002970 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002971 */
2972 private synchronized boolean isPerformed() {
2973 if (mCompleted) {
2974 return true;
2975 }
2976 mCompleted = true;
2977 return false;
2978 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002979 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002980
Vikram Aggarwald503df42012-05-11 10:13:35 -07002981 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2982 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002983 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002984 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002985 Collection<Conversation> target, boolean batch, boolean showUndo,
2986 final boolean isMoveTo) {
Jin Cao479753f2014-07-31 14:31:01 -07002987 // Actions are destructive only when the current folder can be un-assigned from and
Mindy Pereira8db7e402012-07-13 10:32:47 -07002988 // when the list of folders contains the current folder.
2989 final boolean isDestructive = mFolder
Jin Cao479753f2014-07-31 14:31:01 -07002990 .supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION)
Mindy Pereira8db7e402012-07-13 10:32:47 -07002991 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002992 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2993 if (isDestructive) {
2994 for (final Conversation c : target) {
2995 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002996 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002997 }
mindypc84759c2012-08-29 09:51:53 -07002998 final DestructiveAction folderChange;
Jin Cao30c881a2014-04-08 14:28:36 -07002999 final UndoCallback undoCallback = isMoveTo ?
3000 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to,
3001 mCurrentConversation)
3002 : null;
Vikram Aggarwald503df42012-05-11 10:13:35 -07003003 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003004 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07003005 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003006 /*
3007 * If this is a MOVE operation, we want the action folder to be the destination folder.
3008 * Otherwise, we want it to be the current folder.
3009 *
3010 * A set of folder operations is a move if there are exactly two operations: an add and
3011 * a remove.
3012 */
3013 final Folder actionFolder;
3014 if (folderOps.size() != 2) {
3015 actionFolder = mFolder;
3016 } else {
3017 Folder addedFolder = null;
3018 boolean hasRemove = false;
3019 for (final FolderOperation folderOperation : folderOps) {
3020 if (folderOperation.mAdd) {
3021 addedFolder = folderOperation.mFolder;
3022 } else {
3023 hasRemove = true;
3024 }
3025 }
3026
3027 if (hasRemove && addedFolder != null) {
3028 actionFolder = addedFolder;
3029 } else {
3030 actionFolder = mFolder;
3031 }
3032 }
3033
mindypc84759c2012-08-29 09:51:53 -07003034 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003035 batch, showUndo, isMoveTo, actionFolder, undoCallback);
Scott Kennedycaaeed32013-06-12 13:39:16 -07003036 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003037 } else {
mindypc84759c2012-08-29 09:51:53 -07003038 folderChange = getFolderChange(target, folderOps, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003039 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003040 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07003041 }
3042 }
3043
Mindy Pereira967ede62012-03-22 09:29:09 -07003044 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003045 public final void onRefreshRequired() {
Andy Huang12a05d22014-08-28 21:36:18 -07003046 if (isAnimating()) {
Andy Huangf21787a2014-05-02 14:22:01 +02003047 final ConversationListFragment f = getConversationListFragment();
3048 LogUtils.w(ConversationCursor.LOG_TAG,
3049 "onRefreshRequired: delay until animating done. cursor=%s adapter=%s",
3050 mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null);
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003051 return;
3052 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003053 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003054 if (mConversationListCursor.isRefreshRequired()) {
3055 mConversationListCursor.refresh();
3056 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003057 }
3058
mindyp5390fca2012-08-22 12:12:25 -07003059 @Override
mindyp6f54e1b2012-10-09 09:54:08 -07003060 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003061 boolean isAnimating = false;
3062 ConversationListFragment convListFragment = getConversationListFragment();
3063 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07003064 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07003065 }
3066 return isAnimating;
3067 }
3068
Marc Blankbf128eb2012-04-18 15:58:45 -07003069 /**
3070 * Called when the {@link ConversationCursor} is changed or has new data in it.
3071 * <p>
3072 * {@inheritDoc}
3073 */
3074 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003075 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08003076 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
3077 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06003078
3079 if (mDestroyed) {
3080 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
3081 return;
3082 }
3083
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003084 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07003085 // Swap cursors
3086 mConversationListCursor.sync();
Andy Huangf21787a2014-05-02 14:22:01 +02003087 } else {
3088 // (CLF guaranteed to be non-null due to check in isAnimating)
3089 LogUtils.w(LOG_TAG,
3090 "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s",
3091 mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
Marc Blankbf128eb2012-04-18 15:58:45 -07003092 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07003093 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003094 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07003095 }
3096
3097 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003098 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003099 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003100 mConversationListObservable.notifyChanged();
Jin Caoec0fa482014-08-28 16:38:08 -07003101 mCheckedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003102 }
3103
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003104 /**
3105 * If the Conversation List Fragment is visible, updates the fragment.
3106 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003107 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003108 final ConversationListFragment convList = getConversationListFragment();
3109 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003110 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003111 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003112 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003113 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003114 }
3115 }
3116
3117 /**
3118 * This class handles throttled refresh of the conversation list
3119 */
3120 static class RefreshTimerTask extends TimerTask {
3121 final Handler mHandler;
3122 final AbstractActivityController mController;
3123
3124 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3125 mHandler = handler;
3126 mController = controller;
3127 }
3128
3129 @Override
3130 public void run() {
3131 mHandler.post(new Runnable() {
3132 @Override
3133 public void run() {
3134 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3135 mController.onRefreshRequired();
3136 }});
3137 }
3138 }
3139
3140 /**
3141 * Cancel the refresh task, if it's running
3142 */
3143 private void cancelRefreshTask () {
3144 if (mConversationListRefreshTask != null) {
3145 mConversationListRefreshTask.cancel();
3146 mConversationListRefreshTask = null;
3147 }
3148 }
3149
3150 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003151 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Andy Huangf21787a2014-05-02 14:22:01 +02003152 if (animatedAdapter != null) {
3153 LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor,
3154 animatedAdapter);
3155 }
Paul Westbrook026139c2012-09-19 22:35:37 -07003156 if (mConversationListCursor == null) {
3157 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3158 return;
3159 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003160 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003161 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003162 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003163 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003164
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003165 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003166 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003167 mConversationListCursor.refresh();
3168 }
mindyp6f54e1b2012-10-09 09:54:08 -07003169 if (mRecentsDataUpdated) {
3170 mRecentsDataUpdated = false;
3171 mRecentFolderObservers.notifyChanged();
3172 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003173 }
3174
3175 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003176 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003177 // There are no selected conversations. Ensure that the listener and its associated actions
3178 // are blanked out.
3179 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003180 }
3181
3182 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003183 public void onSetPopulated(ConversationCheckedSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003184 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003185 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003186 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003187 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003188 }
3189
Mindy Pereira967ede62012-03-22 09:29:09 -07003190 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003191 public void onSetChanged(ConversationCheckedSet set) {
Mindy Pereira967ede62012-03-22 09:29:09 -07003192 // Do nothing. We don't care about changes to the set.
3193 }
3194
3195 @Override
Jin Caoec0fa482014-08-28 16:38:08 -07003196 public ConversationCheckedSet getCheckedSet() {
3197 return mCheckedSet;
Mindy Pereira967ede62012-03-22 09:29:09 -07003198 }
3199
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003200 /**
3201 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3202 */
3203 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003204 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003205 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003206 if (mCabActionMenu != null) {
3207 mCabActionMenu.deactivate();
3208 }
3209 }
3210
3211 /**
3212 * Re-enable the CAB menu if required. The selection set is not changed.
3213 */
3214 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003215 if (mCabActionMenu != null &&
3216 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003217 mCabActionMenu.activate();
3218 }
3219 }
3220
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003221 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003222 * Re-enable CAB mode only if we have an active selection
3223 */
3224 protected void maybeEnableCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003225 if (!mCheckedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003226 if (mCabActionMenu != null) {
3227 mCabActionMenu.activate();
3228 }
Tony Mantler43fab322013-07-26 16:38:35 -07003229 }
3230 }
3231
3232 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003233 * Unselect conversations and exit CAB mode.
3234 */
3235 protected final void exitCabMode() {
Jin Caoec0fa482014-08-28 16:38:08 -07003236 mCheckedSet.clear();
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003237 }
3238
Mindy Pereira967ede62012-03-22 09:29:09 -07003239 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003240 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003241 if (mAccount == null) {
3242 // We cannot search if there is no account. Drop the request to the floor.
3243 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3244 return;
3245 }
James Lemieux3531d7e2014-01-28 11:10:05 -08003246 if (mAccount.supportsSearch()) {
Jin Caoc6801eb2014-08-12 18:16:57 -07003247 mSearchViewController.showSearchActionBar(
3248 MaterialSearchViewController.SEARCH_VIEW_STATE_VISIBLE);
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003249 } else {
3250 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003251 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003252 }
3253 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003254
Mindy Pereira0963ef82012-04-10 11:43:01 -07003255 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003256 public void onTouchEvent(MotionEvent event) {
3257 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003258 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
James Lemieux9a110112014-08-07 15:23:13 -07003259 // if the toast bar is still animating, ignore this attempt to hide it
3260 if (mToastBar.isAnimating()) {
3261 return;
3262 }
3263
3264 // if the toast bar has not been seen long enough, ignore this attempt to hide it
3265 if (mToastBar.cannotBeHidden()) {
3266 return;
3267 }
3268
3269 // hide the toast bar
3270 mToastBar.hide(true /* animated */, false /* actionClicked */);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003271 }
3272 }
3273 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003274
Andy Huang632721e2012-04-11 16:57:26 -07003275 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003276 public void onConversationSeen() {
3277 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003278 }
3279
Andy Huang9d3fd922012-09-26 22:23:58 -07003280 @Override
3281 public boolean isInitialConversationLoading() {
3282 return mPagerController.isInitialConversationLoading();
3283 }
3284
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003285 /**
3286 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3287 * insufficient because that doesn't check if the window is currently in focus or not.
3288 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003289 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003290 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3291 }
3292
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003293 /**
3294 * This class handles callbacks that create a {@link ConversationCursor}.
3295 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003296 private class ConversationListLoaderCallbacks implements
3297 LoaderManager.LoaderCallbacks<ConversationCursor> {
3298
3299 @Override
3300 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003301 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3302 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003303 final boolean ignoreInitialConversationLimit =
3304 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003305 if (account == null || folder == null) {
3306 return null;
3307 }
Andrew Sapperstein5bb4d052014-03-31 16:22:31 -07003308 return new ConversationCursorLoader(mActivity, account,
Andy Huangf21787a2014-05-02 14:22:01 +02003309 folder.conversationListUri, folder.getTypeDescription(),
3310 ignoreInitialConversationLimit);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003311 }
3312
3313 @Override
3314 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003315 LogUtils.d(LOG_TAG,
3316 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3317 data, loader, this);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003318 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003319 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003320 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003321 return;
3322 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003323 // Clear our all pending destructive actions before swapping the conversation cursor
3324 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003325 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003326 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003327 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003328 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003329 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003330 // Handle actions that were deferred until after the conversation list was loaded.
3331 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3332 callback.onLoadFinished();
3333 }
3334 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003335
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003336 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003337 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003338 // The conversation list is already listening to list changes and gets notified
3339 // in the mConversationListObservable.notifyChanged() line above. We only need to
3340 // check and inform the cursor of the change in visibility here.
3341 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003342 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003343 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003344 }
3345
3346 @Override
3347 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003348 LogUtils.d(LOG_TAG,
3349 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3350 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003351
3352 if (mConversationListCursor != null) {
3353 // Unregister the listener
3354 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003355 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003356 mConversationListCursor = null;
3357
3358 // Inform anyone who is interested about the change
3359 mTracker.onCursorUpdated();
3360 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003361 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003362 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003363 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003364
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003365 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003366 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3367 */
3368 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3369 @Override
3370 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3371 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3372 switch (id) {
3373 case LOADER_FOLDER_CURSOR:
3374 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3375 final ObjectCursorLoader<Folder> loader = new
3376 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003377 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003378 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3379 return loader;
3380 case LOADER_RECENT_FOLDERS:
3381 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003382 if (mAccount != null && mAccount.recentFolderListUri != null
3383 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003384 return new ObjectCursorLoader<Folder>(mContext,
3385 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3386 }
3387 break;
3388 case LOADER_ACCOUNT_INBOX:
3389 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3390 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3391 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3392 mAccount.folderListUri : defaultInbox;
3393 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3394 if (inboxUri != null) {
3395 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3396 everything, Folder.FACTORY);
3397 }
3398 break;
3399 case LOADER_SEARCH:
3400 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3401 return Folder.forSearchResults(mAccount,
3402 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
Jin Cao1a864cc2014-05-21 11:16:39 -07003403 // We can just use current time as a unique identifier for this search
3404 Long.toString(SystemClock.uptimeMillis()),
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003405 mActivity.getActivityContext());
3406 case LOADER_FIRST_FOLDER:
3407 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3408 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3409 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3410 if (mConversationToShow != null && mConversationToShow.position < 0){
3411 mConversationToShow.position = 0;
3412 }
3413 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3414 everything, Folder.FACTORY);
3415 default:
3416 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3417 return null;
3418 }
3419 return null;
3420 }
3421
3422 @Override
3423 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3424 if (data == null) {
3425 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3426 }
3427 switch (loader.getId()) {
3428 case LOADER_FOLDER_CURSOR:
3429 if (data != null && data.moveToFirst()) {
3430 final Folder folder = data.getModel();
3431 setHasFolderChanged(folder);
3432 mFolder = folder;
3433 mFolderObservable.notifyChanged();
3434 } else {
3435 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003436 mFolder != null ? mFolder.name : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003437 }
3438 break;
3439 case LOADER_RECENT_FOLDERS:
3440 // Few recent folders and we are running on a phone? Populate the default
3441 // recents. The number of default recent folders is at least 2: every provider
3442 // has at least two folders, and the recent folder count never decreases.
3443 // Having a single recent folder is an erroneous case, and we can gracefully
3444 // recover by populating default recents. The default recents will not stomp on
3445 // the existing value: it will be shown in addition to the default folders:
3446 // the max number of recent folders is more than 1+num(defaultRecents).
3447 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3448 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3449 @Override
3450 protected Void doInBackground(Uri... uri) {
3451 // Asking for an update on the URI and ignore the result.
3452 final ContentResolver resolver = mContext.getContentResolver();
3453 resolver.update(uri[0], null, null, null);
3454 return null;
3455 }
3456 }
3457 final Uri uri = mAccount.defaultRecentFolderListUri;
3458 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3459 new PopulateDefault().execute(uri);
3460 break;
3461 }
3462 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3463 mRecentFolderList.loadFromUiProvider(data);
3464 if (isAnimating()) {
3465 mRecentsDataUpdated = true;
3466 } else {
3467 mRecentFolderObservers.notifyChanged();
3468 }
3469 break;
3470 case LOADER_ACCOUNT_INBOX:
3471 if (data != null && !data.isClosed() && data.moveToFirst()) {
3472 final Folder inbox = data.getModel();
Alice Yangebeef1b2013-09-04 06:41:10 +00003473 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003474 // Just want to get the inbox, don't care about updates to it
3475 // as this will be tracked by the folder change listener.
3476 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3477 } else {
3478 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
Tony Mantler26a20752014-02-28 16:44:24 -08003479 mAccount != null ? mAccount.getEmailAddress() : "");
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003480 }
3481 break;
3482 case LOADER_SEARCH:
3483 if (data != null && data.getCount() > 0) {
3484 data.moveToFirst();
3485 final Folder search = data.getModel();
3486 updateFolder(search);
3487 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3488 mActivity.getIntent()
3489 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3490 showConversationList(mConvListContext);
3491 mActivity.invalidateOptionsMenu();
3492 mHaveSearchResults = search.totalCount > 0;
3493 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3494 } else {
3495 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3496 }
3497 break;
3498 case LOADER_FIRST_FOLDER:
3499 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3500 return;
3501 }
3502 final Folder folder = data.getModel();
3503 boolean handled = false;
3504 if (folder != null) {
Alice Yangebeef1b2013-09-04 06:41:10 +00003505 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003506 handled = true;
3507 }
3508 if (mConversationToShow != null) {
3509 // Open the conversation.
3510 showConversation(mConversationToShow);
3511 handled = true;
3512 }
3513 if (!handled) {
3514 // We have an account, but nothing else: load the default inbox.
3515 loadAccountInbox();
3516 }
3517 mConversationToShow = null;
3518 // And don't run this anymore.
3519 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3520 break;
3521 }
3522 }
3523
3524 @Override
3525 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3526 }
3527 }
3528
3529 /**
3530 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3531 */
3532 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3533 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3534 final CursorCreator<Account> mFactory = Account.FACTORY;
3535
3536 @Override
3537 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3538 switch (id) {
3539 case LOADER_ACCOUNT_CURSOR:
3540 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3541 return new ObjectCursorLoader<Account>(mContext,
3542 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3543 case LOADER_ACCOUNT_UPDATE_CURSOR:
3544 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3545 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3546 mFactory);
3547 default:
3548 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3549 break;
3550 }
3551 return null;
3552 }
3553
3554 @Override
3555 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3556 ObjectCursor<Account> data) {
3557 if (data == null) {
3558 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3559 }
3560 switch (loader.getId()) {
3561 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003562 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003563 if (data == null) {
3564 // Nothing useful to do if we have no valid data.
3565 break;
3566 }
Andy Huang761522c2013-08-08 13:09:11 -07003567 final long count = data.getCount();
3568 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003569 // If an empty cursor is returned, the MailAppProvider is indicating that
3570 // no accounts have been specified. We want to navigate to the
3571 // "add account" activity that will handle the intent returned by the
3572 // MailAppProvider
3573
3574 // If the MailAppProvider believes that all accounts have been loaded,
3575 // and the account list is still empty, we want to prompt the user to add
3576 // an account.
3577 final Bundle extras = data.getExtras();
3578 final boolean accountsLoaded =
3579 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3580
3581 if (accountsLoaded) {
3582 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3583 (mContext);
3584 if (noAccountIntent != null) {
3585 mActivity.startActivityForResult(noAccountIntent,
3586 ADD_ACCOUNT_REQUEST_CODE);
3587 }
3588 }
3589 } else {
3590 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003591 if (!mHaveAccountList || accountListUpdated) {
3592 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003593 }
Andy Huang761522c2013-08-08 13:09:11 -07003594 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3595 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003596 }
3597 break;
3598 case LOADER_ACCOUNT_UPDATE_CURSOR:
3599 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003600 if (data != null && data.moveToFirst()) {
3601 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003602 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003603 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003604 final Settings previousSettings = mAccount.settings;
3605
3606 // Update the controller's reference to the current account
3607 mAccount = updatedAccount;
3608 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3609 + "mAccount = %s", mAccount.uri);
3610
3611 // Only notify about a settings change if something differs
3612 if (!Objects.equal(mAccount.settings, previousSettings)) {
3613 mAccountObservers.notifyChanged();
3614 }
3615 perhapsEnterWaitMode();
3616 } else {
3617 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3618 + " %s", updatedAccount.uri, mAccount.uri);
3619 // We need to restart the loader, so the correct account information
3620 // will be returned.
3621 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3622 }
3623 }
3624 break;
3625 }
3626 }
3627
3628 @Override
3629 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003630 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003631 }
3632 }
3633
3634 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003635 * Updates controller state based on search results and shows first conversation if required.
3636 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003637 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003638 if (mCurrentConversation == null) {
3639 // Shown for search results in two-pane mode only.
3640 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3641 && mConversationListCursor.getCount() > 0;
3642 if (!shouldShowFirstConversation()) {
3643 return;
3644 }
3645 mConversationListCursor.moveToPosition(0);
3646 final Conversation conv = new Conversation(mConversationListCursor);
3647 conv.position = 0;
3648 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003649 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003650 }
3651
3652 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003653 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3654 * next destructive action..
3655 * @param nextAction the next destructive action to be performed. This can be null.
3656 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003657 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003658 // If there is a pending action, perform that first.
3659 if (mPendingDestruction != null) {
3660 mPendingDestruction.performAction();
3661 }
3662 mPendingDestruction = nextAction;
3663 }
3664
3665 /**
3666 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003667 * 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 -07003668 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003669 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003670 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003671 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003672 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3673 // destructive action from the controller and for the controller to own the action. This is
3674 // a half-way solution while refactoring DestructiveAction.
3675 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003676 }
3677
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003678 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003679 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003680 final DestructiveAction da = new ConversationAction(action, mCheckedSet.values(), true);
Jin Cao30c881a2014-04-08 14:28:36 -07003681 da.setUndoCallback(undoCallback);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003682 registerDestructiveAction(da);
3683 return da;
3684 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003685
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003686 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003687 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) {
Jin Caoec0fa482014-08-28 16:38:08 -07003688 return getDeferredAction(action, mCheckedSet.values(), true, undoCallback);
mindypf0656a12012-10-01 08:30:57 -07003689 }
3690
3691 /**
3692 * Get a destructive action for a menu action. This is a temporary method,
3693 * to control the profusion of {@link DestructiveAction} classes that are
3694 * created. Please do not copy this paradigm.
3695 * @param action the resource ID of the menu action: R.id.delete, for
3696 * example
3697 * @param target the conversations to act upon.
3698 * @return a {@link DestructiveAction} that performs the specified action.
3699 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003700 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
Jin Cao30c881a2014-04-08 14:28:36 -07003701 boolean batch, UndoCallback callback) {
3702 ConversationAction cAction = new ConversationAction(action, target, batch);
3703 cAction.setUndoCallback(callback);
3704 return cAction;
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003705 }
3706
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003707 /**
3708 * Class to change the folders that are assigned to a set of conversations. This is destructive
3709 * because the user can remove the current folder from the conversation, in which case it has
3710 * to be animated away from the current folder.
3711 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003712 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003713 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003714 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003715 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003716 /** Whether this destructive action has already been performed */
3717 private boolean mCompleted;
Martin Hibdone78c40f2013-10-10 18:29:25 -07003718 private final boolean mIsSelectedSet;
3719 private final boolean mShowUndo;
3720 private final int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003721 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003722
Jin Cao30c881a2014-04-08 14:28:36 -07003723 private UndoCallback mUndoCallback;
3724
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003725 /**
3726 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003727 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003728 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003729 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003730 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003731 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003732 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003733 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003734 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003735 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003736 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003737 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003738 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003739 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003740 }
3741
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003742 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07003743 public void setUndoCallback(UndoCallback undoCallback) {
3744 mUndoCallback = undoCallback;
3745 }
3746
3747 @Override
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003748 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003749 if (isPerformed()) {
3750 return;
3751 }
Jin Cao2cb6c1c2014-09-18 14:18:17 -07003752 if (mIsDestructive && mShowUndo && mTarget.size() > 0) {
mindypcb0b30e2012-11-30 10:16:35 -08003753 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003754 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003755 onUndoAvailable(undoOp);
3756 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003757 // For each conversation, for each operation, add/ remove the
3758 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003759 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3760 ArrayList<Uri> folderUris;
3761 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003762 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003763 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3764 .getRawFolders());
3765 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003766 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003767 if (mIsDestructive) {
3768 target.localDeleteOnUpdate = true;
3769 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003770 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003771 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003772 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003773 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003774 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003775 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003776 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003777 }
3778 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003779 ops.add(mConversationListCursor.getConversationFolderOperation(target,
Jin Cao30c881a2014-04-08 14:28:36 -07003780 folderUris, adds, targetFolders.values(), mUndoCallback));
mindyp389f0b22012-08-29 11:12:54 -07003781 }
3782 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003783 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003784 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003785 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003786 if (mIsSelectedSet) {
Jin Caoec0fa482014-08-28 16:38:08 -07003787 mCheckedSet.clear();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003788 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003789 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003790
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003791 /**
3792 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003793 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003794 */
3795 private synchronized boolean isPerformed() {
3796 if (mCompleted) {
3797 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003798 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003799 mCompleted = true;
3800 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003801 }
3802 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003803
mindypc84759c2012-08-29 09:51:53 -07003804 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3805 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003806 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3807 UndoCallback undoCallback) {
mindypc84759c2012-08-29 09:51:53 -07003808 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Jin Cao30c881a2014-04-08 14:28:36 -07003809 isBatch, showUndo, isMoveTo, actionFolder, undoCallback);
mindypc84759c2012-08-29 09:51:53 -07003810 registerDestructiveAction(da);
3811 return da;
3812 }
3813
3814 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003815 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003816 boolean showUndo, final boolean isMoveTo, final Folder actionFolder,
3817 UndoCallback undoCallback) {
3818 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch,
3819 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
3820 fd.setUndoCallback(undoCallback);
3821 return fd;
Mindy Pereira01f30502012-08-14 10:30:51 -07003822 }
3823
3824 @Override
3825 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3826 Folder toRemove, boolean isDestructive, boolean isBatch,
Jin Cao30c881a2014-04-08 14:28:36 -07003827 boolean showUndo, UndoCallback undoCallback) {
Mindy Pereira01f30502012-08-14 10:30:51 -07003828 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3829 folderOps.add(new FolderOperation(toRemove, false));
Jin Cao30c881a2014-04-08 14:28:36 -07003830 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003831 showUndo, R.id.remove_folder, mFolder);
Jin Cao30c881a2014-04-08 14:28:36 -07003832 da.setUndoCallback(undoCallback);
3833 return da;
Mindy Pereira01f30502012-08-14 10:30:51 -07003834 }
3835
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003836 @Override
3837 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003838 final ConversationListFragment convList = getConversationListFragment();
3839 if (convList == null) {
3840 return;
3841 }
3842 convList.requestListRefresh();
3843 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003844
3845 protected final ActionClickedListener getUndoClickedListener(
3846 final AnimatedAdapter listAdapter) {
3847 return new ActionClickedListener() {
3848 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003849 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003850 if (mAccount.undoUri != null) {
3851 // NOTE: We might want undo to return the messages affected, in which case
3852 // the resulting cursor might be interesting...
3853 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3854 // commands to undo
3855 if (mConversationListCursor != null) {
3856 mConversationListCursor.undo(
3857 mActivity.getActivityContext(), mAccount.undoUri);
3858 }
3859 if (listAdapter != null) {
3860 listAdapter.setUndo(true);
3861 }
3862 }
3863 }
3864 };
3865 }
3866
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003867 /**
3868 * Shows an error toast in the bottom when a folder was not fetched successfully.
3869 * @param folder the folder which could not be fetched.
3870 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3871 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003872 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003873
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003874 final ActionClickedListener listener;
3875 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003876 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003877 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003878 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003879 // The sync request that caused this failure.
3880 final int syncRequest = lastSyncResult >> 4;
3881 // Show: User explicitly pressed the refresh button and there is no connection
3882 // Show: The first time the user enters the app and there is no connection
3883 // TODO(viki): Implement this.
3884 // Reference: http://b/7202801
3885 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3886 // Don't show: Already in the app; user switches to a synced label
3887 // Don't show: In a live label and a background sync fails
3888 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3889 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3890 if (avoidToast) {
3891 return;
3892 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003893 listener = getRetryClickedListener(folder);
3894 actionTextResourceId = R.string.retry;
3895 break;
3896 case UIProvider.LastSyncResult.AUTH_ERROR:
3897 listener = getSignInClickedListener();
3898 actionTextResourceId = R.string.signin;
3899 break;
3900 case UIProvider.LastSyncResult.SECURITY_ERROR:
3901 return; // Currently we do nothing for security errors.
3902 case UIProvider.LastSyncResult.STORAGE_ERROR:
3903 listener = getStorageErrorClickedListener();
3904 actionTextResourceId = R.string.info;
3905 break;
3906 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3907 listener = getInternalErrorClickedListener();
3908 actionTextResourceId = R.string.report;
3909 break;
3910 default:
3911 return;
3912 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003913 mToastBar.show(listener,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003914 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003915 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003916 replaceVisibleToast,
James Lemieux0ec03e82014-09-03 14:01:53 -07003917 true /* autohide */,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003918 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003919 }
3920
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003921 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003922 return new ActionClickedListener() {
3923 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003924 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003925 final Uri uri = folder.refreshUri;
3926
3927 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003928 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003929 }
3930 }
3931 };
3932 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003933
3934 private ActionClickedListener getSignInClickedListener() {
3935 return new ActionClickedListener() {
3936 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003937 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003938 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003939 }
3940 };
3941 }
3942
3943 private ActionClickedListener getStorageErrorClickedListener() {
3944 return new ActionClickedListener() {
3945 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003946 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003947 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003948 }
3949 };
3950 }
3951
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003952 private void showStorageErrorDialog() {
3953 DialogFragment fragment = (DialogFragment)
3954 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3955 if (fragment == null) {
3956 fragment = SyncErrorDialogFragment.newInstance();
3957 }
3958 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3959 }
3960
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003961 private ActionClickedListener getInternalErrorClickedListener() {
3962 return new ActionClickedListener() {
3963 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003964 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003965 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003966 }
3967 };
3968 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003969
3970 @Override
3971 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3972 Uri uri = null;
3973 switch (errorStatus) {
3974 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3975 if (folder != null && folder.refreshUri != null) {
3976 uri = folder.refreshUri;
3977 }
3978 break;
3979 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003980 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003981 return;
3982 case UIProvider.LastSyncResult.SECURITY_ERROR:
3983 return; // Currently we do nothing for security errors.
3984 case UIProvider.LastSyncResult.STORAGE_ERROR:
3985 showStorageErrorDialog();
3986 return;
3987 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08003988 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003989 return;
3990 default:
3991 return;
3992 }
3993
3994 if (uri != null) {
3995 startAsyncRefreshTask(uri);
3996 }
3997 }
3998
3999 @Override
4000 public void onFooterViewLoadMoreClick(Folder folder) {
4001 if (folder != null && folder.loadMoreUri != null) {
4002 startAsyncRefreshTask(folder.loadMoreUri);
4003 }
4004 }
4005
4006 private void startAsyncRefreshTask(Uri uri) {
4007 if (mFolderSyncTask != null) {
4008 mFolderSyncTask.cancel(true);
4009 }
4010 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4011 mFolderSyncTask.execute();
4012 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004013
4014 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004015 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004016 final Intent authenticationIntent =
4017 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4018 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4019 }
4020 }
mindypca87de42012-09-28 15:02:39 -07004021
4022 @Override
4023 public void onAccessibilityStateChanged() {
4024 // Clear the cache of objects.
4025 ConversationItemViewModel.onAccessibilityUpdated();
4026 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004027 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004028 if (frag != null) {
4029 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4030 if (adapter != null) {
4031 adapter.notifyDataSetInvalidated();
4032 }
4033 }
4034 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004035
4036 @Override
Jin Cao30c881a2014-04-08 14:28:36 -07004037 public void makeDialogListener (final int action, final boolean isBatch,
4038 UndoCallback undoCallback) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004039 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004040 if (isBatch) {
Jin Caoec0fa482014-08-28 16:38:08 -07004041 target = mCheckedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004042 } else {
4043 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4044 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004045 }
Jin Cao30c881a2014-04-08 14:28:36 -07004046 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch,
4047 undoCallback);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004048 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004049 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004050 mDialogListener = new AlertDialog.OnClickListener() {
4051 @Override
4052 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004053 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004054 // Afterwards, let's remove references to the listener and the action.
4055 setListener(null, -1);
4056 }
4057 };
4058 }
4059
4060 @Override
4061 public AlertDialog.OnClickListener getListener() {
4062 return mDialogListener;
4063 }
4064
4065 /**
4066 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4067 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4068 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004069 * @param listener the listener that will perform the task for this dialog's positive action.
4070 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004071 */
4072 private void setListener(AlertDialog.OnClickListener listener, final int action){
4073 mDialogListener = listener;
4074 mDialogAction = action;
4075 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004076
4077 @Override
4078 public VeiledAddressMatcher getVeiledAddressMatcher() {
4079 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004080 }
4081
4082 @Override
4083 public void setDetachedMode() {
4084 // Tell the conversation list not to select anything.
4085 final ConversationListFragment frag = getConversationListFragment();
4086 if (frag != null) {
4087 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004088 } else if (mIsTablet) {
4089 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004090 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4091 }
4092 mDetachedConvUri = mCurrentConversation.uri;
4093 }
4094
4095 private void clearDetachedMode() {
4096 // Tell the conversation list to go back to its usual selection behavior.
4097 final ConversationListFragment frag = getConversationListFragment();
4098 if (frag != null) {
4099 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004100 } else if (mIsTablet) {
4101 // How did we ever land here? Detached mode, and no CLF on tablet???
4102 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004103 }
4104 mDetachedConvUri = null;
4105 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004106
Andy Huang61f26c22014-03-13 18:24:52 -07004107 @Override
Andy Huang8712f412014-08-21 23:10:41 -07004108 public boolean shouldPreventListSwipesEntirely() {
4109 return false;
4110 }
4111
4112 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004113 public DrawerController getDrawerController() {
4114 return mDrawerListener;
4115 }
4116
4117 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener>
4118 implements DrawerLayout.DrawerListener, DrawerController {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004119 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004120 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004121
4122 public MailDrawerListener() {
4123 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004124 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004125 }
4126
Andy Huang12b3ee42013-04-24 22:49:43 -07004127 @Override
Andy Huang3e95fe92014-07-25 17:46:33 -07004128 public boolean isDrawerEnabled() {
4129 return AbstractActivityController.this.isDrawerEnabled();
4130 }
4131
4132 @Override
Andy Huang61f26c22014-03-13 18:24:52 -07004133 public void registerDrawerListener(DrawerLayout.DrawerListener l) {
4134 registerObserver(l);
4135 }
4136
4137 @Override
4138 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) {
4139 unregisterObserver(l);
4140 }
4141
4142 @Override
4143 public boolean isDrawerOpen() {
4144 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
4145 }
4146
4147 @Override
4148 public boolean isDrawerVisible() {
4149 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout);
4150 }
4151
4152 @Override
Andy Huangf58e4c32014-07-09 16:58:18 -07004153 public void toggleDrawerState() {
4154 AbstractActivityController.this.toggleDrawerState();
4155 }
4156
4157 @Override
Andy Huang12b3ee42013-04-24 22:49:43 -07004158 public void onDrawerOpened(View drawerView) {
4159 mDrawerToggle.onDrawerOpened(drawerView);
Andy Huang61f26c22014-03-13 18:24:52 -07004160
4161 for (DrawerLayout.DrawerListener l : mObservers) {
4162 l.onDrawerOpened(drawerView);
4163 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004164 }
4165
4166 @Override
4167 public void onDrawerClosed(View drawerView) {
4168 mDrawerToggle.onDrawerClosed(drawerView);
4169 if (mHasNewAccountOrFolder) {
4170 refreshDrawer();
4171 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004172
4173 // When closed, we want to use either the burger, or up, based on where we are
4174 final int mode = mViewMode.getMode();
Jin Cao9695e002014-05-29 11:56:44 -07004175 final boolean isTopLevel = Folder.isRoot(mFolder);
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004176 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel));
Andy Huang61f26c22014-03-13 18:24:52 -07004177
4178 for (DrawerLayout.DrawerListener l : mObservers) {
4179 l.onDrawerClosed(drawerView);
4180 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004181 }
4182
4183 /**
4184 * As part of the overriden function, it will animate the alpha of the conversation list
4185 * view along with the drawer sliding when we're in the process of switching accounts or
4186 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4187 */
4188 @Override
4189 public void onDrawerSlide(View drawerView, float slideOffset) {
4190 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4191 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4192 mListViewForAnimating.setAlpha(slideOffset);
4193 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004194
4195 // This code handles when to change the visibility of action items
4196 // based on drawer state. The basic logic is that right when we
4197 // open the drawer, we hide the action items. We show the action items
4198 // when the drawer closes. However, due to the animation of the drawer closing,
4199 // to make the reshowing of the action items feel right, we make the items visible
4200 // slightly sooner.
4201 //
4202 // However, to make the animating behavior work properly, we have to know whether
4203 // we're animating open or closed. Only if we're animating closed do we want to
4204 // show the action items early. We save the last slide offset so that we can compare
4205 // the current slide offset to it to determine if we're opening or closing.
4206 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4207 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4208 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004209 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004210 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004211 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4212 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004213 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004214 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004215 }
4216 } else {
4217 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4218 mHideMenuItems = false;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004219 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004220 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004221 } else if (!mHideMenuItems && slideOffset > 0.f) {
4222 mHideMenuItems = true;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07004223 mActivity.supportInvalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004224 disableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004225 }
4226 }
4227
4228 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004229
4230 // If we're sliding, we always want to show the burger
4231 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */);
Andy Huang61f26c22014-03-13 18:24:52 -07004232
4233 for (DrawerLayout.DrawerListener l : mObservers) {
4234 l.onDrawerSlide(drawerView, slideOffset);
4235 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004236 }
4237
4238 /**
4239 * This condition here should only be called when the drawer is stuck in a weird state
4240 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4241 * and, more importantly, unlock the drawer when this is the case.
4242 */
4243 @Override
4244 public void onDrawerStateChanged(int newState) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004245 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004246 mDrawerState = newState;
4247 mDrawerToggle.onDrawerStateChanged(mDrawerState);
Andy Huang61f26c22014-03-13 18:24:52 -07004248
4249 for (DrawerLayout.DrawerListener l : mObservers) {
4250 l.onDrawerStateChanged(newState);
4251 }
4252
Andy Huange764cfd2014-02-26 11:55:03 -08004253 if (mViewMode.isSearchMode()) {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004254 return;
4255 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004256 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4257 if (mHasNewAccountOrFolder) {
4258 refreshDrawer();
4259 }
4260 if (mConversationListLoadFinishedIgnored) {
4261 mConversationListLoadFinishedIgnored = false;
4262 final Bundle args = new Bundle();
4263 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4264 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4265 mActivity.getLoaderManager().initLoader(
4266 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4267 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004268 }
4269 }
4270
4271 /**
4272 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4273 * conversation list, and finish end actions. Also, make
4274 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4275 */
4276 public void refreshDrawer() {
4277 mHasNewAccountOrFolder = false;
4278 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4279 ConversationListFragment conversationList = getConversationListFragment();
4280 if (conversationList != null) {
4281 conversationList.clear();
4282 }
Tony Mantler54022ee2014-07-07 13:43:35 -07004283 mFolderOrAccountObservers.notifyChanged();
Andy Huang12b3ee42013-04-24 22:49:43 -07004284 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004285
4286 /**
4287 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4288 * by {@link #onDrawerStateChanged(int)}.
4289 * @return The {@link DrawerLayout}'s current state. One of
4290 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4291 * or {@link DrawerLayout#STATE_SETTLING}.
4292 */
4293 public int getDrawerState() {
4294 return mDrawerState;
4295 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004296 }
4297
Scott Kennedy8a72b852013-05-02 14:18:50 -07004298 @Override
4299 public boolean isDrawerPullEnabled() {
Martin Hibdon371a71c2014-02-19 13:55:28 -08004300 return true;
Scott Kennedy8a72b852013-05-02 14:18:50 -07004301 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004302
4303 @Override
4304 public boolean shouldHideMenuItems() {
4305 return mHideMenuItems;
4306 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004307
4308 protected void navigateUpFolderHierarchy() {
4309 new AsyncTask<Void, Void, Folder>() {
4310 @Override
4311 protected Folder doInBackground(final Void... params) {
4312 if (mInbox == null) {
4313 // We don't have an inbox, but we need it
4314 final Cursor cursor = mContext.getContentResolver().query(
4315 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4316 null, null);
4317
4318 if (cursor != null) {
4319 try {
4320 if (cursor.moveToFirst()) {
4321 mInbox = new Folder(cursor);
4322 }
4323 } finally {
4324 cursor.close();
4325 }
4326 }
4327 }
4328
4329 // Now try to load our parent
4330 final Folder folder;
4331
Alice Yang5ac10032013-09-04 06:41:43 +00004332 if (mFolder != null) {
Martin Hibdone78c40f2013-10-10 18:29:25 -07004333 Cursor cursor = null;
4334 try {
4335 cursor = mContext.getContentResolver().query(mFolder.parent,
4336 UIProvider.FOLDERS_PROJECTION, null, null, null);
Alice Yangebeef1b2013-09-04 06:41:10 +00004337
Martin Hibdone78c40f2013-10-10 18:29:25 -07004338 if (cursor == null || !cursor.moveToFirst()) {
4339 // We couldn't load the parent, so use the inbox
4340 folder = mInbox;
4341 } else {
Alice Yang5ac10032013-09-04 06:41:43 +00004342 folder = new Folder(cursor);
Martin Hibdone78c40f2013-10-10 18:29:25 -07004343 }
4344 } finally {
4345 if (cursor != null) {
Alice Yang5ac10032013-09-04 06:41:43 +00004346 cursor.close();
4347 }
Alice Yangebeef1b2013-09-04 06:41:10 +00004348 }
Alice Yang5ac10032013-09-04 06:41:43 +00004349 } else {
4350 folder = mInbox;
Alice Yangebeef1b2013-09-04 06:41:10 +00004351 }
4352
4353 return folder;
4354 }
4355
4356 @Override
4357 protected void onPostExecute(final Folder result) {
4358 onFolderSelected(result);
4359 }
4360 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4361 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004362
4363 @Override
4364 public Parcelable getConversationListScrollPosition(final String folderUri) {
4365 return mConversationListScrollPositions.getParcelable(folderUri);
4366 }
4367
4368 @Override
4369 public void setConversationListScrollPosition(final String folderUri,
4370 final Parcelable savedPosition) {
4371 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4372 }
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004373
4374 @Override
4375 public View.OnClickListener getNavigationViewClickListener() {
4376 return mHomeButtonListener;
4377 }
4378
4379 // TODO: Fold this into the outer class when b/16627877 is fixed
4380 private class HomeButtonListener implements View.OnClickListener {
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004381 @Override
4382 public void onClick(View v) {
Jin Cao405a3442014-08-25 13:49:33 -07004383 handleUpPress();
Andrew Sapperstein53de4482014-07-29 02:39:39 +00004384 }
4385 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004386}