blob: 625915638ad05b28387bb6e922ecdb4fd91f509f [file] [log] [blame]
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001/*******************************************************************************
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
Andy Huang12b3ee42013-04-24 22:49:43 -070020import android.animation.ValueAnimator;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080021import android.app.ActionBar;
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080022import android.app.ActionBar.LayoutParams;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080023import android.app.Activity;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070024import android.app.AlertDialog;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080025import android.app.Dialog;
Andrew Sapperstein00179f12012-08-09 15:15:40 -070026import android.app.DialogFragment;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -070027import android.app.Fragment;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -070028import android.app.FragmentManager;
Andy Huangf9a73482012-03-13 15:54:02 -070029import android.app.LoaderManager;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070030import android.app.SearchManager;
Andy Huang839ada22012-07-20 15:48:40 -070031import android.content.ContentProviderOperation;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080032import android.content.ContentResolver;
Mindy Pereira6c2663d2012-07-20 15:37:29 -070033import android.content.ContentValues;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080034import android.content.Context;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070035import android.content.DialogInterface;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080036import android.content.DialogInterface.OnClickListener;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080037import android.content.Intent;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -080038import android.content.Loader;
Paul Westbrook57246a42013-04-21 09:40:22 -070039import android.content.res.Configuration;
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -080040import android.content.res.Resources;
Scott Kennedyad418142013-07-10 17:18:22 -070041import android.database.Cursor;
Andy Huang632721e2012-04-11 16:57:26 -070042import android.database.DataSetObservable;
43import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080044import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070045import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080046import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070047import android.os.Handler;
Scott Kennedyf77806e2013-08-30 11:38:15 -070048import android.os.Parcelable;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070049import android.provider.SearchRecentSuggestions;
Andy Huang12b3ee42013-04-24 22:49:43 -070050import android.support.v4.app.ActionBarDrawerToggle;
51import android.support.v4.widget.DrawerLayout;
Mindy Pereiraacf60392012-04-06 09:11:00 -070052import android.view.DragEvent;
Andy Huang12b3ee42013-04-24 22:49:43 -070053import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080054import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080055import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080056import 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;
68import com.android.mail.analytics.AnalyticsUtils;
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;
Mindy Pereira9b875682012-02-15 18:10:54 -080077import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080078import com.android.mail.content.CursorCreator;
79import com.android.mail.content.ObjectCursor;
80import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080081import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080082import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070083import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080084import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070085import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070086import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080087import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070088import com.android.mail.providers.SuggestionsProvider;
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;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070091import com.android.mail.providers.UIProvider.AccountColumns;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070092import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070093import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070094import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080095import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070096import com.android.mail.providers.UIProvider.FolderCapabilities;
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;
Scott Kennedycb85aea2013-02-25 13:08:32 -0800102import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700103import com.android.mail.utils.Observable;
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;
Andy Huangc1fb9a92013-02-11 13:09:12 -0800115import java.util.Deque;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700116import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700117import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800118import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700119import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800120
121
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800122/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800123 * This is an abstract implementation of the Activity Controller. This class
124 * knows how to respond to menu items, state changes, layout changes, etc. It
125 * weaves together the views and listeners, dispatching actions to the
126 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800127 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800128 * Even though this class is abstract, it should provide default implementations
129 * for most, if not all the methods in the ActivityController interface. This
130 * makes the task of the subclasses easier: OnePaneActivityController and
131 * TwoPaneActivityController can be concise when the common functionality is in
132 * AbstractActivityController.
133 * </p>
134 * <p>
135 * In the Gmail codebase, this was called BaseActivityController
136 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800137 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700138public abstract class AbstractActivityController implements ActivityController,
139 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800140 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700141 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800142 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700143 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700144 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700145 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700146 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700147 /** Tag for {@link #mSelectedSet} */
148 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700149 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700150 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700151 /** Tag for {@link #mFolderListFolder} */
152 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700153 /** Tag for {@link ConversationListContext#searchQuery} */
154 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800155 /** Tag for {@link #mDialogAction} */
156 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800157 /** Tag for {@link #mDialogFromSelectedSet} */
158 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800159 /** Tag for {@link #mDetachedConvUri} */
160 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Scott Kennedyad418142013-07-10 17:18:22 -0700161 /** Key to store {@link #mInbox}. */
Scott Kennedyf77806e2013-08-30 11:38:15 -0700162 private static final String SAVED_INBOX_KEY = "m-inbox";
163 /** Key to store {@link #mConversationListScrollPositions} */
164 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS =
165 "saved-conversation-list-scroll-positions";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800166
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700167 /** Tag used when loading a wait fragment */
168 protected static final String TAG_WAIT = "wait-fragment";
169 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700170 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Scott Kennedy103319a2013-07-26 13:35:35 -0700171 /** Tag used when loading a custom fragment. */
172 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700173
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700174 /** Key to store an account in a bundle */
175 private final String BUNDLE_ACCOUNT_KEY = "account";
176 /** Key to store a folder in a bundle */
177 private final String BUNDLE_FOLDER_KEY = "folder";
178
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800179 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700180 protected Folder mFolder;
Scott Kennedyad418142013-07-10 17:18:22 -0700181 protected Folder mInbox;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700182 /** True when {@link #mFolder} is first shown to the user. */
183 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700184 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700185 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800186 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700187 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800188 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800189 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800190 protected Conversation mCurrentConversation;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800191 /**
192 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
193 */
194 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800195
Scott Kennedyf77806e2013-08-30 11:38:15 -0700196 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */
197 private final Bundle mConversationListScrollPositions = new Bundle();
198
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700199 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
200 private SuppressNotificationReceiver mNewEmailReceiver = null;
201
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800202 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700203 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700204
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800205 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800206 * The current mode of the application. All changes in mode are initiated by
207 * the activity controller. View mode changes are propagated to classes that
208 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800209 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800210 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800211 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700212 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800213 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800214
Andy Huang4e0158f2012-08-07 21:06:01 -0700215 private boolean mDestroyed;
216
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800217 /** True if running on tablet */
218 private final boolean mIsTablet;
219
Andy Huang1ee96b22012-08-24 20:19:53 -0700220 /**
221 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
222 * transactions? (including back stack manipulation)
223 * <p>
224 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
225 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
226 * and onResume.
227 */
228 private boolean mSafeToModifyFragments = true;
229
Paul Westbrook23b74b92012-02-29 11:36:12 -0800230 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700231 protected ConversationCursor mConversationListCursor;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700232 private final DataSetObservable mConversationListObservable = new Observable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700233
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800234 /** Runnable that checks the logging level to enable/disable the logging service. */
235 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700236 /** List of all accounts currently known to the controller. This is never null. */
237 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800238
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700239 private FolderWatcher mFolderWatcher;
240
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800241 /**
242 * Interface for actions that are deferred until after a load completes. This is for handling
243 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
244 * that cursor is loaded.
245 */
246 private interface LoadFinishedCallback {
247 void onLoadFinished();
248 }
249
250 /** The deferred actions to execute when mConversationListCursor load completes. */
251 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
252 new ArrayList<LoadFinishedCallback>();
253
Marc Blankbf128eb2012-04-18 15:58:45 -0700254 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700255
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700256 /** Listeners that are interested in changes to the current account. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700257 private final DataSetObservable mAccountObservers = new Observable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700258 /** Listeners that are interested in changes to the recent folders. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700259 private final DataSetObservable mRecentFolderObservers = new Observable("RecentFolder");
260 /** Listeners that are interested in changes to the list of all accounts. */
261 private final DataSetObservable mAllAccountObservers = new Observable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700262 /** Listeners that are interested in changes to the current folder. */
263 private final DataSetObservable mFolderObservable = new Observable("CurrentFolder");
Rohan Shah0f73d902013-04-19 17:06:37 -0700264 /** Listeners that are interested in changes to the drawer state. */
265 private final DataSetObservable mDrawerObservers = new Observable("Drawer");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700266
Mindy Pereira967ede62012-03-22 09:29:09 -0700267 /**
268 * Selected conversations, if any.
269 */
Andy Huang4556a442012-03-30 16:42:05 -0700270 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800271
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700272 private final int mFolderItemUpdateDelayMs;
273
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700274 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700275 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700276
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700277 /**
278 * Action menu associated with the selected set.
279 */
280 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700281 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700282 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700283
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700284 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700285 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700286 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700287 private final ConversationListLoaderCallbacks mListCursorCallbacks =
288 new ConversationListLoaderCallbacks();
289
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800290 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
291 private final FolderLoads mFolderCallbacks = new FolderLoads();
292 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
293 private final AccountLoads mAccountCallbacks = new AccountLoads();
294
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800295 /**
296 * Matched addresses that must be shielded from users because they are temporary. Even though
297 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
298 * to live past the life of an account.
299 */
300 private final VeiledAddressMatcher mVeiledMatcher;
301
Paul Westbrookb334c902012-06-25 11:42:46 -0700302 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700303
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700304 // Loader constants: Accounts
305 /**
306 * The list of accounts. This loader is started early in the application life-cycle since
307 * the list of accounts is central to all other data the application needs: unread counts for
308 * folders, critical UI settings like show/hide checkboxes, ...
309 * The loader is started when the application is created: both in
310 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
311 * destroyed since the cursor is needed through the life of the application. When the list of
312 * accounts changes, we notify {@link #mAllAccountObservers}.
313 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800314 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700315
316 /**
317 * The current account. This loader is started when we have an account. The mail application
318 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
319 * we start a loader to observe for changes on the current account.
320 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
321 * When the current account object changes, we notify {@link #mAccountObservers}.
322 * A possible performance improvement would be to listen purely on
323 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
324 * and would avoid two updates when a single setting on the current account changes.
325 */
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700326 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700327
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700328 // Loader constants: Folders
329 /** The current folder. This loader watches for updates to the current folder in a manner
330 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
331 * might be due to server-side changes (unread count), or local changes (sync window or sync
332 * status change).
333 * The change of current folder calls {@link #updateFolder(Folder)}.
334 * This is responsible for restarting a loader using the URI of the provided folder. When the
335 * loader returns, the current folder is updated and consumers, if any, are notified.
336 * When the current folder changes, we notify {@link #mFolderObservable}
337 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700338 private static final int LOADER_FOLDER_CURSOR = 2;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700339 /**
340 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
341 * folders are tied to the current account being viewed. When the account is changed,
342 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
343 * phones historically, when they were displayed in the spinner. On the tablet,
344 * they showed in the {@link FolderListFragment} and were not-populated. The code to
345 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
346 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
347 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
348 * Recent folders are needed for the life of the current account.
349 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
350 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700351 private static final int LOADER_RECENT_FOLDERS = 3;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700352 /**
353 * The primary inbox for the current account. The mechanism to load the default inbox for the
354 * current account is (sadly) different from loading other folders. The method
355 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
356 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
357 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
358 * over the current folder.
359 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
360 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700361 private static final int LOADER_ACCOUNT_INBOX = 5;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700362 /**
363 * The fake folder of search results for a term. When we search for a term,
364 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
365 * we start a loader which returns conversations that match the user-provided query.
366 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
367 * a new activity.
368 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700369 private static final int LOADER_SEARCH = 6;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700370 /**
371 * The initial folder at app start. When the application is launched from an intent that
372 * specifies the initial folder (notifications/widgets/shortcuts),
373 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
374 * shortcuts and widgets persist past application update, they might have incorrect
375 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
376 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
377 * An additional complication arises if we have to view a specific conversation within this
378 * folder. This is the case when launching the app from a single conversation notification
379 * or tapping on a specific conversation in the widget. In these cases, the conversation is
380 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
381 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800382 public static final int LOADER_FIRST_FOLDER = 8;
383
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700384 // Loader constants: Conversations
385 /** The conversation cursor over the current conversation list. This loader provides
386 * a cursor over conversation entries from a folder to display a conversation
387 * list.
388 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
389 * or when the controller is told that a folder/account change is imminent
390 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
391 * the current folder. When the user switches folders, the old loader is destroyed and a new
392 * one is created.
393 *
394 * When the conversation list changes, we notify {@link #mConversationListObservable}.
395 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700396 private static final int LOADER_CONVERSATION_LIST = 4;
397
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700398 /**
399 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
400 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
401 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
402 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
403 * other class that uses this activity's LoaderManager. If another class needs activity-level
404 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
405 * perhaps.
406 */
407 public static final int LAST_LOADER_ID = 100;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800408 /**
409 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
410 * fragments, and within an activity, loader IDs need to be unique. Currently,
411 * {@link SectionedInboxTeaserView} is the only class that uses the
412 * {@link ConversationListFragment}'s LoaderManager.
413 */
414 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800415
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700416 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700417 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700418 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700419 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700420
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700421 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
422 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700423 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700424 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700425 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700426 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700427 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700428 /** A wait fragment we added, if any. */
429 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800430 /** True if we have results from a search query */
431 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800432 /** If a confirmation dialog is being show, the listener for the positive action. */
433 private OnClickListener mDialogListener;
434 /**
435 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
436 * is used to create a new {@link #mDialogListener} on orientation changes.
437 */
438 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800439 /**
440 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
441 * and false if it acts on the currently selected conversation
442 */
443 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800444
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800445 /** Which conversation to show, if started from widget/notification. */
446 private Conversation mConversationToShow = null;
447
Andy Huangc94d07f2013-06-03 16:19:35 -0700448 /**
449 * A temporary reference to the pending destructive action that was deferred due to an
450 * auto-advance transition in progress.
451 * <p>
452 * In detail: when auto-advance triggers a mode change, we must wait until the transition
453 * completes before executing the destructive action to ensure a smooth mode change transition.
454 * This member variable houses the pending destructive action work to be run upon completion.
455 */
456 private Runnable mAutoAdvanceOp = null;
457
Andy Huangc1fb9a92013-02-11 13:09:12 -0800458 private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
459
Andy Huang12b3ee42013-04-24 22:49:43 -0700460 protected DrawerLayout mDrawerContainer;
461 protected View mDrawerPullout;
462 protected ActionBarDrawerToggle mDrawerToggle;
463 protected ListView mListViewForAnimating;
464 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700465 private boolean mConversationListLoadFinishedIgnored;
466 protected MailDrawerListener mDrawerListener;
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700467 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700468
Andy Huang144bfe72013-06-11 13:27:52 -0700469 private final DrawIdler mDrawIdler = new DrawIdler();
470
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700471 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700472
Scott Kennedycb85aea2013-02-25 13:08:32 -0800473 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
474 @Override
475 public void onChanged() {
476 super.onChanged();
477
478 if (mConversationListCursor != null) {
479 mConversationListCursor.handleNotificationActions();
480 }
481 }
482 };
483
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800484 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
485 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700486 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800487 mViewMode = viewMode;
488 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700489 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700490 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700491 // Allow the fragment to observe changes to its own selection set. No other object is
492 // aware of the selected set.
493 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700494
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800495 final Resources r = mContext.getResources();
496 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
497 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800498 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800499 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700500 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800501 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800502
503 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800504 public Account getCurrentAccount() {
505 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800506 }
507
508 @Override
509 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800510 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800511 }
512
513 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800514 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700515 final int mode = mViewMode.getMode();
516 final int helpContextResId;
517 switch (mode) {
518 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
519 helpContextResId = R.string.wait_help_context;
520 break;
521 default:
522 helpContextResId = R.string.main_help_context;
523 }
524 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800525 }
526
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800527 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700528 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700529 return mConversationListCursor;
530 }
531
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700532 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700533 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800534 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700535 * @return true if the fragment is valid, false otherwise
536 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800537 private static boolean isValidFragment(Fragment in) {
538 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700539 }
540
541 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700542 * Get the conversation list fragment for this activity. If the conversation list fragment is
543 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700544 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700545 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
546 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
547 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
548 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
549 * need the fragment immediately after adding it, consider making the fragment an observer of
550 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700551 */
552 protected ConversationListFragment getConversationListFragment() {
553 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700554 if (isValidFragment(fragment)) {
555 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700556 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700557 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700558 }
559
560 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700561 * Returns the folder list fragment attached with this activity. If no such fragment is attached
562 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700563 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700564 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
565 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
566 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
567 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
568 * need the fragment immediately after adding it, consider making the fragment an observer of
569 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700570 */
571 protected FolderListFragment getFolderListFragment() {
Tony Mantlerb29890a2013-07-30 15:12:05 -0700572 final Fragment fragment = mFragmentManager.findFragmentById(R.id.drawer_pullout);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700573 if (isValidFragment(fragment)) {
574 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700575 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700576 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700577 }
578
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800579 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800580 * Initialize the action bar. This is not visible to OnePaneController and
581 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800582 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700583 private void initializeActionBar() {
584 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700585 if (actionBar == null) {
586 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700587 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700588
589 // be sure to inherit from the ActionBar theme when inflating
590 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700591 final boolean isSearch = mActivity.getIntent() != null
592 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
593 mActionBarView = (MailActionBarView) inflater.inflate(
594 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Vikram Aggarwaldac89fa2013-03-05 16:43:09 -0800595 mActionBarView.initialize(mActivity, this, actionBar);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700596
Andy Huang12b3ee42013-04-24 22:49:43 -0700597 // init the action bar to allow the 'up' affordance.
598 // any configurations that disallow 'up' should do that later.
599 mActionBarView.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700600 }
601
602 /**
603 * Attach the action bar to the activity.
604 */
605 private void attachActionBar() {
606 final ActionBar actionBar = mActivity.getActionBar();
607 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800608 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal26b0bfd2013-03-29 13:05:08 -0700609 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700610 // Show a custom view and home icon, keep the title and subttitle
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700611 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
612 | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700613 actionBar.setDisplayOptions(mask, mask);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800614 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700615 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800616 }
617
618 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800619 * Returns whether the conversation list fragment is visible or not.
620 * Different layouts will have their own notion on the visibility of
621 * fragments, so this method needs to be overriden.
622 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800623 */
624 protected abstract boolean isConversationListVisible();
625
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700626 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700627 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700628 */
629 final void perhapsEnterWaitMode() {
630 // If the account is not initialized, then show the wait fragment, since nothing can be
631 // shown.
632 if (mAccount.isAccountInitializationRequired()) {
633 showWaitForInitialization();
634 return;
635 }
636
637 final boolean inWaitingMode = inWaitMode();
638 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
639 if (isSyncRequired) {
640 if (inWaitingMode) {
641 // Update the WaitFragment's account object
642 updateWaitMode();
643 } else {
644 // Transition to waiting mode
645 showWaitForInitialization();
646 }
647 } else if (inWaitingMode) {
648 // Dismiss waiting mode
649 hideWaitForInitialization();
650 }
651 }
652
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800653 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700654 public void switchToDefaultInboxOrChangeAccount(Account account) {
655 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700656 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700657 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700658 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700659 if (switchToDefaultInbox) {
660 loadAccountInbox();
661 return;
662 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700663 changeAccount(account);
664 }
665
666 @Override
667 public void changeAccount(Account account) {
668 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
669 // Is the account or account settings different from the existing account?
670 final boolean firstLoad = mAccount == null;
671 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
672
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800673 // If nothing has changed, return early without wasting any more time.
674 if (!accountChanged && !account.settingsDiffer(mAccount)) {
675 return;
676 }
677 // We also don't want to do anything if the new account is null
678 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700679 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800680 return;
681 }
682 final String accountName = account.name;
683 mHandler.post(new Runnable() {
684 @Override
685 public void run() {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800686 MailActivity.setNfcMessage(accountName);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700687 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800688 });
689 if (accountChanged) {
690 commitDestructiveActions(false);
691 }
Andy Huang761522c2013-08-08 13:09:11 -0700692 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE,
693 AnalyticsUtils.getAccountTypeForAccount(accountName));
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800694 // Change the account here
695 setAccount(account);
696 // And carry out associated actions.
697 cancelRefreshTask();
698 if (accountChanged) {
699 loadAccountInbox();
700 }
701 // Check if we need to force setting up an account before proceeding.
702 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
703 // Launch the intent!
704 final Intent intent = new Intent(Intent.ACTION_EDIT);
705 intent.setData(mAccount.settings.setupIntentUri);
706 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800707 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800708 }
709
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700710 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700711 * Adds a listener interested in change in the current account. If a class is storing a
712 * reference to the current account, it should listen on changes, so it can receive updates to
713 * settings. Must happen in the UI thread.
714 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800715 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700716 public void registerAccountObserver(DataSetObserver obs) {
717 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800718 }
719
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700720 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700721 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700722 * Must happen in the UI thread.
723 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700724 @Override
725 public void unregisterAccountObserver(DataSetObserver obs) {
726 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700727 }
728
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700729 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700730 public void registerAllAccountObserver(DataSetObserver observer) {
731 mAllAccountObservers.registerObserver(observer);
732 }
733
734 @Override
735 public void unregisterAllAccountObserver(DataSetObserver observer) {
736 mAllAccountObservers.unregisterObserver(observer);
737 }
738
739 @Override
740 public Account[] getAllAccounts() {
741 return mAllAccounts;
742 }
743
744 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700745 public Account getAccount() {
746 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700747 }
748
Rohan Shah0f73d902013-04-19 17:06:37 -0700749 @Override
750 public void registerDrawerClosedObserver(final DataSetObserver observer) {
751 mDrawerObservers.registerObserver(observer);
752 }
753
754 @Override
755 public void unregisterDrawerClosedObserver(final DataSetObserver observer) {
756 mDrawerObservers.unregisterObserver(observer);
757 }
758
759 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700760 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
761 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
762 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700763 */
764 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700765 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
766 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700767 if (!isDrawerEnabled()) {
768 mDrawerObservers.notifyChanged();
769 return;
770 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700771 // If there are no new folders or accounts to switch to, just close the drawer
772 if (!hasNewFolderOrAccount) {
773 mDrawerContainer.closeDrawers();
774 return;
775 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700776 // Otherwise, start preloading the conversation list for the new folder.
777 if (nextFolder != null) {
778 preloadConvList(nextAccount, nextFolder);
779 }
780 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700781 final ConversationListFragment conversationList = getConversationListFragment();
782 if (conversationList != null) {
783 mListViewForAnimating = conversationList.getListView();
784 } else {
785 // There is no conversation list to animate, so just set it to null
786 mListViewForAnimating = null;
787 }
788
789 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
790 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
791 mHasNewAccountOrFolder = true;
792 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
793 } else {
794 // Drawer is already closed, notify observers that is the case.
795 mDrawerObservers.notifyChanged();
796 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700797 }
798
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700799 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700800 * Load the conversation list early for the given folder. This happens when some UI element
801 * (usually the drawer) instructs the controller that an account change or folder change is
802 * imminent. While the UI element is animating, the controller can preload the conversation
803 * list for the default inbox of the account provided here or to the folder provided here.
804 *
805 * @param nextAccount The account which the app will switch to shortly, possibly null.
806 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700807 */
808 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
809 // Fire off the conversation list loader for this account already with a fake
810 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700811 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700812 if (nextAccount != null) {
813 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
814 } else {
815 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
816 }
817 if (nextFolder != null) {
818 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700819 } else {
820 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700821 }
822 mFolder = null;
823 final LoaderManager lm = mActivity.getLoaderManager();
824 lm.destroyLoader(LOADER_CONVERSATION_LIST);
825 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
826 }
827
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700828 /**
829 * Initiates the async request to create a fake search folder, which returns conversations that
830 * match the query term provided by the user. Returns immediately.
831 * @param intent Intent that the app was started with. This intent contains the search query.
832 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800833 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700834 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700835 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800836 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800837 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800838 }
839
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800840 @Override
Scott Kennedyad418142013-07-10 17:18:22 -0700841 public void onFolderChanged(Folder folder, final boolean force) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700842 /** If the folder doesn't exist, or its parent URI is empty,
843 * this is not a child folder */
844 final boolean isTopLevel = (folder == null) || (folder.parent == Uri.EMPTY);
845 final int mode = mViewMode.getMode();
846 mDrawerToggle.setDrawerIndicatorEnabled(
847 getShouldShowDrawerIndicator(mode, isTopLevel));
Scott Kennedy2b254dc2013-07-10 11:40:27 -0700848 mDrawerContainer.setDrawerLockMode(getShouldAllowDrawerPull(mode)
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -0700849 ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
850
Andy Huang12b3ee42013-04-24 22:49:43 -0700851 mDrawerContainer.closeDrawers();
Scott Kennedyd2a0c392013-08-29 13:21:05 -0700852
853 if (mFolder == null || !mFolder.equals(folder)) {
854 // We are actually changing the folder, so exit cab mode
855 exitCabMode();
856 }
857
Scott Kennedyad418142013-07-10 17:18:22 -0700858 changeFolder(folder, null, force);
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700859 }
860
861 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700862 * Sets the folder state without changing view mode and without creating a list fragment, if
863 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800864 * @param folder the folder whose list of conversations are to be shown
865 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700866 */
867 private void setListContext(Folder folder, String query) {
868 updateFolder(folder);
869 if (query != null) {
870 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
871 } else {
872 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
873 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700874 cancelRefreshTask();
875 }
876
877 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700878 * Changes the folder to the value provided here. This causes the view mode to change.
879 * @param folder the folder to change to
880 * @param query if non-null, this represents the search string that the folder represents.
Scott Kennedyad418142013-07-10 17:18:22 -0700881 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow
882 * changing to the current folder
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700883 */
Scott Kennedyad418142013-07-10 17:18:22 -0700884 private void changeFolder(Folder folder, String query, final boolean force) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700885 if (!Objects.equal(mFolder, folder)) {
886 commitDestructiveActions(false);
887 }
Scott Kennedyad418142013-07-10 17:18:22 -0700888 if (folder != null && (!folder.equals(mFolder) || force)
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700889 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700890 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800891 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700892 // Touch the current folder: it is different, and it has been accessed.
893 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800894 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800895 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800896 }
897
Mindy Pereira13c12a62012-05-31 15:41:08 -0700898 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700899 public void onFolderSelected(Folder folder) {
Scott Kennedyad418142013-07-10 17:18:22 -0700900 onFolderChanged(folder, false /* force */);
Mindy Pereira13c12a62012-05-31 15:41:08 -0700901 }
902
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700903 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700904 * Adds a listener interested in change in the recent folders. If a class is storing a
905 * reference to the recent folders, it should listen on changes, so it can receive updates.
906 * Must happen in the UI thread.
907 */
908 @Override
909 public void registerRecentFolderObserver(DataSetObserver obs) {
910 mRecentFolderObservers.registerObserver(obs);
911 }
912
913 /**
914 * Removes a listener from receiving recent folder changes.
915 * Must happen in the UI thread.
916 */
917 @Override
918 public void unregisterRecentFolderObserver(DataSetObserver obs) {
919 mRecentFolderObservers.unregisterObserver(obs);
920 }
921
922 @Override
923 public RecentFolderList getRecentFolders() {
924 return mRecentFolderList;
925 }
926
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700927 @Override
928 public void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700929 boolean handled = false;
930 if (mFolderWatcher != null) {
931 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
932 if (inbox != null) {
Scott Kennedyad418142013-07-10 17:18:22 -0700933 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700934 handled = true;
935 }
936 }
937 if (!handled) {
938 LogUtils.w(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
939 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
940 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700941 final int mode = mViewMode.getMode();
942 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700943 mViewMode.enterConversationListMode();
944 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700945 }
946
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700947 @Override
948 public void setFolderWatcher(FolderWatcher watcher) {
949 mFolderWatcher = watcher;
950 }
951
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700952 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700953 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
954 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
955 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800956 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700957 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800958 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700959 // We should never try to assign a null folder. But in the rare event that we do, we should
960 // only set the bit when we have a valid folder, and null is not valid.
961 if (newFolder == null) {
962 return;
963 }
964 // If the previous folder was null, or if the two folders represent different data, then we
965 // consider that the folder has changed.
Scott Kennedy259df5b2013-07-11 13:24:01 -0700966 if (mFolder == null || !newFolder.equals(mFolder)) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700967 mFolderChanged = true;
968 }
969 }
970
971 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700972 * Sets the current folder if it is different from the object provided here. This method does
973 * NOT notify the folder observers that a change has happened. Observers are notified when we
974 * get an updated folder from the loaders, which will happen as a consequence of this method
975 * (since this method starts/restarts the loaders).
976 * @param folder The folder to assign
977 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700978 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700979 if (folder == null || !folder.isInitialized()) {
980 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
981 return;
982 }
983 if (folder.equals(mFolder)) {
984 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
985 return;
986 }
987 final boolean wasNull = mFolder == null;
988 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
989 final LoaderManager lm = mActivity.getLoaderManager();
990 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
991 // ensure that the folder is different from the previous folder before marking the
992 // folder changed.
993 setHasFolderChanged(folder);
994 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700995
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700996 // We do not need to notify folder observers yet. Instead we start the loaders and
997 // when the load finishes, we will get an updated folder. Then, we notify the
998 // folderObservers in onLoadFinished.
999 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -07001000
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001001 // Only when we switch from one folder to another do we want to restart the
1002 // folder and conversation list loaders (to trigger onCreateLoader).
1003 // The first time this runs when the activity is [re-]initialized, we want to re-use the
1004 // previous loader's instance and data upon configuration change (e.g. rotation).
1005 // If there was not already an instance of the loader, init it.
1006 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001007 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001008 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001009 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001010 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001011 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
1012 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001013 // folders, we want to restart the loader to get the information
1014 // for the newly selected folder
1015 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001016 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001017 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -07001018 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07001019 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
1020 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -08001021 }
1022
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001023 @Override
Andy Huang090db1e2012-07-25 13:25:28 -07001024 public Folder getFolder() {
1025 return mFolder;
1026 }
1027
1028 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -07001029 public Folder getHierarchyFolder() {
1030 return mFolderListFolder;
1031 }
1032
1033 @Override
1034 public void setHierarchyFolder(Folder folder) {
1035 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001036 }
1037
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001038 /**
1039 * The mail activity calls other activities for two specific reasons:
1040 * <ul>
1041 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1042 * <li>To update the password on a current account. The result {@link
1043 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1044 * </ul>
1045 * @param requestCode
1046 * @param resultCode
1047 * @param data
1048 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001049 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001050 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001051 switch (requestCode) {
1052 case ADD_ACCOUNT_REQUEST_CODE:
1053 // We were waiting for the user to create an account
1054 if (resultCode == Activity.RESULT_OK) {
1055 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001056 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1057 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001058 } else {
1059 // The user failed to create an account, just exit the app
1060 mActivity.finish();
1061 }
1062 break;
1063 case REAUTHENTICATE_REQUEST_CODE:
1064 if (resultCode == Activity.RESULT_OK) {
1065 // The user successfully authenticated, attempt to refresh the list
1066 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1067 if (refreshUri != null) {
1068 startAsyncRefreshTask(refreshUri);
1069 }
1070 }
1071 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001072 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001073 }
1074
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001075 /**
1076 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001077 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001078 */
1079 protected synchronized void informCursorVisiblity(boolean visible) {
1080 if (mConversationListCursor != null) {
1081 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1082 // We have informed the cursor. Subsequent visibility changes should not tell it that
1083 // the folder has changed.
1084 mFolderChanged = false;
1085 }
1086 }
1087
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001088 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001089 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001090 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001091 commitAutoAdvanceOperation();
Scott Kennedy32ddb842013-08-28 17:38:22 -07001092
1093 // Notify special views
1094 final ConversationListFragment convListFragment = getConversationListFragment();
1095 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
1096 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible);
1097 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001098 }
1099
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001100 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001101 * Called when a conversation is visible. Child classes must call the super class implementation
1102 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001103 */
1104 @Override
1105 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001106 commitAutoAdvanceOperation();
1107 }
1108
1109 /**
1110 * Commits any pending destructive action that was earlier deferred by an auto-advance
1111 * mode-change transition.
1112 */
1113 private void commitAutoAdvanceOperation() {
1114 if (mAutoAdvanceOp != null) {
1115 mAutoAdvanceOp.run();
1116 mAutoAdvanceOp = null;
1117 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001118 }
1119
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001120 /**
1121 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1122 * to turn it on for shipped versions.
1123 */
1124 private void initializeDevLoggingService() {
1125 if (!MailLogService.DEBUG_ENABLED) {
1126 return;
1127 }
1128 // Check every 5 minutes.
1129 final int WAIT_TIME = 5 * 60 * 1000;
1130 // Start a runnable that periodically checks the log level and starts/stops the service.
1131 mLogServiceChecker = new Runnable() {
1132 /** True if currently logging. */
1133 private boolean mCurrentlyLogging = false;
1134
1135 /**
1136 * If the logging level has been changed since the previous run, start or stop the
1137 * service.
1138 */
1139 private void startOrStopService() {
1140 // If the log level is already high, start the service.
1141 final Intent i = new Intent(mContext, MailLogService.class);
1142 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1143 if (mCurrentlyLogging == loggingEnabled) {
1144 // No change since previous run, just return;
1145 return;
1146 }
1147 if (loggingEnabled) {
1148 LogUtils.e(LOG_TAG, "Starting MailLogService");
1149 mContext.startService(i);
1150 } else {
1151 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1152 mContext.stopService(i);
1153 }
1154 mCurrentlyLogging = loggingEnabled;
1155 }
1156
1157 @Override
1158 public void run() {
1159 startOrStopService();
1160 mHandler.postDelayed(this, WAIT_TIME);
1161 }
1162 };
1163 // Start the runnable right away.
1164 mHandler.post(mLogServiceChecker);
1165 }
1166
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001167 /**
1168 * The application can be started from the following entry points:
1169 * <ul>
1170 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1171 * as “Starting the app”.</li>
1172 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1173 * <li>Widget: Shows the contents of a synced label, and allows:
1174 * <ul>
1175 * <li>Viewing the list (tapping on the title)</li>
1176 * <li>Composing a new message (tapping on the new message icon in the title. This
1177 * launches the {@link ComposeActivity}.
1178 * </li>
1179 * <li>Viewing a single message (tapping on a list element)</li>
1180 * </ul>
1181 *
1182 * </li>
1183 * <li>Tapping on a notification:
1184 * <ul>
1185 * <li>Shows message list if more than one message</li>
1186 * <li>Shows the conversation if the notification is for a single message</li>
1187 * </ul>
1188 * </li>
1189 * <li>...and most importantly, the activity life cycle can tear down the application and
1190 * restart it:
1191 * <ul>
1192 * <li>Rotate the application: it is destroyed and recreated.</li>
1193 * <li>Navigate away, and return from recent applications.</li>
1194 * </ul>
1195 * </li>
1196 * <li>Add a new account: fires off an intent to add an account,
1197 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1198 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1199 * <li>Composing can happen from many entry points: third party applications fire off an
1200 * intent to compose email, and launch directly into the {@link ComposeActivity}
1201 * .</li>
1202 * </ul>
1203 * {@inheritDoc}
1204 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001205 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001206 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001207 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001208 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001209 // Allow shortcut keys to function for the ActionBar and menus.
1210 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001211 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001212 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001213 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001214 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001215
Tony Mantlerc6691fe2013-07-15 15:46:12 -07001216 // the "open drawer description" argument is for when the drawer is open
1217 // so tell the user that interacting will cause the drawer to close
1218 // and vice versa for the "close drawer description" argument
Andy Huang12b3ee42013-04-24 22:49:43 -07001219 mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer,
Tony Mantlerc6691fe2013-07-15 15:46:12 -07001220 R.drawable.ic_drawer, R.string.drawer_close, R.string.drawer_open);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07001221 mDrawerListener = new MailDrawerListener();
1222 mDrawerContainer.setDrawerListener(mDrawerListener);
Andy Huang12b3ee42013-04-24 22:49:43 -07001223 mDrawerContainer.setDrawerShadow(
1224 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
1225
1226 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1227
Mindy Pereira161f50d2012-02-28 15:47:19 -08001228 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001229 // simplifies the amount of logic in the AbstractActivityController, but increases the
1230 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001231 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001232 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07001233 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001234 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -07001235 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -07001236
Andy Huang144bfe72013-06-11 13:27:52 -07001237 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1238
Andy Huang632721e2012-04-11 16:57:26 -07001239 final Intent intent = mActivity.getIntent();
Andy Huang4fe0af82013-08-20 17:24:51 -07001240
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001241 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001242 // that does not rely on restored fragments or loader data
1243 // any state restoration that relies on those can be done later in
1244 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1245 if (savedState != null) {
1246 if (savedState.containsKey(SAVED_ACCOUNT)) {
1247 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001248 }
1249 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001250 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001251 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001252 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001253 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001254 if (savedState.containsKey(SAVED_ACTION)) {
1255 mDialogAction = savedState.getInt(SAVED_ACTION);
1256 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001257 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001258 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001259 } else if (intent != null) {
1260 handleIntent(intent);
1261 }
Andy Huang632721e2012-04-11 16:57:26 -07001262 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001263 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1264 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001265 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001266 }
1267
1268 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001269 public void onPostCreate(Bundle savedState) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001270 // Sync the toggle state after onRestoreInstanceState has occurred.
1271 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001272
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001273 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001274 }
1275
1276 @Override
1277 public void onConfigurationChanged(Configuration newConfig) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001278 mDrawerToggle.onConfigurationChanged(newConfig);
1279 }
1280
1281 /**
1282 * If drawer is open/visible (even partially), close it.
1283 */
1284 protected void closeDrawerIfOpen() {
1285 if (!isDrawerEnabled()) {
1286 return;
1287 }
1288 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1289 mDrawerContainer.closeDrawers();
1290 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001291 }
1292
1293 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001294 public void onStart() {
1295 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001296
1297 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang761522c2013-08-08 13:09:11 -07001298
1299 if (mViewMode.getMode() != ViewMode.UNKNOWN) {
1300 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString());
1301 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001302 }
1303
1304 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001305 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001306 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001307 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1308 if (fragment != null) {
1309 fragment.dismiss();
1310 }
mindypea04f932012-08-27 14:17:59 -07001311 // When the user places the app in the background by pressing "home",
1312 // dismiss the toast bar. However, since there is no way to determine if
1313 // home was pressed, just dismiss any existing toast bar when restarting
1314 // the app.
1315 if (mToastBar != null) {
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07001316 mToastBar.hide(false, false /* actionClicked */);
mindypea04f932012-08-27 14:17:59 -07001317 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001318 }
1319
1320 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001321 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001322 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001323 }
1324
1325 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001326 public final boolean onCreateOptionsMenu(Menu menu) {
Andrew Sapperstein0fa9efd2013-08-07 14:09:31 -07001327 if (mViewMode.isAdMode()) {
1328 return false;
1329 }
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001330 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001331 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -08001332 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001333 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001334 }
1335
1336 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001337 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001338 return false;
1339 }
1340
mindyp17a8e782012-11-29 14:56:17 -08001341 public abstract boolean doesActionChangeConversationListVisibility(int action);
1342
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001343 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001344 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang761522c2013-08-08 13:09:11 -07001345
Andy Huang12b3ee42013-04-24 22:49:43 -07001346 /*
1347 * The action bar home/up action should open or close the drawer.
1348 * mDrawerToggle will take care of this.
1349 */
1350 if (mDrawerToggle.onOptionsItemSelected(item)) {
Andy Huang042a5302013-08-13 12:39:08 -07001351 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle",
1352 null, 0);
Andy Huang12b3ee42013-04-24 22:49:43 -07001353 return true;
1354 }
1355
Andy Huang2b555492013-08-14 21:06:21 -07001356 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM,
1357 item.getItemId(), "action_bar", 0);
Andy Huang042a5302013-08-13 12:39:08 -07001358
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001359 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001360 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001361 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001362 /** This is NOT a batch action. */
1363 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001364 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001365 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001366 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001367 // doing before. Don't animate if we are launching a new screen.
1368 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001369 if (id == R.id.archive) {
1370 final boolean showDialog = (settings != null && settings.confirmArchive);
1371 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation);
1372 } else if (id == R.id.remove_folder) {
1373 delete(R.id.remove_folder, target,
1374 getDeferredRemoveFolder(target, mFolder, true, isBatch, true), isBatch);
1375 } else if (id == R.id.delete) {
1376 final boolean showDialog = (settings != null && settings.confirmDelete);
1377 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation);
1378 } else if (id == R.id.discard_drafts) {
Andy Huang121c8b82013-08-05 11:52:55 -07001379 // drafts are lost forever, so always confirm
1380 confirmAndDelete(id, target, true /* showDialog */,
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001381 R.plurals.confirm_discard_drafts_conversation);
1382 } else if (id == R.id.mark_important) {
1383 updateConversation(Conversation.listOf(mCurrentConversation),
1384 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
1385 } else if (id == R.id.mark_not_important) {
1386 if (mFolder != null && mFolder.isImportantOnly()) {
1387 delete(R.id.mark_not_important, target,
1388 getDeferredAction(R.id.mark_not_important, target, isBatch), isBatch);
1389 } else {
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001390 updateConversation(Conversation.listOf(mCurrentConversation),
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001391 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1392 }
1393 } else if (id == R.id.mute) {
1394 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch), isBatch);
1395 } else if (id == R.id.report_spam) {
1396 delete(R.id.report_spam, target,
1397 getDeferredAction(R.id.report_spam, target, isBatch), isBatch);
1398 } else if (id == R.id.mark_not_spam) {
1399 // Currently, since spam messages are only shown in list with
1400 // other spam messages,
1401 // marking a message not as spam is a destructive action
1402 delete(R.id.mark_not_spam, target,
1403 getDeferredAction(R.id.mark_not_spam, target, isBatch), isBatch);
1404 } else if (id == R.id.report_phishing) {
1405 delete(R.id.report_phishing, target,
1406 getDeferredAction(R.id.report_phishing, target, isBatch), isBatch);
1407 } else if (id == android.R.id.home) {
1408 onUpPressed();
1409 } else if (id == R.id.compose) {
1410 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1411 } else if (id == R.id.refresh) {
1412 requestFolderRefresh();
1413 } else if (id == R.id.settings) {
1414 Utils.showSettings(mActivity.getActivityContext(), mAccount);
1415 } else if (id == R.id.folder_options) {
1416 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
1417 } else if (id == R.id.help_info_menu_item) {
1418 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
1419 } else if (id == R.id.feedback_menu_item) {
1420 Utils.sendFeedback(mActivity, mAccount, false);
1421 } else if (id == R.id.manage_folders_item) {
1422 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07001423 } else if (id == R.id.move_to || id == R.id.change_folders) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001424 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
1425 mActivity.getActivityContext(), mAccount, this,
1426 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
1427 id == R.id.move_to);
1428 if (dialog != null) {
1429 dialog.show();
1430 }
1431 } else if (id == R.id.move_to_inbox) {
1432 new AsyncTask<Void, Void, Folder>() {
1433 @Override
1434 protected Folder doInBackground(final Void... params) {
1435 // Get the "move to" inbox
1436 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1437 true /* allowHidden */);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001438 }
Scott Kennedydd2ec682013-06-03 19:16:13 -07001439
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001440 @Override
1441 protected void onPostExecute(final Folder moveToInbox) {
1442 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1443 // Add inbox
1444 ops.add(new FolderOperation(moveToInbox, true));
1445 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1446 true /* showUndo */, false /* isMoveTo */);
1447 }
1448 }.execute((Void[]) null);
1449 } else if (id == R.id.empty_trash) {
1450 showEmptyDialog();
1451 } else if (id == R.id.empty_spam) {
1452 showEmptyDialog();
1453 } else {
1454 handled = false;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001455 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001456 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001457 }
1458
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001459 /**
1460 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1461 */
1462 private void showEmptyDialog() {
1463 if (mFolder != null) {
1464 final EmptyFolderDialogFragment fragment =
1465 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1466 fragment.setListener(this);
1467 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1468 }
1469 }
1470
1471 @Override
1472 public void onFolderEmptied() {
1473 emptyFolder();
1474 }
1475
1476 /**
1477 * Performs the work of emptying the currently visible folder.
1478 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001479 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001480 if (mConversationListCursor != null) {
1481 mConversationListCursor.emptyFolder();
1482 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001483 }
1484
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001485 private void attachEmptyFolderDialogFragmentListener() {
1486 final EmptyFolderDialogFragment fragment =
1487 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1488 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1489
1490 if (fragment != null) {
1491 fragment.setListener(this);
1492 }
1493 }
1494
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001495 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001496 * Toggles the drawer pullout. If it was open (Fully extended), the
1497 * drawer will be closed. Otherwise, the drawer will be opened. This should
1498 * only be called when used with a toggle item. Other cases should be handled
1499 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001500 */
Tony Mantlerd9eaca82013-08-05 11:42:29 -07001501 protected void toggleDrawerState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001502 if (!isDrawerEnabled()) {
1503 return;
1504 }
1505 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1506 mDrawerContainer.closeDrawers();
1507 } else {
1508 mDrawerContainer.openDrawer(mDrawerPullout);
1509 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001510 }
1511
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001512 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001513 public final boolean onUpPressed() {
1514 for (UpOrBackHandler h : mUpOrBackHandlers) {
1515 if (h.onUpPressed()) {
1516 return true;
1517 }
1518 }
1519 return handleUpPress();
1520 }
1521
1522 @Override
1523 public final boolean onBackPressed() {
1524 for (UpOrBackHandler h : mUpOrBackHandlers) {
1525 if (h.onBackPressed()) {
1526 return true;
1527 }
1528 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001529
1530 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1531 mDrawerContainer.closeDrawers();
1532 return true;
1533 }
1534
Andy Huangc1fb9a92013-02-11 13:09:12 -08001535 return handleBackPress();
1536 }
1537
1538 protected abstract boolean handleBackPress();
1539 protected abstract boolean handleUpPress();
1540
1541 @Override
1542 public void addUpOrBackHandler(UpOrBackHandler handler) {
1543 if (mUpOrBackHandlers.contains(handler)) {
1544 return;
1545 }
1546 mUpOrBackHandlers.addFirst(handler);
1547 }
1548
1549 @Override
1550 public void removeUpOrBackHandler(UpOrBackHandler handler) {
1551 mUpOrBackHandlers.remove(handler);
1552 }
1553
1554 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001555 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001556 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001557 refreshConversationList();
1558 }
1559
1560 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001561 public void updateConversation(Collection <Conversation> target, String columnName,
1562 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001563 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001564 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001565 }
1566
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001567 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001568 public void updateConversation(Collection <Conversation> target, String columnName,
1569 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001570 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001571 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001572 }
1573
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001574 @Override
1575 public void updateConversation(Collection <Conversation> target, String columnName,
1576 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001577 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001578 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001579 }
1580
Andy Huang839ada22012-07-20 15:48:40 -07001581 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001582 public void markConversationMessagesUnread(final Conversation conv,
1583 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001584 // The only caller of this method is the conversation view, from where marking unread should
1585 // *always* take you back to list mode.
1586 showConversation(null);
1587
Andy Huang839ada22012-07-20 15:48:40 -07001588 // locally mark conversation unread (the provider is supposed to propagate message unread
1589 // to conversation unread)
1590 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001591 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001592 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001593
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001594 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1595 @Override
1596 public void onLoadFinished() {
1597 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1598 originalConversationInfo);
1599 }
1600 });
1601 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001602 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001603 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1604 }
1605 }
1606
1607 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1608 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001609 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001610 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001611 final int numMessages = conv.getNumMessages();
1612 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1613 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001614
Andy Huang9e4ca792013-02-28 14:33:43 -08001615 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001616 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001617 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001618 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001619 // Conversations are neither marked read, nor viewed, and we don't want to show
1620 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001621 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001622 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001623 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001624 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1625 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1626 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1627 info);
1628 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001629 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001630
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001631 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001632 if (originalConversationInfo != null) {
1633 mConversationListCursor.setConversationColumn(conv.uri,
1634 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1635 }
Andy Huang839ada22012-07-20 15:48:40 -07001636
1637 // applyBatch with each CPO as an UPDATE op on each affected message uri
1638 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1639 String authority = null;
1640 for (Uri messageUri : unreadMessageUris) {
1641 if (authority == null) {
1642 authority = messageUri.getAuthority();
1643 }
1644 ops.add(ContentProviderOperation.newUpdate(messageUri)
1645 .withValue(UIProvider.MessageColumns.READ, 0)
1646 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001647 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001648 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001649 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001650 new ContentProviderTask() {
1651 @Override
1652 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001653 if (result.exception != null) {
1654 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1655 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001656 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1657 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001658 }
Andy Huang839ada22012-07-20 15:48:40 -07001659 }
1660 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001661 }
Andy Huang839ada22012-07-20 15:48:40 -07001662 }
1663
1664 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001665 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1666 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001667 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1668
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001669 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001670 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1671 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1672 targets.toArray());
1673 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001674 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1675 @Override
1676 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001677 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001678 }
1679 });
1680 } else {
1681 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001682 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001683 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001684 }
1685
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001686 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001687 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001688 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001689 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001690 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001691 final Runnable operation = new Runnable() {
1692 @Override
1693 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001694 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001695 }
1696 };
1697
Scott Kennedycaaeed32013-06-12 13:39:16 -07001698 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001699 // This method will be called again if the user selects an autoadvance option
1700 return;
1701 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001702 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001703
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001704 final int size = targets.size();
1705 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1706 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001707 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001708 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001709
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001710 // We never want to mark unseen here, but we do want to mark it seen
1711 if (read || markViewed) {
1712 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1713 }
1714
Paul Westbrook5109c512012-11-05 11:00:30 -08001715 // The mark read/unread/viewed operations do not show an undo bar
1716 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001717 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001718 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001719 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001720 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001721 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001722 boolean changed = info.markRead(read);
1723 if (changed) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001724 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
mindyp7f55c682012-10-04 11:38:27 -07001725 }
Andy Huang839ada22012-07-20 15:48:40 -07001726 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001727 opList.add(mConversationListCursor.getOperationForConversation(
1728 target, ConversationOperation.UPDATE, value));
1729 // Update the local conversation objects so they immediately change state.
1730 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001731 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001732 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001733 }
Andy Huang839ada22012-07-20 15:48:40 -07001734 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001735 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001736 }
1737
Andy Huang8f6b0062012-07-31 15:36:31 -07001738 /**
1739 * Auto-advance to a different conversation if the currently visible conversation in
1740 * conversation mode is affected (deleted, marked unread, etc.).
1741 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001742 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001743 *
1744 * @param target the set of conversations being deleted/marked unread
1745 */
mindyp9365a822012-09-12 09:09:09 -07001746 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001747 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001748 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001749 }
1750
1751 /**
1752 * Auto-advance to a different conversation if the currently visible conversation in
1753 * conversation mode is affected (deleted, marked unread, etc.).
1754 *
1755 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001756 * <p>
1757 * Clients may pass an operation to execute on the target that this method will run after
1758 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1759 * later, or not at all. Reasons it may run later include:
1760 * <ul>
1761 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1762 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1763 * mode change transition to finish</li>
1764 * </ul>
1765 * <p>If the current conversation is not in the target collection, this method will do nothing,
1766 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001767 *
1768 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001769 * @param operation (optional) the operation to execute after advancing
1770 * @return <code>false</code> if this method handled or will execute the operation,
1771 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001772 */
1773 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001774 final Runnable operation) {
Scott Kennedy8fb8e262012-11-28 15:48:03 -08001775 final int viewMode = mViewMode.getMode();
1776 final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1777 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
Andy Huang8f6b0062012-07-31 15:36:31 -07001778 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001779
Andy Huang8f6b0062012-07-31 15:36:31 -07001780 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001781 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1782
Scott Kennedycaaeed32013-06-12 13:39:16 -07001783 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001784 displayAutoAdvanceDialogAndPerformAction(operation);
1785 return false;
1786 } else {
1787 // If we don't have one set, but we're here, just take the default
Vikram Aggarwal82d37502013-01-10 16:18:49 -08001788 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1789 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001790
1791 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1792 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001793 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1794 // transition doesn't run (i.e. it "completes" immediately).
1795 mAutoAdvanceOp = operation;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001796 showConversation(next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001797 return (mAutoAdvanceOp == null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001798 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001799 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001800
1801 return true;
1802 }
1803
1804 /**
1805 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1806 * stored, and the specified operation is run.
1807 */
1808 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1809 final String[] autoAdvanceDisplayOptions =
1810 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1811 final String[] autoAdvanceOptionValues =
1812 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1813
1814 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1815 int initialIndex = 0;
1816 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1817 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1818 initialIndex = i;
1819 break;
1820 }
1821 }
1822
1823 final DialogInterface.OnClickListener listClickListener =
1824 new DialogInterface.OnClickListener() {
1825 @Override
1826 public void onClick(DialogInterface dialog, int whichItem) {
1827 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1828 final int autoAdvanceValueInt =
1829 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1830 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1831
1832 // Save the user's setting
1833 final ContentValues values = new ContentValues(1);
1834 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1835
1836 final ContentResolver resolver = mContext.getContentResolver();
1837 resolver.update(mAccount.updateSettingsUri, values, null, null);
1838
1839 // Dismiss the dialog, as clicking the items in the list doesn't close the
1840 // dialog.
1841 dialog.dismiss();
1842 if (operation != null) {
1843 operation.run();
1844 }
1845 }
1846 };
1847
1848 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1849 R.string.auto_advance_help_title)
1850 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1851 .setPositiveButton(null, null)
1852 .create()
1853 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001854 }
1855
Andy Huang839ada22012-07-20 15:48:40 -07001856 @Override
1857 public void starMessage(ConversationMessage msg, boolean starred) {
1858 if (msg.starred == starred) {
1859 return;
1860 }
1861
1862 msg.starred = starred;
1863
1864 // locally propagate the change to the owning conversation
1865 // (figure the provider will properly propagate the change when it commits it)
1866 //
1867 // when unstarring, only propagate the change if this was the only message starred
1868 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001869 final Conversation conv = msg.getConversation();
1870 if (conversationStarred != conv.starred) {
1871 conv.starred = conversationStarred;
1872 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001873 ConversationColumns.STARRED, conversationStarred);
1874 }
1875
1876 final ContentValues values = new ContentValues(1);
1877 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1878
1879 new ContentProviderTask.UpdateTask() {
1880 @Override
1881 protected void onPostExecute(Result result) {
1882 // TODO: handle errors?
1883 }
1884 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1885 }
1886
Andy Huang12b3ee42013-04-24 22:49:43 -07001887 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001888 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001889 if (mFolder == null) {
1890 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001891 }
Alice Yang37dda442013-03-26 22:48:53 -07001892 final ConversationListFragment convList = getConversationListFragment();
1893 if (convList == null) {
1894 // This could happen if this account is in initial sync (user
1895 // is seeing the "your mail will appear shortly" message)
1896 return;
1897 }
1898 convList.showSyncStatusBar();
1899
1900 if (mAsyncRefreshTask != null) {
1901 mAsyncRefreshTask.cancel(true);
1902 }
1903 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
1904 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001905 }
1906
Mindy Pereirafbe40192012-03-20 10:40:45 -07001907 /**
1908 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1909 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001910 * @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 -07001911 * @param target the conversations to act upon
1912 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1913 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07001914 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001915 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
1916 boolean showDialog, int confirmResource) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001917 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001918 if (showDialog) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001919 makeDialogListener(actionId, isBatch);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001920 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1921 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001922 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
1923 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001924 } else {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001925 delete(0, target, getDeferredAction(actionId, target, isBatch), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001926 }
1927 }
1928
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001929 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001930 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001931 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07001932 // Order of events is critical! The Conversation View Fragment must be
1933 // notified of the next conversation with showConversation(next) *before* the
1934 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001935 // fragment has a chance to delete the conversation, animating it away.
1936
mindyp84f7d322012-10-01 17:14:40 -07001937 // Update the conversation fragment if the current conversation is
1938 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001939 final Runnable operation = new Runnable() {
1940 @Override
1941 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001942 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001943 }
1944 };
1945
Scott Kennedycaaeed32013-06-12 13:39:16 -07001946 if (!showNextConversation(target, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001947 // This method will be called again if the user selects an autoadvance option
1948 return;
1949 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001950 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001951 // Batch selections are cleared in the end of the action, so not done for batch actions.
1952 if (!isBatch) {
1953 for (final Conversation conv : target) {
1954 if (mSelectedSet.contains(conv)) {
Vikram Aggarwal97ae7842013-04-22 16:29:12 -07001955 mSelectedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001956 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001957 }
1958 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001959 // The conversation list deletes and performs the action if it exists.
1960 final ConversationListFragment convListFragment = getConversationListFragment();
1961 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07001962 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001963 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001964 return;
1965 }
mindyp84f7d322012-10-01 17:14:40 -07001966 // No visible UI element handled it on our behalf. Perform the action
1967 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07001968 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07001969 action.performAction();
1970 }
1971
1972 /**
1973 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001974 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07001975 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001976 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07001977 action.performAction();
1978 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001979 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001980
1981 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001982 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1983 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001984 }
1985
1986 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001987 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001988 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001989 }
1990
Mindy Pereira68f2e222012-03-07 10:36:54 -08001991 @Override
1992 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07001993 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001994 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001995 }
1996
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001997 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001998 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001999 // Register the receiver that will prevent the status receiver from
2000 // displaying its notification icon as long as we're running.
2001 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
2002 // that the notification was received for.
2003 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07002004
2005 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07002006
2007 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07002008
2009 // Invalidating the options menu so that when we make changes in settings,
2010 // the changes will always be updated in the action bar/options menu/
2011 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002012 }
2013
2014 @Override
2015 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002016 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002017 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002018 outState.putParcelable(SAVED_ACCOUNT, mAccount);
2019 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07002020 if (mFolder != null) {
2021 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08002022 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07002023 // If this is a search activity, let's store the search query term as well.
2024 if (ConversationListContext.isSearchResult(mConvListContext)) {
2025 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2026 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002027 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002028 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2029 }
Andy Huang4556a442012-03-30 16:42:05 -07002030 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002031 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002032 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002033 if (mToastBar.getVisibility() == View.VISIBLE) {
2034 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2035 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002036 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002037 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002038 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002039 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002040 // 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 -08002041 if (mDialogAction != -1) {
2042 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002043 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002044 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002045 if (mDetachedConvUri != null) {
2046 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2047 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002048
Scott Kennedyb10212e2013-02-22 16:27:00 -08002049 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002050 mSafeToModifyFragments = false;
Scott Kennedyad418142013-07-10 17:18:22 -07002051
2052 outState.putParcelable(SAVED_INBOX_KEY, mInbox);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002053
2054 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS,
2055 mConversationListScrollPositions);
Andy Huang1ee96b22012-08-24 20:19:53 -07002056 }
2057
2058 /**
2059 * @see #mSafeToModifyFragments
2060 */
2061 protected boolean safeToModifyFragments() {
2062 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002063 }
2064
2065 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002066 public void executeSearch(String query) {
Mindy Pereira68f2e222012-03-07 10:36:54 -08002067 Intent intent = new Intent();
2068 intent.setAction(Intent.ACTION_SEARCH);
2069 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2070 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2071 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07002072 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08002073 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002074 }
2075
2076 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002077 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002078 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002079 }
2080
Andy Huang632721e2012-04-11 16:57:26 -07002081 @Override
2082 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002083 // stop listening to the cursor on e.g. configuration changes
2084 if (mConversationListCursor != null) {
2085 mConversationListCursor.removeListener(this);
2086 }
Andy Huang144bfe72013-06-11 13:27:52 -07002087 mDrawIdler.setListener(null);
2088 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002089 // unregister the ViewPager's observer on the conversation cursor
2090 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07002091 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002092 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002093 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002094 mHandler.removeCallbacks(mLogServiceChecker);
2095 mLogServiceChecker = null;
Andy Huang632721e2012-04-11 16:57:26 -07002096 }
2097
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002098 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002099 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2100 * or not. The individual controller is responsible for changing the icon based on the mode.
2101 */
2102 protected abstract void resetActionBarIcon();
2103
2104 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002105 * {@inheritDoc} Subclasses must override this to listen to mode changes
2106 * from the ViewMode. Subclasses <b>must</b> call the parent's
2107 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002108 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002109 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002110 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002111 // When we step away from the conversation mode, we don't have a current conversation
2112 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2113 if (!ViewMode.isConversationMode(newMode)) {
2114 setCurrentConversation(null);
2115 }
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07002116
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002117 // If the viewmode is not set, preserve existing icon.
2118 if (newMode != ViewMode.UNKNOWN) {
2119 resetActionBarIcon();
2120 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002121
2122 if (isDrawerEnabled()) {
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002123 /** If the folder doesn't exist, or its parent URI is empty,
2124 * this is not a child folder */
2125 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
2126 mDrawerToggle.setDrawerIndicatorEnabled(
2127 getShouldShowDrawerIndicator(newMode, isTopLevel));
Scott Kennedy2b254dc2013-07-10 11:40:27 -07002128 mDrawerContainer.setDrawerLockMode(getShouldAllowDrawerPull(newMode)
Scott Kennedy8a72b852013-05-02 14:18:50 -07002129 ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002130 closeDrawerIfOpen();
2131 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002132 }
2133
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002134 /**
2135 * Returns true if the drawer icon is shown
2136 * @param viewMode the current view mode
2137 * @param isTopLevel true if the current folder is not a child
2138 * @return whether the drawer indicator is shown
2139 */
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002140 private boolean getShouldShowDrawerIndicator(final int viewMode,
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002141 final boolean isTopLevel) {
2142 // If search list/conv mode: disable indicator
2143 // Indicator is enabled either in conversation list or folder list mode.
Scott Kennedy81b5fa02013-07-11 14:39:18 -07002144 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode)
Scott Kennedyaded5782013-07-16 14:21:53 -07002145 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel);
Scott Kennedy8a72b852013-05-02 14:18:50 -07002146 }
2147
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002148 /**
2149 * Returns true if the left-screen swipe action (or Home icon tap) should pull a drawer out.
2150 * @param viewMode the current view mode.
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002151 * @return whether the drawer can be opened using a swipe action or action bar tap.
2152 */
Scott Kennedy2b254dc2013-07-10 11:40:27 -07002153 private static boolean getShouldAllowDrawerPull(final int viewMode) {
Scott Kennedy8a72b852013-05-02 14:18:50 -07002154 // if search list/conv mode, disable drawer pull
2155 // allow drawer pull everywhere except conversation mode where the list is hidden
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002156 return !ViewMode.isSearchMode(viewMode) && !ViewMode.isConversationMode(viewMode) &&
2157 !ViewMode.isAdMode(viewMode);
Vikram Aggarwaldfbc6982013-07-03 14:39:31 -07002158
2159 // TODO(ath): get this to work to allow drawer pull in 2-pane conv mode.
2160 /* && !isConversationListVisible() */
Scott Kennedy8a72b852013-05-02 14:18:50 -07002161 }
2162
Andy Huang3825f3d2012-08-29 16:44:12 -07002163 public void disablePagerUpdates() {
2164 mPagerController.stopListening();
2165 }
2166
Andy Huang4e0158f2012-08-07 21:06:01 -07002167 public boolean isDestroyed() {
2168 return mDestroyed;
2169 }
2170
mindyp54f120f2012-08-28 13:10:33 -07002171 @Override
2172 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002173 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002174 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002175 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002176 }
2177 }
2178
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002179 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002180 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002181 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002182 // hasFocus already ensures that the window is in focus, so we don't need to call
2183 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002184 if (hasFocus && convList != null && convList.isVisible()) {
2185 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002186 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002187 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002188 }
2189
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002190 /**
2191 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002192 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002193 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002194 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002195 if (account == null) {
2196 LogUtils.w(LOG_TAG, new Error(),
2197 "AAC ignoring null (presumably invalid) account restoration");
2198 return;
2199 }
Andy Huangb1148412012-05-19 00:16:30 -07002200 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002201 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002202 // Only change AAC state here. Do *not* modify any other object's state. The object
2203 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002204 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002205 mActivity.invalidateOptionsMenu();
2206 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002207 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002208 // The Mail instance can be null during test runs.
2209 final MailAppProvider instance = MailAppProvider.getInstance();
2210 if (instance != null) {
2211 instance.setLastViewedAccount(mAccount.uri.toString());
2212 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002213 if (account.settings == null) {
2214 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2215 return;
2216 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002217 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002218 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002219 }
2220
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002221 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002222 * Restore the state from the previous bundle. Subclasses should call this
2223 * method from the parent class, since it performs important UI
2224 * initialization.
2225 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002226 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002227 */
Andy Huang632721e2012-04-11 16:57:26 -07002228 @Override
2229 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002230 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002231 if (savedState.containsKey(SAVED_CONVERSATION)) {
2232 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002233 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002234 if (conversation != null && conversation.position < 0) {
2235 // Set the position to 0 on this conversation, as we don't know where it is
2236 // in the list
2237 conversation.position = 0;
2238 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002239 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002240 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002241
Mindy Pereirad33674992012-06-25 16:26:30 -07002242 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002243 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002244 if (op != null) {
2245 if (op.getType() == ToastBarOperation.UNDO) {
2246 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002247 } else if (op.getType() == ToastBarOperation.ERROR) {
2248 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002249 }
2250 }
2251 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002252 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002253 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002254 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002255 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002256 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002257 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002258 * Restore the state of selected conversations. This needs to be done after the correct mode
2259 * is set and the action bar is fully initialized. If not, several key pieces of state
2260 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002261 */
Andy Huang4556a442012-03-30 16:42:05 -07002262 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002263 // Order is important!!!
2264 // The dialog listener needs to happen *after* the selected set is restored.
2265
2266 // If there has been an orientation change, and we need to recreate the listener for the
2267 // confirm dialog fragment (delete/archive/...), then do it here.
2268 if (mDialogAction != -1) {
2269 makeDialogListener(mDialogAction, mDialogFromSelectedSet);
2270 }
Scott Kennedyad418142013-07-10 17:18:22 -07002271
2272 mInbox = savedState.getParcelable(SAVED_INBOX_KEY);
Scott Kennedyf77806e2013-08-30 11:38:15 -07002273
2274 mConversationListScrollPositions.clear();
2275 mConversationListScrollPositions.putAll(
2276 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS));
Andy Huang632721e2012-04-11 16:57:26 -07002277 }
2278
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002279 /**
2280 * Handle an intent to open the app. This method is called only when there is no saved state,
2281 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2282 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002283 *
2284 * This method is called for a subset of the reasons mentioned in
2285 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2286 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002287 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002288 */
Andy Huang632721e2012-04-11 16:57:26 -07002289 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002290 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002291 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2292 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002293 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002294 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002295 if (mAccount == null) {
2296 return;
Andy Huang632721e2012-04-11 16:57:26 -07002297 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002298 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Andy Huang4fe0af82013-08-20 17:24:51 -07002299
2300 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
2301 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE,
2302 AnalyticsUtils.getAccountTypeForAccount(mAccount.name));
2303 Analytics.getInstance().sendEvent("notification_click",
2304 isConversationMode ? "conversation" : "conversation_list", null, 0);
2305 }
2306
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002307 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002308 mViewMode.enterConversationMode();
2309 } else {
2310 mViewMode.enterConversationListMode();
2311 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002312 // Put the folder and conversation, and ask the loader to create this folder.
2313 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002314
2315 final Uri folderUri;
2316 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
2317 folderUri = (Uri) intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
2318 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2319 final Folder folder =
2320 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
Scott Kennedy259df5b2013-07-11 13:24:01 -07002321 folderUri = folder.folderUri.fullUri;
Scott Kennedy48cfe462013-04-10 11:32:02 -07002322 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002323 final Bundle extras = intent.getExtras();
2324 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2325 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002326 folderUri = mAccount.settings.defaultInbox;
2327 }
2328
Scott Kennedy60593352013-03-13 13:45:30 -07002329 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002330 args.putParcelable(Utils.EXTRA_CONVERSATION,
2331 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2332 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002333 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2334 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002335 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07002336 // Save this search query for future suggestions.
2337 final String query = intent.getStringExtra(SearchManager.QUERY);
2338 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002339 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07002340 mContext, authority, SuggestionsProvider.MODE);
2341 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002342 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2343 fetchSearchFolder(intent);
2344 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002345 mViewMode.enterSearchResultsConversationMode();
2346 } else {
2347 mViewMode.enterSearchResultsListMode();
2348 }
Andy Huang632721e2012-04-11 16:57:26 -07002349 } else {
2350 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2351 mActivity.finish();
2352 }
2353 }
2354 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002355 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002356 }
2357 }
2358
Andy Huang4556a442012-03-30 16:42:05 -07002359 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002360 * Returns true if we should enter conversation mode with search.
2361 */
2362 protected final boolean shouldEnterSearchConvMode() {
2363 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2364 }
2365
2366 /**
Andy Huang4556a442012-03-30 16:42:05 -07002367 * Copy any selected conversations stored in the saved bundle into our selection set,
2368 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2369 *
2370 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002371 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002372 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07002373 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002374 return;
2375 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002376 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002377 if (selectedSet == null || selectedSet.isEmpty()) {
2378 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002379 return;
2380 }
Andy Huang632721e2012-04-11 16:57:26 -07002381
2382 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002383 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002384 }
2385
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002386 private void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07002387 showConversation(conversation, false /* inLoaderCallbacks */);
2388 }
2389
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002390 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002391 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2392 * object, which is a signal to back out of conversation view mode.
2393 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002394 * @param conversation the conversation to be shown, or null if we want to back out to list
2395 * mode.
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002396 * @param inLoaderCallbacks true if the method is called as a result of
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002397 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002398 */
Andy Huang1ee96b22012-08-24 20:19:53 -07002399 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Andy Huang243c2362013-03-01 17:50:35 -08002400 if (conversation != null) {
2401 Utils.sConvLoadTimer.start();
2402 }
2403
Andy Huang54e925e2013-03-14 13:24:18 -07002404 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002405 // Set the current conversation just in case it wasn't already set.
2406 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002407 }
2408
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002409 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002410 * Children can override this method, but they must call super.showWaitForInitialization().
2411 * {@inheritDoc}
2412 */
2413 @Override
2414 public void showWaitForInitialization() {
2415 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002416 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002417 }
2418
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002419 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002420 final FragmentManager manager = mActivity.getFragmentManager();
2421 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002422 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002423 if (waitFragment != null) {
2424 waitFragment.updateAccount(mAccount);
2425 }
2426 }
2427
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002428 /**
2429 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2430 * method, though they must call the parent implementation <b>after</b> they do anything.
2431 */
2432 protected void hideWaitForInitialization() {
2433 mWaitFragment = null;
2434 }
2435
2436 /**
2437 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2438 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2439 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002440 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002441 */
2442 protected final WaitFragment getWaitFragment() {
2443 final FragmentManager manager = mActivity.getFragmentManager();
2444 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2445 if (waitFrag != null) {
2446 // The Fragment Manager knows better, so use its instance.
2447 mWaitFragment = waitFrag;
2448 }
2449 return mWaitFragment;
2450 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002451
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002452 /**
2453 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2454 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002455 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002456 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002457 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002458 if (waitFragment != null) {
2459 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002460 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002461 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2462 }
2463 return false;
2464 }
2465
2466 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002467 * Children can override this method, but they must call super.showConversationList().
2468 * {@inheritDoc}
2469 */
2470 @Override
2471 public void showConversationList(ConversationListContext listContext) {
2472 }
2473
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002474 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002475 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002476 final ConversationListFragment convListFragment = getConversationListFragment();
2477 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2478 convListFragment.getAnimatedAdapter().onConversationSelected();
2479 }
mindypaa55bc92012-08-24 09:49:56 -07002480 // Only animate destructive actions if we are going to be showing the
2481 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002482 commitDestructiveActions(mIsTablet);
Andy Huang1ee96b22012-08-24 20:19:53 -07002483 showConversation(conversation, inLoaderCallbacks);
2484 }
2485
2486 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002487 public final void onCabModeEntered() {
2488 final ConversationListFragment convListFragment = getConversationListFragment();
2489 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2490 convListFragment.getAnimatedAdapter().onCabModeEntered();
2491 }
2492 }
2493
2494 @Override
Scott Kennedycc139832013-08-19 18:03:54 -07002495 public final void onCabModeExited() {
2496 final ConversationListFragment convListFragment = getConversationListFragment();
2497 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2498 convListFragment.getAnimatedAdapter().onCabModeExited();
2499 }
2500 }
2501
2502 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002503 public Conversation getCurrentConversation() {
2504 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002505 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002506
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002507 /**
2508 * Set the current conversation. This is the conversation on which all actions are performed.
2509 * Do not modify mCurrentConversation except through this method, which makes it easy to
2510 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002511 * @param conversation new conversation to view. Passing null indicates that we are backing
2512 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002513 */
Andy Huang632721e2012-04-11 16:57:26 -07002514 @Override
2515 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002516 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002517 // we are going back to conversation list mode.
2518 if (mDetachedConvUri != null && (conversation == null
2519 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002520 clearDetachedMode();
2521 }
2522
2523 // Must happen *before* setting mCurrentConversation because this sets
2524 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002525 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002526 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002527
2528 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07002529 mActionBarView.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002530 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002531 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002532 }
2533
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002534 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002535 * {@link LoaderManager} currently has a bug in
2536 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2537 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2538 * this bug by destroying any loaders that may have been created as null (essentially because
2539 * they are optional loads, and may not apply to a particular account).
2540 * <p>
2541 * A simple null check before restarting a loader will not work, because that would not
2542 * give the controller a chance to invalidate UI corresponding the prior loader result.
2543 *
2544 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002545 * @param handler the LoaderCallback which will handle this loader ID.
2546 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2547 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002548 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002549 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002550 final LoaderManager lm = mActivity.getLoaderManager();
2551 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002552 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002553 }
2554
Andy Huang632721e2012-04-11 16:57:26 -07002555 @Override
2556 public void registerConversationListObserver(DataSetObserver observer) {
2557 mConversationListObservable.registerObserver(observer);
2558 }
2559
2560 @Override
2561 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002562 try {
2563 mConversationListObservable.unregisterObserver(observer);
2564 } catch (IllegalStateException e) {
2565 // Log instead of crash
2566 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2567 + "hasn't been registered");
2568 }
Andy Huang632721e2012-04-11 16:57:26 -07002569 }
2570
Andy Huang090db1e2012-07-25 13:25:28 -07002571 @Override
2572 public void registerFolderObserver(DataSetObserver observer) {
2573 mFolderObservable.registerObserver(observer);
2574 }
2575
2576 @Override
2577 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002578 try {
2579 mFolderObservable.unregisterObserver(observer);
2580 } catch (IllegalStateException e) {
2581 // Log instead of crash
2582 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2583 + "hasn't been registered");
2584 }
Andy Huang090db1e2012-07-25 13:25:28 -07002585 }
2586
Andy Huang9d3fd922012-09-26 22:23:58 -07002587 @Override
2588 public void registerConversationLoadedObserver(DataSetObserver observer) {
2589 mPagerController.registerConversationLoadedObserver(observer);
2590 }
2591
2592 @Override
2593 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002594 try {
2595 mPagerController.unregisterConversationLoadedObserver(observer);
2596 } catch (IllegalStateException e) {
2597 // Log instead of crash
2598 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2599 + "that hasn't been registered");
2600 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002601 }
2602
Vikram Aggarwal60069912012-07-24 14:26:09 -07002603 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002604 * Returns true if the number of accounts is different, or if the current account has
2605 * changed. This method is meant to filter frequent changes to the list of
2606 * accounts, and only return true if the new list is substantially different from the existing
2607 * list. Returning true is safe here, it leads to more work in creating the
2608 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002609 * @param accountCursor the cursor which points to all the accounts.
2610 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002611 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002612 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002613 // Check to see if the current account hasn't been set, or the account cursor is empty
2614 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002615 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002616 }
2617
2618 // Check to see if the number of accounts are different, from the number we saw on the last
2619 // updated
2620 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2621 return true;
2622 }
2623
2624 // Check to see if the account list is different or if the current account is not found in
2625 // the cursor.
2626 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002627 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002628 final Account account = accountCursor.getModel();
2629 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2630 if (mAccount.settingsDiffer(account)) {
2631 // Settings changed, and we don't need to look any further.
2632 return true;
2633 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002634 foundCurrentAccount = true;
2635 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002636 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002637 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002638 return true;
2639 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002640 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002641
2642 // As long as we found the current account, the list hasn't been updated
2643 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002644 }
2645
2646 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002647 * Updates accounts for the app. If the current account is missing, the first
2648 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002649 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002650 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002651 * @return true if the update was successful, false otherwise
2652 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002653 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002654 if (accounts == null || !accounts.moveToFirst()) {
2655 return false;
2656 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002657
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002658 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002659 // A match for the current account's URI in the list of accounts.
2660 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002661
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002662 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002663 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002664 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002665 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002666 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002667 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2668 currentFromList = account;
2669 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002670 }
2671
Vikram Aggarwal60069912012-07-24 14:26:09 -07002672 // 1. current account is already set and is in allAccounts:
2673 // 1a. It has changed -> load the updated account.
2674 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002675 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002676 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002677 // 4. otherwise just pick first
2678
Vikram Aggarwal60069912012-07-24 14:26:09 -07002679 boolean accountChanged = false;
2680 /// Assume case 4, initialize to first account, and see if we can find anything better.
2681 Account newAccount = allAccounts[0];
2682 if (currentFromList != null) {
2683 // Case 1: Current account exists but has changed
2684 if (!currentFromList.equals(mAccount)) {
2685 newAccount = currentFromList;
2686 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002687 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002688 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002689 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002690 // Case 2: Current account is not in allAccounts, the account needs to change.
2691 accountChanged = true;
2692 if (mAccount == null) {
2693 // Case 3: Check for last viewed account, and check if it exists in the list.
2694 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2695 if (lastAccountUri != null) {
2696 for (final Account account : allAccounts) {
2697 if (lastAccountUri.equals(account.uri.toString())) {
2698 newAccount = account;
2699 break;
2700 }
Andy Huang0d647352012-03-21 21:48:16 -07002701 }
2702 }
2703 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002704 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002705 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002706 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002707 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002708
Vikram Aggarwal60069912012-07-24 14:26:09 -07002709 // Whether we have updated the current account or not, we need to update the list of
2710 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002711 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002712 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002713 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002714 }
2715
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002716 private void disableNotifications() {
2717 mNewEmailReceiver.activate(mContext, this);
2718 }
2719
2720 private void enableNotifications() {
2721 mNewEmailReceiver.deactivate();
2722 }
2723
2724 private void disableNotificationsOnAccountChange(Account account) {
2725 // If the new mail suppression receiver is activated for a different account, we want to
2726 // activate it for the new account.
2727 if (mNewEmailReceiver.activated() &&
2728 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2729 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2730 mNewEmailReceiver.deactivate();
2731 mNewEmailReceiver.activate(mContext, this);
2732 }
2733 }
2734
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002735 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002736 * Destructive actions on Conversations. This class should only be created by controllers, and
2737 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2738 * Only the controllers should know what kind of destructive actions are being created.
2739 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002740 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002741 /**
2742 * The action to be performed. This is specified as the resource ID of the menu item
2743 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2744 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002745 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002746 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002747 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002748 /** Whether this destructive action has already been performed */
2749 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002750 /** Whether this is an action on the currently selected set. */
2751 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002752
Mindy Pereirafbe40192012-03-20 10:40:45 -07002753 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002754 * Create a listener object.
2755 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002756 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002757 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002758 * @param isBatch whether the conversations are in the currently selected batch set.
2759 */
2760 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002761 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002762 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002763 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002764 }
2765
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002766 /**
2767 * The action common to child classes. This performs the action specified in the constructor
2768 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002769 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002770 @Override
2771 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002772 if (isPerformed()) {
2773 return;
2774 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002775 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002776
2777 // Are we destroying the currently shown conversation? Show the next one.
2778 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002779 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2780 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002781 Conversation.toString(mTarget), mCurrentConversation);
2782 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002783
Paul Westbrooke1221d22012-08-19 11:09:07 -07002784 if (mConversationListCursor == null) {
2785 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2786 + "\nmTarget=%s\nCurrent=%s",
2787 Conversation.toString(mTarget), mCurrentConversation);
2788 return;
2789 }
2790
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002791 if (mAction == R.id.archive) {
2792 LogUtils.d(LOG_TAG, "Archiving");
2793 mConversationListCursor.archive(mTarget);
2794 } else if (mAction == R.id.delete) {
2795 LogUtils.d(LOG_TAG, "Deleting");
2796 mConversationListCursor.delete(mTarget);
2797 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Paul Westbrookef362542012-08-27 14:53:32 -07002798 undoEnabled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002799 }
2800 } else if (mAction == R.id.mute) {
2801 LogUtils.d(LOG_TAG, "Muting");
2802 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2803 for (Conversation c : mTarget) {
2804 c.localDeleteOnUpdate = true;
2805 }
2806 }
2807 mConversationListCursor.mute(mTarget);
2808 } else if (mAction == R.id.report_spam) {
2809 LogUtils.d(LOG_TAG, "Reporting spam");
2810 mConversationListCursor.reportSpam(mTarget);
2811 } else if (mAction == R.id.mark_not_spam) {
2812 LogUtils.d(LOG_TAG, "Marking not spam");
2813 mConversationListCursor.reportNotSpam(mTarget);
2814 } else if (mAction == R.id.report_phishing) {
2815 LogUtils.d(LOG_TAG, "Reporting phishing");
2816 mConversationListCursor.reportPhishing(mTarget);
2817 } else if (mAction == R.id.remove_star) {
2818 LogUtils.d(LOG_TAG, "Removing star");
2819 // Star removal is destructive in the Starred folder.
2820 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2821 false);
2822 } else if (mAction == R.id.mark_not_important) {
2823 LogUtils.d(LOG_TAG, "Marking not-important");
2824 // Marking not important is destructive in a mailbox
2825 // containing only important messages
2826 if (mFolder != null && mFolder.isImportantOnly()) {
2827 for (Conversation conv : mTarget) {
2828 conv.localDeleteOnUpdate = true;
2829 }
2830 }
2831 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2832 UIProvider.ConversationPriority.LOW);
2833 } else if (mAction == R.id.discard_drafts) {
2834 LogUtils.d(LOG_TAG, "Discarding draft messages");
2835 // Discarding draft messages is destructive in a "draft" mailbox
2836 if (mFolder != null && mFolder.isDraft()) {
2837 for (Conversation conv : mTarget) {
2838 conv.localDeleteOnUpdate = true;
2839 }
2840 }
2841 mConversationListCursor.discardDrafts(mTarget);
2842 // We don't support undoing discarding drafts
2843 undoEnabled = false;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002844 }
2845 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002846 mHandler.postDelayed(new Runnable() {
2847 @Override
2848 public void run() {
2849 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002850 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002851 }
2852 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002853 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002854 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002855 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002856 mSelectedSet.clear();
2857 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002858 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002859
2860 /**
2861 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002862 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002863 */
2864 private synchronized boolean isPerformed() {
2865 if (mCompleted) {
2866 return true;
2867 }
2868 mCompleted = true;
2869 return false;
2870 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002871 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002872
Vikram Aggarwald503df42012-05-11 10:13:35 -07002873 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2874 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002875 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002876 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002877 Collection<Conversation> target, boolean batch, boolean showUndo,
2878 final boolean isMoveTo) {
Mindy Pereira8db7e402012-07-13 10:32:47 -07002879 // Actions are destructive only when the current folder can be assigned
2880 // to (which is the same as being able to un-assign a conversation from the folder) and
2881 // when the list of folders contains the current folder.
2882 final boolean isDestructive = mFolder
2883 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2884 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002885 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2886 if (isDestructive) {
2887 for (final Conversation c : target) {
2888 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002889 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002890 }
mindypc84759c2012-08-29 09:51:53 -07002891 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002892 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002893 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002894 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002895 /*
2896 * If this is a MOVE operation, we want the action folder to be the destination folder.
2897 * Otherwise, we want it to be the current folder.
2898 *
2899 * A set of folder operations is a move if there are exactly two operations: an add and
2900 * a remove.
2901 */
2902 final Folder actionFolder;
2903 if (folderOps.size() != 2) {
2904 actionFolder = mFolder;
2905 } else {
2906 Folder addedFolder = null;
2907 boolean hasRemove = false;
2908 for (final FolderOperation folderOperation : folderOps) {
2909 if (folderOperation.mAdd) {
2910 addedFolder = folderOperation.mFolder;
2911 } else {
2912 hasRemove = true;
2913 }
2914 }
2915
2916 if (hasRemove && addedFolder != null) {
2917 actionFolder = addedFolder;
2918 } else {
2919 actionFolder = mFolder;
2920 }
2921 }
2922
mindypc84759c2012-08-29 09:51:53 -07002923 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002924 batch, showUndo, isMoveTo, actionFolder);
Scott Kennedycaaeed32013-06-12 13:39:16 -07002925 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002926 } else {
mindypc84759c2012-08-29 09:51:53 -07002927 folderChange = getFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002928 batch, showUndo, false /* isMoveTo */, mFolder);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002929 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002930 }
2931 }
2932
Mindy Pereira967ede62012-03-22 09:29:09 -07002933 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002934 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002935 if (isAnimating() || isDragging()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002936 LogUtils.i(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done");
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002937 return;
2938 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002939 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002940 if (mConversationListCursor.isRefreshRequired()) {
2941 mConversationListCursor.refresh();
2942 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002943 }
2944
mindyp5390fca2012-08-22 12:12:25 -07002945 @Override
2946 public void startDragMode() {
2947 mIsDragHappening = true;
2948 }
2949
2950 @Override
2951 public void stopDragMode() {
2952 mIsDragHappening = false;
2953 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002954 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: try sync");
mindyp5390fca2012-08-22 12:12:25 -07002955 onRefreshReady();
2956 }
2957
2958 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002959 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: refresh");
mindyp5390fca2012-08-22 12:12:25 -07002960 mConversationListCursor.refresh();
2961 }
2962 }
2963
2964 private boolean isDragging() {
2965 return mIsDragHappening;
2966 }
2967
mindyp6f54e1b2012-10-09 09:54:08 -07002968 @Override
2969 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002970 boolean isAnimating = false;
2971 ConversationListFragment convListFragment = getConversationListFragment();
2972 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07002973 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002974 }
2975 return isAnimating;
2976 }
2977
Marc Blankbf128eb2012-04-18 15:58:45 -07002978 /**
2979 * Called when the {@link ConversationCursor} is changed or has new data in it.
2980 * <p>
2981 * {@inheritDoc}
2982 */
2983 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002984 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002985 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2986 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06002987
2988 if (mDestroyed) {
2989 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2990 return;
2991 }
2992
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002993 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002994 // Swap cursors
2995 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002996 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002997 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002998 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002999 }
3000
3001 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003002 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07003003 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07003004 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07003005 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07003006 }
3007
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07003008 /**
3009 * If the Conversation List Fragment is visible, updates the fragment.
3010 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003011 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07003012 final ConversationListFragment convList = getConversationListFragment();
3013 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003014 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003015 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07003016 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07003017 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003018 }
3019 }
3020
3021 /**
3022 * This class handles throttled refresh of the conversation list
3023 */
3024 static class RefreshTimerTask extends TimerTask {
3025 final Handler mHandler;
3026 final AbstractActivityController mController;
3027
3028 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
3029 mHandler = handler;
3030 mController = controller;
3031 }
3032
3033 @Override
3034 public void run() {
3035 mHandler.post(new Runnable() {
3036 @Override
3037 public void run() {
3038 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
3039 mController.onRefreshRequired();
3040 }});
3041 }
3042 }
3043
3044 /**
3045 * Cancel the refresh task, if it's running
3046 */
3047 private void cancelRefreshTask () {
3048 if (mConversationListRefreshTask != null) {
3049 mConversationListRefreshTask.cancel();
3050 mConversationListRefreshTask = null;
3051 }
3052 }
3053
3054 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003055 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07003056 if (mConversationListCursor == null) {
3057 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3058 return;
3059 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003060 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003061 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003062 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003063 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003064
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003065 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003066 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003067 mConversationListCursor.refresh();
3068 }
mindyp6f54e1b2012-10-09 09:54:08 -07003069 if (mRecentsDataUpdated) {
3070 mRecentsDataUpdated = false;
3071 mRecentFolderObservers.notifyChanged();
3072 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003073 }
3074
3075 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003076 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003077 // There are no selected conversations. Ensure that the listener and its associated actions
3078 // are blanked out.
3079 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003080 }
3081
3082 @Override
3083 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003084 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003085 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003086 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003087 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003088 }
3089
Mindy Pereira967ede62012-03-22 09:29:09 -07003090 @Override
3091 public void onSetChanged(ConversationSelectionSet set) {
3092 // Do nothing. We don't care about changes to the set.
3093 }
3094
3095 @Override
3096 public ConversationSelectionSet getSelectedSet() {
3097 return mSelectedSet;
3098 }
3099
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003100 /**
3101 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3102 */
3103 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003104 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003105 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003106 if (mCabActionMenu != null) {
3107 mCabActionMenu.deactivate();
3108 }
3109 }
3110
3111 /**
3112 * Re-enable the CAB menu if required. The selection set is not changed.
3113 */
3114 protected void enableCabMode() {
Tony Mantler3d6751a2013-08-02 11:30:59 -07003115 if (mCabActionMenu != null &&
3116 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) {
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003117 mCabActionMenu.activate();
3118 }
3119 }
3120
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003121 /**
Tony Mantler43fab322013-07-26 16:38:35 -07003122 * Re-enable CAB mode only if we have an active selection
3123 */
3124 protected void maybeEnableCabMode() {
3125 if (!mSelectedSet.isEmpty()) {
Tony Mantlera703d8a2013-07-31 12:00:46 -07003126 if (mCabActionMenu != null) {
3127 mCabActionMenu.activate();
3128 }
Tony Mantler43fab322013-07-26 16:38:35 -07003129 }
3130 }
3131
3132 /**
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003133 * Unselect conversations and exit CAB mode.
3134 */
3135 protected final void exitCabMode() {
3136 mSelectedSet.clear();
3137 }
3138
Mindy Pereira967ede62012-03-22 09:29:09 -07003139 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003140 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003141 if (mAccount == null) {
3142 // We cannot search if there is no account. Drop the request to the floor.
3143 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3144 return;
3145 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003146 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
Andy Huang313ac132013-03-04 23:40:58 -08003147 || mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
3148 mActionBarView.expandSearch();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003149 } else {
3150 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003151 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003152 }
3153 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003154
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07003155 @Override
3156 public void exitSearchMode() {
3157 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
3158 mActivity.finish();
3159 }
3160 }
3161
Mindy Pereiraacf60392012-04-06 09:11:00 -07003162 /**
3163 * Supports dragging conversations to a folder.
3164 */
3165 @Override
3166 public boolean supportsDrag(DragEvent event, Folder folder) {
3167 return (folder != null
3168 && event != null
3169 && event.getClipDescription() != null
3170 && folder.supportsCapability
3171 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
3172 && folder.supportsCapability
3173 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
Scott Kennedy259df5b2013-07-11 13:24:01 -07003174 && !mFolder.equals(folder));
Mindy Pereiraacf60392012-04-06 09:11:00 -07003175 }
3176
3177 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07003178 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07003179 */
3180 @Override
3181 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07003182 if (!supportsDrag(event, folder)) {
3183 return;
3184 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003185 if (folder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003186 // Moving a conversation to the starred folder adds the star and
3187 // removes the current label
3188 handleDropInStarred(folder);
3189 return;
3190 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003191 if (mFolder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003192 handleDragFromStarred(folder);
3193 return;
3194 }
mindypa8492632012-09-24 09:27:54 -07003195 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08003196 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07003197 // Add the drop target folder.
3198 dragDropOperations.add(new FolderOperation(folder, true));
3199 // Remove the current folder unless the user is viewing "all".
3200 // That operation should just add the new folder.
3201 boolean isDestructive = !mFolder.isViewAll()
3202 && mFolder.supportsCapability
3203 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
3204 if (isDestructive) {
3205 dragDropOperations.add(new FolderOperation(mFolder, false));
3206 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003207 // Drag and drop is destructive: we remove conversations from the
3208 // current folder.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003209 final DestructiveAction action =
3210 getFolderChange(conversations, dragDropOperations, isDestructive,
3211 true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder);
mindypa8492632012-09-24 09:27:54 -07003212 if (isDestructive) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07003213 delete(0, conversations, action, true);
mindypa8492632012-09-24 09:27:54 -07003214 } else {
3215 action.performAction();
3216 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003217 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07003218
mindypae7e6a02012-11-29 13:28:10 -08003219 private void handleDragFromStarred(Folder folder) {
3220 final Collection<Conversation> conversations = mSelectedSet.values();
3221 // The conversation list deletes and performs the action if it exists.
3222 final ConversationListFragment convListFragment = getConversationListFragment();
3223 // There should always be a convlistfragment, or the user could not have
3224 // dragged/ dropped conversations.
3225 if (convListFragment != null) {
3226 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
3227 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08003228 ArrayList<Uri> folderUris;
3229 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08003230 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003231 folderUris = new ArrayList<Uri>();
3232 adds = new ArrayList<Boolean>();
Scott Kennedy259df5b2013-07-11 13:24:01 -07003233 folderUris.add(folder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003234 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003235 final HashMap<Uri, Folder> targetFolders =
3236 Folder.hashMapForFolders(target.getRawFolders());
Scott Kennedy259df5b2013-07-11 13:24:01 -07003237 targetFolders.put(folder.folderUri.fullUri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003238 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3239 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08003240 }
3241 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003242 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003243 }
3244 refreshConversationList();
3245 mSelectedSet.clear();
mindypae7e6a02012-11-29 13:28:10 -08003246 }
3247 }
3248
3249 private void handleDropInStarred(Folder folder) {
3250 final Collection<Conversation> conversations = mSelectedSet.values();
3251 // The conversation list deletes and performs the action if it exists.
3252 final ConversationListFragment convListFragment = getConversationListFragment();
3253 // There should always be a convlistfragment, or the user could not have
3254 // dragged/ dropped conversations.
3255 if (convListFragment != null) {
3256 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07003257 convListFragment.requestDelete(R.id.change_folders, conversations,
mindyp5cc0ab22012-12-11 08:47:35 -08003258 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08003259 }
3260 }
3261
3262 // When dragging conversations to the starred folder, remove from the
3263 // original folder and add a star
3264 private class DroppedInStarredAction implements DestructiveAction {
3265 private Collection<Conversation> mConversations;
3266 private Folder mInitialFolder;
mindyp5cc0ab22012-12-11 08:47:35 -08003267 private Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08003268
mindyp5cc0ab22012-12-11 08:47:35 -08003269 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
3270 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08003271 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08003272 mInitialFolder = initialFolder;
3273 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08003274 }
3275
3276 @Override
3277 public void performAction() {
3278 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07003279 R.id.change_folders, ToastBarOperation.UNDO, true /* batch */, mInitialFolder);
mindypae7e6a02012-11-29 13:28:10 -08003280 onUndoAvailable(undoOp);
3281 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3282 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08003283 ArrayList<Uri> folderUris;
3284 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08003285 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08003286 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003287 folderUris = new ArrayList<Uri>();
3288 adds = new ArrayList<Boolean>();
Scott Kennedy259df5b2013-07-11 13:24:01 -07003289 folderUris.add(mStarred.folderUri.fullUri);
mindyp5cc0ab22012-12-11 08:47:35 -08003290 adds.add(Boolean.TRUE);
Scott Kennedy259df5b2013-07-11 13:24:01 -07003291 folderUris.add(mInitialFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003292 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08003293 final HashMap<Uri, Folder> targetFolders =
3294 Folder.hashMapForFolders(target.getRawFolders());
Scott Kennedy259df5b2013-07-11 13:24:01 -07003295 targetFolders.put(mStarred.folderUri.fullUri, mStarred);
3296 targetFolders.remove(mInitialFolder.folderUri.fullUri);
mindyp5cc0ab22012-12-11 08:47:35 -08003297 values.put(ConversationColumns.STARRED, true);
3298 operation = mConversationListCursor.getConversationFolderOperation(target,
3299 folderUris, adds, targetFolders.values(), values);
3300 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08003301 }
3302 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003303 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003304 }
3305 refreshConversationList();
3306 mSelectedSet.clear();
3307 }
3308 }
3309
Mindy Pereira0963ef82012-04-10 11:43:01 -07003310 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003311 public void onTouchEvent(MotionEvent event) {
3312 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003313 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07003314 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003315 }
3316 }
3317 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003318
Mark Weid243d452012-10-31 16:24:08 -07003319 protected abstract void hideOrRepositionToastBar(boolean animated);
3320
Andy Huang632721e2012-04-11 16:57:26 -07003321 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003322 public void onConversationSeen() {
3323 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003324 }
3325
Andy Huang9d3fd922012-09-26 22:23:58 -07003326 @Override
3327 public boolean isInitialConversationLoading() {
3328 return mPagerController.isInitialConversationLoading();
3329 }
3330
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003331 /**
3332 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3333 * insufficient because that doesn't check if the window is currently in focus or not.
3334 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003335 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003336 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3337 }
3338
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003339 /**
3340 * This class handles callbacks that create a {@link ConversationCursor}.
3341 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003342 private class ConversationListLoaderCallbacks implements
3343 LoaderManager.LoaderCallbacks<ConversationCursor> {
3344
3345 @Override
3346 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003347 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3348 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
3349 if (account == null || folder == null) {
3350 return null;
3351 }
3352 return new ConversationCursorLoader((Activity) mActivity, account,
3353 folder.conversationListUri, folder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003354 }
3355
3356 @Override
3357 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003358 LogUtils.d(LOG_TAG,
3359 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3360 data, loader, this);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003361 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003362 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003363 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003364 return;
3365 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003366 // Clear our all pending destructive actions before swapping the conversation cursor
3367 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003368 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003369 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003370 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003371 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003372 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003373 // Handle actions that were deferred until after the conversation list was loaded.
3374 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3375 callback.onLoadFinished();
3376 }
3377 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003378
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003379 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003380 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003381 // The conversation list is already listening to list changes and gets notified
3382 // in the mConversationListObservable.notifyChanged() line above. We only need to
3383 // check and inform the cursor of the change in visibility here.
3384 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003385 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003386 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003387 }
3388
3389 @Override
3390 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003391 LogUtils.d(LOG_TAG,
3392 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3393 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003394
3395 if (mConversationListCursor != null) {
3396 // Unregister the listener
3397 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003398 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003399 mConversationListCursor = null;
3400
3401 // Inform anyone who is interested about the change
3402 mTracker.onCursorUpdated();
3403 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003404 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003405 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003406 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003407
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003408 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003409 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3410 */
3411 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3412 @Override
3413 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3414 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3415 switch (id) {
3416 case LOADER_FOLDER_CURSOR:
3417 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3418 final ObjectCursorLoader<Folder> loader = new
3419 ObjectCursorLoader<Folder>(
Scott Kennedy259df5b2013-07-11 13:24:01 -07003420 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003421 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3422 return loader;
3423 case LOADER_RECENT_FOLDERS:
3424 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003425 if (mAccount != null && mAccount.recentFolderListUri != null
3426 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003427 return new ObjectCursorLoader<Folder>(mContext,
3428 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3429 }
3430 break;
3431 case LOADER_ACCOUNT_INBOX:
3432 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3433 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3434 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3435 mAccount.folderListUri : defaultInbox;
3436 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3437 if (inboxUri != null) {
3438 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3439 everything, Folder.FACTORY);
3440 }
3441 break;
3442 case LOADER_SEARCH:
3443 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3444 return Folder.forSearchResults(mAccount,
3445 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
3446 mActivity.getActivityContext());
3447 case LOADER_FIRST_FOLDER:
3448 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3449 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3450 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3451 if (mConversationToShow != null && mConversationToShow.position < 0){
3452 mConversationToShow.position = 0;
3453 }
3454 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3455 everything, Folder.FACTORY);
3456 default:
3457 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3458 return null;
3459 }
3460 return null;
3461 }
3462
3463 @Override
3464 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3465 if (data == null) {
3466 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3467 }
3468 switch (loader.getId()) {
3469 case LOADER_FOLDER_CURSOR:
3470 if (data != null && data.moveToFirst()) {
3471 final Folder folder = data.getModel();
3472 setHasFolderChanged(folder);
3473 mFolder = folder;
3474 mFolderObservable.notifyChanged();
3475 } else {
3476 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
3477 mFolder != null ? mAccount.name : "");
3478 }
3479 break;
3480 case LOADER_RECENT_FOLDERS:
3481 // Few recent folders and we are running on a phone? Populate the default
3482 // recents. The number of default recent folders is at least 2: every provider
3483 // has at least two folders, and the recent folder count never decreases.
3484 // Having a single recent folder is an erroneous case, and we can gracefully
3485 // recover by populating default recents. The default recents will not stomp on
3486 // the existing value: it will be shown in addition to the default folders:
3487 // the max number of recent folders is more than 1+num(defaultRecents).
3488 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3489 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3490 @Override
3491 protected Void doInBackground(Uri... uri) {
3492 // Asking for an update on the URI and ignore the result.
3493 final ContentResolver resolver = mContext.getContentResolver();
3494 resolver.update(uri[0], null, null, null);
3495 return null;
3496 }
3497 }
3498 final Uri uri = mAccount.defaultRecentFolderListUri;
3499 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3500 new PopulateDefault().execute(uri);
3501 break;
3502 }
3503 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3504 mRecentFolderList.loadFromUiProvider(data);
3505 if (isAnimating()) {
3506 mRecentsDataUpdated = true;
3507 } else {
3508 mRecentFolderObservers.notifyChanged();
3509 }
3510 break;
3511 case LOADER_ACCOUNT_INBOX:
3512 if (data != null && !data.isClosed() && data.moveToFirst()) {
3513 final Folder inbox = data.getModel();
Scott Kennedyad418142013-07-10 17:18:22 -07003514 onFolderChanged(inbox, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003515 // Just want to get the inbox, don't care about updates to it
3516 // as this will be tracked by the folder change listener.
3517 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3518 } else {
3519 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
3520 mAccount != null ? mAccount.name : "");
3521 }
3522 break;
3523 case LOADER_SEARCH:
3524 if (data != null && data.getCount() > 0) {
3525 data.moveToFirst();
3526 final Folder search = data.getModel();
3527 updateFolder(search);
3528 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3529 mActivity.getIntent()
3530 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3531 showConversationList(mConvListContext);
3532 mActivity.invalidateOptionsMenu();
3533 mHaveSearchResults = search.totalCount > 0;
3534 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3535 } else {
3536 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3537 }
3538 break;
3539 case LOADER_FIRST_FOLDER:
3540 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3541 return;
3542 }
3543 final Folder folder = data.getModel();
3544 boolean handled = false;
3545 if (folder != null) {
Scott Kennedyad418142013-07-10 17:18:22 -07003546 onFolderChanged(folder, false /* force */);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003547 handled = true;
3548 }
3549 if (mConversationToShow != null) {
3550 // Open the conversation.
3551 showConversation(mConversationToShow);
3552 handled = true;
3553 }
3554 if (!handled) {
3555 // We have an account, but nothing else: load the default inbox.
3556 loadAccountInbox();
3557 }
3558 mConversationToShow = null;
3559 // And don't run this anymore.
3560 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3561 break;
3562 }
3563 }
3564
3565 @Override
3566 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3567 }
3568 }
3569
3570 /**
3571 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3572 */
3573 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3574 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3575 final CursorCreator<Account> mFactory = Account.FACTORY;
3576
3577 @Override
3578 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3579 switch (id) {
3580 case LOADER_ACCOUNT_CURSOR:
3581 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3582 return new ObjectCursorLoader<Account>(mContext,
3583 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3584 case LOADER_ACCOUNT_UPDATE_CURSOR:
3585 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3586 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3587 mFactory);
3588 default:
3589 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3590 break;
3591 }
3592 return null;
3593 }
3594
3595 @Override
3596 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3597 ObjectCursor<Account> data) {
3598 if (data == null) {
3599 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3600 }
3601 switch (loader.getId()) {
3602 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003603 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003604 if (data == null) {
3605 // Nothing useful to do if we have no valid data.
3606 break;
3607 }
Andy Huang761522c2013-08-08 13:09:11 -07003608 final long count = data.getCount();
3609 if (count == 0) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003610 // If an empty cursor is returned, the MailAppProvider is indicating that
3611 // no accounts have been specified. We want to navigate to the
3612 // "add account" activity that will handle the intent returned by the
3613 // MailAppProvider
3614
3615 // If the MailAppProvider believes that all accounts have been loaded,
3616 // and the account list is still empty, we want to prompt the user to add
3617 // an account.
3618 final Bundle extras = data.getExtras();
3619 final boolean accountsLoaded =
3620 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3621
3622 if (accountsLoaded) {
3623 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3624 (mContext);
3625 if (noAccountIntent != null) {
3626 mActivity.startActivityForResult(noAccountIntent,
3627 ADD_ACCOUNT_REQUEST_CODE);
3628 }
3629 }
3630 } else {
3631 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003632 if (!mHaveAccountList || accountListUpdated) {
3633 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003634 }
Andy Huang761522c2013-08-08 13:09:11 -07003635 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT,
3636 Long.toString(count));
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003637 }
3638 break;
3639 case LOADER_ACCOUNT_UPDATE_CURSOR:
3640 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003641 if (data != null && data.moveToFirst()) {
3642 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003643 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003644 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003645 final Settings previousSettings = mAccount.settings;
3646
3647 // Update the controller's reference to the current account
3648 mAccount = updatedAccount;
3649 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3650 + "mAccount = %s", mAccount.uri);
3651
3652 // Only notify about a settings change if something differs
3653 if (!Objects.equal(mAccount.settings, previousSettings)) {
3654 mAccountObservers.notifyChanged();
3655 }
3656 perhapsEnterWaitMode();
3657 } else {
3658 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3659 + " %s", updatedAccount.uri, mAccount.uri);
3660 // We need to restart the loader, so the correct account information
3661 // will be returned.
3662 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3663 }
3664 }
3665 break;
3666 }
3667 }
3668
3669 @Override
3670 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003671 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003672 }
3673 }
3674
3675 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003676 * Updates controller state based on search results and shows first conversation if required.
3677 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003678 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003679 if (mCurrentConversation == null) {
3680 // Shown for search results in two-pane mode only.
3681 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3682 && mConversationListCursor.getCount() > 0;
3683 if (!shouldShowFirstConversation()) {
3684 return;
3685 }
3686 mConversationListCursor.moveToPosition(0);
3687 final Conversation conv = new Conversation(mConversationListCursor);
3688 conv.position = 0;
3689 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003690 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003691 }
3692
3693 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003694 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3695 * next destructive action..
3696 * @param nextAction the next destructive action to be performed. This can be null.
3697 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003698 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003699 // If there is a pending action, perform that first.
3700 if (mPendingDestruction != null) {
3701 mPendingDestruction.performAction();
3702 }
3703 mPendingDestruction = nextAction;
3704 }
3705
3706 /**
3707 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003708 * 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 -07003709 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003710 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003711 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003712 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003713 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3714 // destructive action from the controller and for the controller to own the action. This is
3715 // a half-way solution while refactoring DestructiveAction.
3716 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003717 }
3718
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003719 @Override
3720 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003721 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003722 registerDestructiveAction(da);
3723 return da;
3724 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003725
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003726 @Override
3727 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07003728 return getDeferredAction(action, mSelectedSet.values(), true);
3729 }
3730
3731 /**
3732 * Get a destructive action for a menu action. This is a temporary method,
3733 * to control the profusion of {@link DestructiveAction} classes that are
3734 * created. Please do not copy this paradigm.
3735 * @param action the resource ID of the menu action: R.id.delete, for
3736 * example
3737 * @param target the conversations to act upon.
3738 * @return a {@link DestructiveAction} that performs the specified action.
3739 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003740 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
mindypf0656a12012-10-01 08:30:57 -07003741 boolean batch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003742 return new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003743 }
3744
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003745 /**
3746 * Class to change the folders that are assigned to a set of conversations. This is destructive
3747 * because the user can remove the current folder from the conversation, in which case it has
3748 * to be animated away from the current folder.
3749 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003750 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003751 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003752 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003753 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003754 /** Whether this destructive action has already been performed */
3755 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003756 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003757 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003758 private int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003759 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003760
3761 /**
3762 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003763 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003764 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003765 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003766 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003767 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003768 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003769 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003770 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003771 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003772 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003773 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003774 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003775 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003776 }
3777
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003778 @Override
3779 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003780 if (isPerformed()) {
3781 return;
3782 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07003783 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08003784 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003785 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003786 onUndoAvailable(undoOp);
3787 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003788 // For each conversation, for each operation, add/ remove the
3789 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003790 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3791 ArrayList<Uri> folderUris;
3792 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003793 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003794 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3795 .getRawFolders());
3796 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003797 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003798 if (mIsDestructive) {
3799 target.localDeleteOnUpdate = true;
3800 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003801 for (FolderOperation op : mFolderOps) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003802 folderUris.add(op.mFolder.folderUri.fullUri);
mindypcb0b30e2012-11-30 10:16:35 -08003803 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003804 if (op.mAdd) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003805 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003806 } else {
Scott Kennedy259df5b2013-07-11 13:24:01 -07003807 targetFolders.remove(op.mFolder.folderUri.fullUri);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003808 }
3809 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003810 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3811 folderUris, adds, targetFolders.values()));
mindyp389f0b22012-08-29 11:12:54 -07003812 }
3813 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003814 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003815 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003816 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003817 if (mIsSelectedSet) {
3818 mSelectedSet.clear();
3819 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003820 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003821
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003822 /**
3823 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003824 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003825 */
3826 private synchronized boolean isPerformed() {
3827 if (mCompleted) {
3828 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003829 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003830 mCompleted = true;
3831 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003832 }
3833 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003834
mindypc84759c2012-08-29 09:51:53 -07003835 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3836 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003837 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
mindypc84759c2012-08-29 09:51:53 -07003838 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003839 isBatch, showUndo, isMoveTo, actionFolder);
mindypc84759c2012-08-29 09:51:53 -07003840 registerDestructiveAction(da);
3841 return da;
3842 }
3843
3844 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003845 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003846 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
3847 return new FolderDestruction(target, folders, isDestructive, isBatch, showUndo,
Andrew Sapperstein6c570db2013-08-06 17:21:36 -07003848 isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003849 }
3850
3851 @Override
3852 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3853 Folder toRemove, boolean isDestructive, boolean isBatch,
3854 boolean showUndo) {
3855 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3856 folderOps.add(new FolderOperation(toRemove, false));
3857 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003858 showUndo, R.id.remove_folder, mFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003859 }
3860
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003861 @Override
3862 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003863 final ConversationListFragment convList = getConversationListFragment();
3864 if (convList == null) {
3865 return;
3866 }
3867 convList.requestListRefresh();
3868 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003869
3870 protected final ActionClickedListener getUndoClickedListener(
3871 final AnimatedAdapter listAdapter) {
3872 return new ActionClickedListener() {
3873 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003874 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003875 if (mAccount.undoUri != null) {
3876 // NOTE: We might want undo to return the messages affected, in which case
3877 // the resulting cursor might be interesting...
3878 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3879 // commands to undo
3880 if (mConversationListCursor != null) {
3881 mConversationListCursor.undo(
3882 mActivity.getActivityContext(), mAccount.undoUri);
3883 }
3884 if (listAdapter != null) {
3885 listAdapter.setUndo(true);
3886 }
3887 }
3888 }
3889 };
3890 }
3891
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003892 /**
3893 * Shows an error toast in the bottom when a folder was not fetched successfully.
3894 * @param folder the folder which could not be fetched.
3895 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3896 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003897 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003898
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003899 final ActionClickedListener listener;
3900 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003901 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003902 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003903 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003904 // The sync request that caused this failure.
3905 final int syncRequest = lastSyncResult >> 4;
3906 // Show: User explicitly pressed the refresh button and there is no connection
3907 // Show: The first time the user enters the app and there is no connection
3908 // TODO(viki): Implement this.
3909 // Reference: http://b/7202801
3910 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3911 // Don't show: Already in the app; user switches to a synced label
3912 // Don't show: In a live label and a background sync fails
3913 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3914 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3915 if (avoidToast) {
3916 return;
3917 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003918 listener = getRetryClickedListener(folder);
3919 actionTextResourceId = R.string.retry;
3920 break;
3921 case UIProvider.LastSyncResult.AUTH_ERROR:
3922 listener = getSignInClickedListener();
3923 actionTextResourceId = R.string.signin;
3924 break;
3925 case UIProvider.LastSyncResult.SECURITY_ERROR:
3926 return; // Currently we do nothing for security errors.
3927 case UIProvider.LastSyncResult.STORAGE_ERROR:
3928 listener = getStorageErrorClickedListener();
3929 actionTextResourceId = R.string.info;
3930 break;
3931 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3932 listener = getInternalErrorClickedListener();
3933 actionTextResourceId = R.string.report;
3934 break;
3935 default:
3936 return;
3937 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003938 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07003939 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003940 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003941 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003942 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003943 replaceVisibleToast,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003944 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003945 }
3946
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003947 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003948 return new ActionClickedListener() {
3949 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003950 public void onActionClicked(Context context) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003951 final Uri uri = folder.refreshUri;
3952
3953 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003954 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003955 }
3956 }
3957 };
3958 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003959
3960 private ActionClickedListener getSignInClickedListener() {
3961 return new ActionClickedListener() {
3962 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003963 public void onActionClicked(Context context) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003964 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003965 }
3966 };
3967 }
3968
3969 private ActionClickedListener getStorageErrorClickedListener() {
3970 return new ActionClickedListener() {
3971 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003972 public void onActionClicked(Context context) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003973 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003974 }
3975 };
3976 }
3977
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003978 private void showStorageErrorDialog() {
3979 DialogFragment fragment = (DialogFragment)
3980 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3981 if (fragment == null) {
3982 fragment = SyncErrorDialogFragment.newInstance();
3983 }
3984 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3985 }
3986
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003987 private ActionClickedListener getInternalErrorClickedListener() {
3988 return new ActionClickedListener() {
3989 @Override
Andrew Sappersteinf8ccdcf2013-08-08 19:30:38 -07003990 public void onActionClicked(Context context) {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003991 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003992 }
3993 };
3994 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003995
3996 @Override
3997 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3998 Uri uri = null;
3999 switch (errorStatus) {
4000 case UIProvider.LastSyncResult.CONNECTION_ERROR:
4001 if (folder != null && folder.refreshUri != null) {
4002 uri = folder.refreshUri;
4003 }
4004 break;
4005 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07004006 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004007 return;
4008 case UIProvider.LastSyncResult.SECURITY_ERROR:
4009 return; // Currently we do nothing for security errors.
4010 case UIProvider.LastSyncResult.STORAGE_ERROR:
4011 showStorageErrorDialog();
4012 return;
4013 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08004014 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07004015 return;
4016 default:
4017 return;
4018 }
4019
4020 if (uri != null) {
4021 startAsyncRefreshTask(uri);
4022 }
4023 }
4024
4025 @Override
4026 public void onFooterViewLoadMoreClick(Folder folder) {
4027 if (folder != null && folder.loadMoreUri != null) {
4028 startAsyncRefreshTask(folder.loadMoreUri);
4029 }
4030 }
4031
4032 private void startAsyncRefreshTask(Uri uri) {
4033 if (mFolderSyncTask != null) {
4034 mFolderSyncTask.cancel(true);
4035 }
4036 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
4037 mFolderSyncTask.execute();
4038 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07004039
4040 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07004041 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07004042 final Intent authenticationIntent =
4043 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
4044 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
4045 }
4046 }
mindypca87de42012-09-28 15:02:39 -07004047
4048 @Override
4049 public void onAccessibilityStateChanged() {
4050 // Clear the cache of objects.
4051 ConversationItemViewModel.onAccessibilityUpdated();
4052 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004053 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07004054 if (frag != null) {
4055 AnimatedAdapter adapter = frag.getAnimatedAdapter();
4056 if (adapter != null) {
4057 adapter.notifyDataSetInvalidated();
4058 }
4059 }
4060 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004061
4062 @Override
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07004063 public void makeDialogListener (final int action, final boolean isBatch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004064 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004065 if (isBatch) {
4066 target = mSelectedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004067 } else {
4068 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4069 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004070 }
4071 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004072 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004073 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004074 mDialogListener = new AlertDialog.OnClickListener() {
4075 @Override
4076 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004077 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004078 // Afterwards, let's remove references to the listener and the action.
4079 setListener(null, -1);
4080 }
4081 };
4082 }
4083
4084 @Override
4085 public AlertDialog.OnClickListener getListener() {
4086 return mDialogListener;
4087 }
4088
4089 /**
4090 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4091 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4092 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004093 * @param listener the listener that will perform the task for this dialog's positive action.
4094 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004095 */
4096 private void setListener(AlertDialog.OnClickListener listener, final int action){
4097 mDialogListener = listener;
4098 mDialogAction = action;
4099 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004100
4101 @Override
4102 public VeiledAddressMatcher getVeiledAddressMatcher() {
4103 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004104 }
4105
4106 @Override
4107 public void setDetachedMode() {
4108 // Tell the conversation list not to select anything.
4109 final ConversationListFragment frag = getConversationListFragment();
4110 if (frag != null) {
4111 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004112 } else if (mIsTablet) {
4113 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004114 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4115 }
4116 mDetachedConvUri = mCurrentConversation.uri;
4117 }
4118
4119 private void clearDetachedMode() {
4120 // Tell the conversation list to go back to its usual selection behavior.
4121 final ConversationListFragment frag = getConversationListFragment();
4122 if (frag != null) {
4123 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004124 } else if (mIsTablet) {
4125 // How did we ever land here? Detached mode, and no CLF on tablet???
4126 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004127 }
4128 mDetachedConvUri = null;
4129 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004130
4131 private class MailDrawerListener implements DrawerLayout.DrawerListener {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004132 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004133 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004134
4135 public MailDrawerListener() {
4136 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004137 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004138 }
4139
Andy Huang12b3ee42013-04-24 22:49:43 -07004140 @Override
4141 public void onDrawerOpened(View drawerView) {
4142 mDrawerToggle.onDrawerOpened(drawerView);
4143 }
4144
4145 @Override
4146 public void onDrawerClosed(View drawerView) {
4147 mDrawerToggle.onDrawerClosed(drawerView);
4148 if (mHasNewAccountOrFolder) {
4149 refreshDrawer();
4150 }
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004151
4152 // When closed, we want to use either the burger, or up, based on where we are
4153 final int mode = mViewMode.getMode();
4154 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
4155 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel));
Andy Huang12b3ee42013-04-24 22:49:43 -07004156 }
4157
4158 /**
4159 * As part of the overriden function, it will animate the alpha of the conversation list
4160 * view along with the drawer sliding when we're in the process of switching accounts or
4161 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4162 */
4163 @Override
4164 public void onDrawerSlide(View drawerView, float slideOffset) {
4165 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4166 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4167 mListViewForAnimating.setAlpha(slideOffset);
4168 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004169
4170 // This code handles when to change the visibility of action items
4171 // based on drawer state. The basic logic is that right when we
4172 // open the drawer, we hide the action items. We show the action items
4173 // when the drawer closes. However, due to the animation of the drawer closing,
4174 // to make the reshowing of the action items feel right, we make the items visible
4175 // slightly sooner.
4176 //
4177 // However, to make the animating behavior work properly, we have to know whether
4178 // we're animating open or closed. Only if we're animating closed do we want to
4179 // show the action items early. We save the last slide offset so that we can compare
4180 // the current slide offset to it to determine if we're opening or closing.
4181 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4182 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4183 mHideMenuItems = false;
4184 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004185 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004186 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4187 mHideMenuItems = true;
4188 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004189 disableCabMode();
Tony Mantlerb29890a2013-07-30 15:12:05 -07004190 final FolderListFragment folderListFragment = getFolderListFragment();
4191 if (folderListFragment != null) {
4192 folderListFragment.updateScroll();
4193 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004194 }
4195 } else {
4196 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4197 mHideMenuItems = false;
4198 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004199 maybeEnableCabMode();
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004200 } else if (!mHideMenuItems && slideOffset > 0.f) {
4201 mHideMenuItems = true;
4202 mActivity.invalidateOptionsMenu();
Tony Mantler43fab322013-07-26 16:38:35 -07004203 disableCabMode();
Tony Mantlerb29890a2013-07-30 15:12:05 -07004204 final FolderListFragment folderListFragment = getFolderListFragment();
4205 if (folderListFragment != null) {
4206 folderListFragment.updateScroll();
4207 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004208 }
4209 }
4210
4211 mOldSlideOffset = slideOffset;
Scott Kennedy10c99ef2013-08-29 13:44:17 -07004212
4213 // If we're sliding, we always want to show the burger
4214 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */);
Andy Huang12b3ee42013-04-24 22:49:43 -07004215 }
4216
4217 /**
4218 * This condition here should only be called when the drawer is stuck in a weird state
4219 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4220 * and, more importantly, unlock the drawer when this is the case.
4221 */
4222 @Override
4223 public void onDrawerStateChanged(int newState) {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004224 mDrawerState = newState;
4225 mDrawerToggle.onDrawerStateChanged(mDrawerState);
4226 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4227 if (mHasNewAccountOrFolder) {
4228 refreshDrawer();
4229 }
4230 if (mConversationListLoadFinishedIgnored) {
4231 mConversationListLoadFinishedIgnored = false;
4232 final Bundle args = new Bundle();
4233 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4234 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4235 mActivity.getLoaderManager().initLoader(
4236 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4237 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004238 }
4239 }
4240
4241 /**
4242 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4243 * conversation list, and finish end actions. Also, make
4244 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4245 */
4246 public void refreshDrawer() {
4247 mHasNewAccountOrFolder = false;
4248 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4249 ConversationListFragment conversationList = getConversationListFragment();
4250 if (conversationList != null) {
4251 conversationList.clear();
4252 }
4253 mDrawerObservers.notifyChanged();
4254 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004255
4256 /**
4257 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4258 * by {@link #onDrawerStateChanged(int)}.
4259 * @return The {@link DrawerLayout}'s current state. One of
4260 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4261 * or {@link DrawerLayout#STATE_SETTLING}.
4262 */
4263 public int getDrawerState() {
4264 return mDrawerState;
4265 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004266 }
4267
Scott Kennedy8a72b852013-05-02 14:18:50 -07004268 @Override
4269 public boolean isDrawerPullEnabled() {
Scott Kennedy2b254dc2013-07-10 11:40:27 -07004270 return getShouldAllowDrawerPull(mViewMode.getMode());
Scott Kennedy8a72b852013-05-02 14:18:50 -07004271 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004272
4273 @Override
4274 public boolean shouldHideMenuItems() {
4275 return mHideMenuItems;
4276 }
Scott Kennedyad418142013-07-10 17:18:22 -07004277
4278 protected void navigateUpFolderHierarchy() {
4279 new AsyncTask<Void, Void, Folder>() {
4280 @Override
4281 protected Folder doInBackground(final Void... params) {
4282 if (mInbox == null) {
4283 // We don't have an inbox, but we need it
4284 final Cursor cursor = mContext.getContentResolver().query(
4285 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null,
4286 null, null);
4287
4288 if (cursor != null) {
4289 try {
4290 if (cursor.moveToFirst()) {
4291 mInbox = new Folder(cursor);
4292 }
4293 } finally {
4294 cursor.close();
4295 }
4296 }
4297 }
4298
4299 // Now try to load our parent
4300 final Folder folder;
4301
Scott Kennedya776c982013-08-29 11:17:13 -07004302 if (mFolder != null) {
4303 final Cursor cursor = mContext.getContentResolver().query(mFolder.parent,
4304 UIProvider.FOLDERS_PROJECTION, null, null, null);
Scott Kennedyad418142013-07-10 17:18:22 -07004305
Scott Kennedya776c982013-08-29 11:17:13 -07004306 if (cursor == null) {
4307 // We couldn't load the parent, so use the inbox
4308 folder = mInbox;
4309 } else {
4310 try {
4311 cursor.moveToFirst();
4312 folder = new Folder(cursor);
4313 } finally {
4314 cursor.close();
4315 }
Scott Kennedyad418142013-07-10 17:18:22 -07004316 }
Scott Kennedya776c982013-08-29 11:17:13 -07004317 } else {
4318 folder = mInbox;
Scott Kennedyad418142013-07-10 17:18:22 -07004319 }
4320
4321 return folder;
4322 }
4323
4324 @Override
4325 protected void onPostExecute(final Folder result) {
4326 onFolderSelected(result);
4327 }
4328 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
4329 }
Scott Kennedyf77806e2013-08-30 11:38:15 -07004330
4331 @Override
4332 public Parcelable getConversationListScrollPosition(final String folderUri) {
4333 return mConversationListScrollPositions.getParcelable(folderUri);
4334 }
4335
4336 @Override
4337 public void setConversationListScrollPosition(final String folderUri,
4338 final Parcelable savedPosition) {
4339 mConversationListScrollPositions.putParcelable(folderUri, savedPosition);
4340 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004341}