blob: cf38843479a762ee246a293f8a79a5b26dba0878 [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;
Andy Huang632721e2012-04-11 16:57:26 -070041import android.database.DataSetObservable;
42import android.database.DataSetObserver;
Paul Westbrook23b74b92012-02-29 11:36:12 -080043import android.net.Uri;
Vikram Aggarwal27d89ad2012-06-12 13:38:40 -070044import android.os.AsyncTask;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080045import android.os.Bundle;
Mindy Pereira21ab4902012-03-19 18:48:03 -070046import android.os.Handler;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070047import android.provider.SearchRecentSuggestions;
Andy Huang12b3ee42013-04-24 22:49:43 -070048import android.support.v4.app.ActionBarDrawerToggle;
49import android.support.v4.widget.DrawerLayout;
Mindy Pereiraacf60392012-04-06 09:11:00 -070050import android.view.DragEvent;
Andy Huang12b3ee42013-04-24 22:49:43 -070051import android.view.Gravity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080052import android.view.KeyEvent;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080053import android.view.LayoutInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080054import android.view.Menu;
Mindy Pereira28d5f722012-02-15 12:32:40 -080055import android.view.MenuInflater;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080056import android.view.MenuItem;
57import android.view.MotionEvent;
Mindy Pereirad33674992012-06-25 16:26:30 -070058import android.view.View;
Andy Huang12b3ee42013-04-24 22:49:43 -070059import android.widget.ListView;
Mindy Pereirafd0c2972012-03-27 13:50:39 -070060import android.widget.Toast;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -080061
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -080062import com.android.mail.ConversationListContext;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -080063import com.android.mail.MailLogService;
Andy Huangf9a73482012-03-13 15:54:02 -070064import com.android.mail.R;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -080065import com.android.mail.browse.ConfirmDialogFragment;
Mindy Pereira967ede62012-03-22 09:29:09 -070066import com.android.mail.browse.ConversationCursor;
Yu Ping Hu7c909c72013-01-18 11:58:01 -080067import com.android.mail.browse.ConversationCursor.ConversationOperation;
mindypca87de42012-09-28 15:02:39 -070068import com.android.mail.browse.ConversationItemViewModel;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070069import com.android.mail.browse.ConversationMessage;
Paul Westbrookbf232c32012-04-18 03:17:41 -070070import com.android.mail.browse.ConversationPagerController;
Marc Blank7c9f6ac2012-04-02 13:27:19 -070071import com.android.mail.browse.SelectedConversationsActionMenu;
Andy Huang991f4532012-08-14 13:32:55 -070072import com.android.mail.browse.SyncErrorDialogFragment;
Mindy Pereira9b875682012-02-15 18:10:54 -080073import com.android.mail.compose.ComposeActivity;
Vikram Aggarwal177097f2013-03-08 11:19:53 -080074import com.android.mail.content.CursorCreator;
75import com.android.mail.content.ObjectCursor;
76import com.android.mail.content.ObjectCursorLoader;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080077import com.android.mail.providers.Account;
Mindy Pereira9b875682012-02-15 18:10:54 -080078import com.android.mail.providers.Conversation;
Andy Huang839ada22012-07-20 15:48:40 -070079import com.android.mail.providers.ConversationInfo;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -080080import com.android.mail.providers.Folder;
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -070081import com.android.mail.providers.FolderWatcher;
Paul Westbrookc2074c42012-03-22 15:26:58 -070082import com.android.mail.providers.MailAppProvider;
Mindy Pereiradac00542012-03-01 10:50:33 -080083import com.android.mail.providers.Settings;
Vikram Aggarwale620a7a2012-03-28 13:16:14 -070084import com.android.mail.providers.SuggestionsProvider;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -080085import com.android.mail.providers.UIProvider;
Mindy Pereira3cb28b52012-05-24 15:26:39 -070086import com.android.mail.providers.UIProvider.AccountCapabilities;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070087import com.android.mail.providers.UIProvider.AccountColumns;
Paul Westbrook2388c5d2012-03-25 12:29:11 -070088import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -070089import com.android.mail.providers.UIProvider.AutoAdvance;
Mindy Pereirac9d59182012-03-22 16:06:46 -070090import com.android.mail.providers.UIProvider.ConversationColumns;
Paul Westbrook5109c512012-11-05 11:00:30 -080091import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -070092import com.android.mail.providers.UIProvider.FolderCapabilities;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -070093import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
Andy Huang839ada22012-07-20 15:48:40 -070094import com.android.mail.utils.ContentProviderTask;
Andy Huang144bfe72013-06-11 13:27:52 -070095import com.android.mail.utils.DrawIdler;
Paul Westbrookb334c902012-06-25 11:42:46 -070096import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080097import com.android.mail.utils.LogUtils;
Scott Kennedycb85aea2013-02-25 13:08:32 -080098import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -070099import com.android.mail.utils.Observable;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800100import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800101import com.android.mail.utils.VeiledAddressMatcher;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -0700102
Paul Westbrookca08fc12012-07-31 12:01:15 -0700103import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700104import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700105import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700106import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800107
Marc Blank167faa82012-03-21 13:11:53 -0700108import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800109import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700110import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700111import java.util.Collections;
Andy Huangc1fb9a92013-02-11 13:09:12 -0800112import java.util.Deque;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700113import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700114import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800115import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700116import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800117
118
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800119/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800120 * This is an abstract implementation of the Activity Controller. This class
121 * knows how to respond to menu items, state changes, layout changes, etc. It
122 * weaves together the views and listeners, dispatching actions to the
123 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800124 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800125 * Even though this class is abstract, it should provide default implementations
126 * for most, if not all the methods in the ActivityController interface. This
127 * makes the task of the subclasses easier: OnePaneActivityController and
128 * TwoPaneActivityController can be concise when the common functionality is in
129 * AbstractActivityController.
130 * </p>
131 * <p>
132 * In the Gmail codebase, this was called BaseActivityController
133 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800134 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700135public abstract class AbstractActivityController implements ActivityController,
136 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800137 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700138 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800139 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700140 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700141 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700142 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700143 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700144 /** Tag for {@link #mSelectedSet} */
145 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700146 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700147 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700148 /** Tag for {@link #mFolderListFolder} */
149 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700150 /** Tag for {@link ConversationListContext#searchQuery} */
151 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800152 /** Tag for {@link #mDialogAction} */
153 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800154 /** Tag for {@link #mDialogFromSelectedSet} */
155 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800156 /** Tag for {@link #mDetachedConvUri} */
157 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800158
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700159 /** Tag used when loading a wait fragment */
160 protected static final String TAG_WAIT = "wait-fragment";
161 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700162 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700163 /** Tag used when loading a folder list fragment. */
164 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
165
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700166 /** Key to store an account in a bundle */
167 private final String BUNDLE_ACCOUNT_KEY = "account";
168 /** Key to store a folder in a bundle */
169 private final String BUNDLE_FOLDER_KEY = "folder";
170
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800171 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700172 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700173 /** True when {@link #mFolder} is first shown to the user. */
174 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700175 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700176 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800177 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700178 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800179 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800180 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800181 protected Conversation mCurrentConversation;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800182 /**
183 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
184 */
185 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800186
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700187 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
188 private SuppressNotificationReceiver mNewEmailReceiver = null;
189
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800190 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700191 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700192
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800193 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800194 * The current mode of the application. All changes in mode are initiated by
195 * the activity controller. View mode changes are propagated to classes that
196 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800197 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800198 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800199 protected ContentResolver mResolver;
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -0700200 protected boolean mHaveAccountList = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800201 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800202
Andy Huang4e0158f2012-08-07 21:06:01 -0700203 private boolean mDestroyed;
204
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800205 /** True if running on tablet */
206 private final boolean mIsTablet;
207
Andy Huang1ee96b22012-08-24 20:19:53 -0700208 /**
209 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
210 * transactions? (including back stack manipulation)
211 * <p>
212 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
213 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
214 * and onResume.
215 */
216 private boolean mSafeToModifyFragments = true;
217
Paul Westbrook23b74b92012-02-29 11:36:12 -0800218 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700219 protected ConversationCursor mConversationListCursor;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700220 private final DataSetObservable mConversationListObservable = new Observable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700221
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800222 /** Runnable that checks the logging level to enable/disable the logging service. */
223 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700224 /** List of all accounts currently known to the controller. This is never null. */
225 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800226
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700227 private FolderWatcher mFolderWatcher;
228
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800229 /**
230 * Interface for actions that are deferred until after a load completes. This is for handling
231 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
232 * that cursor is loaded.
233 */
234 private interface LoadFinishedCallback {
235 void onLoadFinished();
236 }
237
238 /** The deferred actions to execute when mConversationListCursor load completes. */
239 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
240 new ArrayList<LoadFinishedCallback>();
241
Marc Blankbf128eb2012-04-18 15:58:45 -0700242 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700243
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700244 /** Listeners that are interested in changes to the current account. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700245 private final DataSetObservable mAccountObservers = new Observable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700246 /** Listeners that are interested in changes to the recent folders. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700247 private final DataSetObservable mRecentFolderObservers = new Observable("RecentFolder");
248 /** Listeners that are interested in changes to the list of all accounts. */
249 private final DataSetObservable mAllAccountObservers = new Observable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700250 /** Listeners that are interested in changes to the current folder. */
251 private final DataSetObservable mFolderObservable = new Observable("CurrentFolder");
Rohan Shah0f73d902013-04-19 17:06:37 -0700252 /** Listeners that are interested in changes to the drawer state. */
253 private final DataSetObservable mDrawerObservers = new Observable("Drawer");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700254
Mindy Pereira967ede62012-03-22 09:29:09 -0700255 /**
256 * Selected conversations, if any.
257 */
Andy Huang4556a442012-03-30 16:42:05 -0700258 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800259
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700260 private final int mFolderItemUpdateDelayMs;
261
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700262 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700263 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700264
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700265 /**
266 * Action menu associated with the selected set.
267 */
268 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700269 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700270 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700271
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700272 // This is split out from the general loader dispatcher because its loader doesn't return a
Andy Huangb1c34dc2012-04-17 16:36:19 -0700273 // basic Cursor
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700274 /** Handles loader callbacks to create a convesation cursor. */
Andy Huangb1c34dc2012-04-17 16:36:19 -0700275 private final ConversationListLoaderCallbacks mListCursorCallbacks =
276 new ConversationListLoaderCallbacks();
277
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800278 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
279 private final FolderLoads mFolderCallbacks = new FolderLoads();
280 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
281 private final AccountLoads mAccountCallbacks = new AccountLoads();
282
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800283 /**
284 * Matched addresses that must be shielded from users because they are temporary. Even though
285 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
286 * to live past the life of an account.
287 */
288 private final VeiledAddressMatcher mVeiledMatcher;
289
Paul Westbrookb334c902012-06-25 11:42:46 -0700290 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700291
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700292 // Loader constants: Accounts
293 /**
294 * The list of accounts. This loader is started early in the application life-cycle since
295 * the list of accounts is central to all other data the application needs: unread counts for
296 * folders, critical UI settings like show/hide checkboxes, ...
297 * The loader is started when the application is created: both in
298 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never
299 * destroyed since the cursor is needed through the life of the application. When the list of
300 * accounts changes, we notify {@link #mAllAccountObservers}.
301 */
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800302 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700303
304 /**
305 * The current account. This loader is started when we have an account. The mail application
306 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount},
307 * we start a loader to observe for changes on the current account.
308 * The loader is always restarted when an account is set in {@link #setAccount(Account)}.
309 * When the current account object changes, we notify {@link #mAccountObservers}.
310 * A possible performance improvement would be to listen purely on
311 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list,
312 * and would avoid two updates when a single setting on the current account changes.
313 */
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700314 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700315
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700316 // Loader constants: Folders
317 /** The current folder. This loader watches for updates to the current folder in a manner
318 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder
319 * might be due to server-side changes (unread count), or local changes (sync window or sync
320 * status change).
321 * The change of current folder calls {@link #updateFolder(Folder)}.
322 * This is responsible for restarting a loader using the URI of the provided folder. When the
323 * loader returns, the current folder is updated and consumers, if any, are notified.
324 * When the current folder changes, we notify {@link #mFolderObservable}
325 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700326 private static final int LOADER_FOLDER_CURSOR = 2;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700327 /**
328 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent
329 * folders are tied to the current account being viewed. When the account is changed,
330 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for
331 * phones historically, when they were displayed in the spinner. On the tablet,
332 * they showed in the {@link FolderListFragment} and were not-populated. The code to
333 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of
334 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then
335 * does the appropriate thing to populate recent folders, and notify of a change on the cursor.
336 * Recent folders are needed for the life of the current account.
337 * When the recent folders change, we notify {@link #mRecentFolderObservers}.
338 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700339 private static final int LOADER_RECENT_FOLDERS = 3;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700340 /**
341 * The primary inbox for the current account. The mechanism to load the default inbox for the
342 * current account is (sadly) different from loading other folders. The method
343 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns
344 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually
345 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR}
346 * over the current folder.
347 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical.
348 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700349 private static final int LOADER_ACCOUNT_INBOX = 5;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700350 /**
351 * The fake folder of search results for a term. When we search for a term,
352 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity,
353 * we start a loader which returns conversations that match the user-provided query.
354 * We destroy the loader when we obtain a valid cursor since subsequent searches will create
355 * a new activity.
356 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700357 private static final int LOADER_SEARCH = 6;
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700358 /**
359 * The initial folder at app start. When the application is launched from an intent that
360 * specifies the initial folder (notifications/widgets/shortcuts),
361 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since
362 * shortcuts and widgets persist past application update, they might have incorrect
363 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri},
364 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed.
365 * An additional complication arises if we have to view a specific conversation within this
366 * folder. This is the case when launching the app from a single conversation notification
367 * or tapping on a specific conversation in the widget. In these cases, the conversation is
368 * saved in {@link #mConversationToShow} and is retrieved when the loader returns.
369 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800370 public static final int LOADER_FIRST_FOLDER = 8;
371
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700372 // Loader constants: Conversations
373 /** The conversation cursor over the current conversation list. This loader provides
374 * a cursor over conversation entries from a folder to display a conversation
375 * list.
376 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)},
377 * or when the controller is told that a folder/account change is imminent
378 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of
379 * the current folder. When the user switches folders, the old loader is destroyed and a new
380 * one is created.
381 *
382 * When the conversation list changes, we notify {@link #mConversationListObservable}.
383 */
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700384 private static final int LOADER_CONVERSATION_LIST = 4;
385
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700386 /**
387 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
388 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
389 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
390 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
391 * other class that uses this activity's LoaderManager. If another class needs activity-level
392 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
393 * perhaps.
394 */
395 public static final int LAST_LOADER_ID = 100;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800396 /**
397 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
398 * fragments, and within an activity, loader IDs need to be unique. Currently,
399 * {@link SectionedInboxTeaserView} is the only class that uses the
400 * {@link ConversationListFragment}'s LoaderManager.
401 */
402 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800403
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700404 /** Code returned after an account has been added. */
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700405 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Vikram Aggarwal6aca6892013-06-04 13:53:27 -0700406 /** Code returned when the user has to enter the new password on an existing account. */
Paul Westbrook122f7c22012-08-20 17:50:31 -0700407 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700408
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700409 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
410 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700411 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700412 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700413 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700414 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700415 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700416 /** A wait fragment we added, if any. */
417 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800418 /** True if we have results from a search query */
419 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800420 /** If a confirmation dialog is being show, the listener for the positive action. */
421 private OnClickListener mDialogListener;
422 /**
423 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
424 * is used to create a new {@link #mDialogListener} on orientation changes.
425 */
426 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800427 /**
428 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
429 * and false if it acts on the currently selected conversation
430 */
431 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800432
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800433 /** Which conversation to show, if started from widget/notification. */
434 private Conversation mConversationToShow = null;
435
Andy Huangc94d07f2013-06-03 16:19:35 -0700436 /**
437 * A temporary reference to the pending destructive action that was deferred due to an
438 * auto-advance transition in progress.
439 * <p>
440 * In detail: when auto-advance triggers a mode change, we must wait until the transition
441 * completes before executing the destructive action to ensure a smooth mode change transition.
442 * This member variable houses the pending destructive action work to be run upon completion.
443 */
444 private Runnable mAutoAdvanceOp = null;
445
Andy Huangc1fb9a92013-02-11 13:09:12 -0800446 private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
447
Andy Huang12b3ee42013-04-24 22:49:43 -0700448 protected DrawerLayout mDrawerContainer;
449 protected View mDrawerPullout;
450 protected ActionBarDrawerToggle mDrawerToggle;
451 protected ListView mListViewForAnimating;
452 protected boolean mHasNewAccountOrFolder;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700453 private boolean mConversationListLoadFinishedIgnored;
454 protected MailDrawerListener mDrawerListener;
Andrew Sapperstein5747e152013-05-13 14:13:08 -0700455 private boolean mHideMenuItems;
Andy Huang12b3ee42013-04-24 22:49:43 -0700456
Andy Huang144bfe72013-06-11 13:27:52 -0700457 private final DrawIdler mDrawIdler = new DrawIdler();
458
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700459 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700460
Scott Kennedycb85aea2013-02-25 13:08:32 -0800461 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
462 @Override
463 public void onChanged() {
464 super.onChanged();
465
466 if (mConversationListCursor != null) {
467 mConversationListCursor.handleNotificationActions();
468 }
469 }
470 };
471
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800472 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
473 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700474 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800475 mViewMode = viewMode;
476 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700477 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700478 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700479 // Allow the fragment to observe changes to its own selection set. No other object is
480 // aware of the selected set.
481 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700482
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800483 final Resources r = mContext.getResources();
484 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
485 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800486 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800487 mIsTablet = Utils.useTabletUI(r);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -0700488 mConversationListLoadFinishedIgnored = false;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800489 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800490
491 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800492 public Account getCurrentAccount() {
493 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800494 }
495
496 @Override
497 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800498 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800499 }
500
501 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800502 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700503 final int mode = mViewMode.getMode();
504 final int helpContextResId;
505 switch (mode) {
506 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
507 helpContextResId = R.string.wait_help_context;
508 break;
509 default:
510 helpContextResId = R.string.main_help_context;
511 }
512 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800513 }
514
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800515 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700516 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700517 return mConversationListCursor;
518 }
519
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700520 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700521 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800522 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700523 * @return true if the fragment is valid, false otherwise
524 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800525 private static boolean isValidFragment(Fragment in) {
526 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700527 }
528
529 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700530 * Get the conversation list fragment for this activity. If the conversation list fragment is
531 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700532 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700533 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
534 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
535 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
536 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
537 * need the fragment immediately after adding it, consider making the fragment an observer of
538 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700539 */
540 protected ConversationListFragment getConversationListFragment() {
541 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700542 if (isValidFragment(fragment)) {
543 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700544 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700545 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700546 }
547
548 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700549 * Returns the folder list fragment attached with this activity. If no such fragment is attached
550 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700551 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700552 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
553 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
554 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
555 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
556 * need the fragment immediately after adding it, consider making the fragment an observer of
557 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700558 */
559 protected FolderListFragment getFolderListFragment() {
560 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700561 if (isValidFragment(fragment)) {
562 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700563 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700564 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700565 }
566
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800567 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800568 * Initialize the action bar. This is not visible to OnePaneController and
569 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800570 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700571 private void initializeActionBar() {
572 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700573 if (actionBar == null) {
574 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700575 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700576
577 // be sure to inherit from the ActionBar theme when inflating
578 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700579 final boolean isSearch = mActivity.getIntent() != null
580 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
581 mActionBarView = (MailActionBarView) inflater.inflate(
582 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Vikram Aggarwaldac89fa2013-03-05 16:43:09 -0800583 mActionBarView.initialize(mActivity, this, actionBar);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700584
Andy Huang12b3ee42013-04-24 22:49:43 -0700585 // init the action bar to allow the 'up' affordance.
586 // any configurations that disallow 'up' should do that later.
587 mActionBarView.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700588 }
589
590 /**
591 * Attach the action bar to the activity.
592 */
593 private void attachActionBar() {
594 final ActionBar actionBar = mActivity.getActionBar();
595 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800596 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal26b0bfd2013-03-29 13:05:08 -0700597 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700598 // Show a custom view and home icon, keep the title and subttitle
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700599 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
600 | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700601 actionBar.setDisplayOptions(mask, mask);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800602 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700603 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800604 }
605
606 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800607 * Returns whether the conversation list fragment is visible or not.
608 * Different layouts will have their own notion on the visibility of
609 * fragments, so this method needs to be overriden.
610 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800611 */
612 protected abstract boolean isConversationListVisible();
613
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700614 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700615 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700616 */
617 final void perhapsEnterWaitMode() {
618 // If the account is not initialized, then show the wait fragment, since nothing can be
619 // shown.
620 if (mAccount.isAccountInitializationRequired()) {
621 showWaitForInitialization();
622 return;
623 }
624
625 final boolean inWaitingMode = inWaitMode();
626 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
627 if (isSyncRequired) {
628 if (inWaitingMode) {
629 // Update the WaitFragment's account object
630 updateWaitMode();
631 } else {
632 // Transition to waiting mode
633 showWaitForInitialization();
634 }
635 } else if (inWaitingMode) {
636 // Dismiss waiting mode
637 hideWaitForInitialization();
638 }
639 }
640
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800641 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700642 public void switchToDefaultInboxOrChangeAccount(Account account) {
643 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700644 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700645 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Vikram Aggarwald70fe492013-06-04 12:52:07 -0700646 // If the active account has been clicked in the drawer, go to default inbox
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700647 if (switchToDefaultInbox) {
648 loadAccountInbox();
649 return;
650 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700651 changeAccount(account);
652 }
653
654 @Override
655 public void changeAccount(Account account) {
656 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
657 // Is the account or account settings different from the existing account?
658 final boolean firstLoad = mAccount == null;
659 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
660
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800661 // If nothing has changed, return early without wasting any more time.
662 if (!accountChanged && !account.settingsDiffer(mAccount)) {
663 return;
664 }
665 // We also don't want to do anything if the new account is null
666 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700667 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800668 return;
669 }
670 final String accountName = account.name;
671 mHandler.post(new Runnable() {
672 @Override
673 public void run() {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800674 MailActivity.setNfcMessage(accountName);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700675 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800676 });
677 if (accountChanged) {
678 commitDestructiveActions(false);
679 }
680 // Change the account here
681 setAccount(account);
682 // And carry out associated actions.
683 cancelRefreshTask();
684 if (accountChanged) {
685 loadAccountInbox();
686 }
687 // Check if we need to force setting up an account before proceeding.
688 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
689 // Launch the intent!
690 final Intent intent = new Intent(Intent.ACTION_EDIT);
691 intent.setData(mAccount.settings.setupIntentUri);
692 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800693 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800694 }
695
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700696 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700697 * Adds a listener interested in change in the current account. If a class is storing a
698 * reference to the current account, it should listen on changes, so it can receive updates to
699 * settings. Must happen in the UI thread.
700 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800701 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700702 public void registerAccountObserver(DataSetObserver obs) {
703 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800704 }
705
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700706 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700707 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700708 * Must happen in the UI thread.
709 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700710 @Override
711 public void unregisterAccountObserver(DataSetObserver obs) {
712 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700713 }
714
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700715 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700716 public void registerAllAccountObserver(DataSetObserver observer) {
717 mAllAccountObservers.registerObserver(observer);
718 }
719
720 @Override
721 public void unregisterAllAccountObserver(DataSetObserver observer) {
722 mAllAccountObservers.unregisterObserver(observer);
723 }
724
725 @Override
726 public Account[] getAllAccounts() {
727 return mAllAccounts;
728 }
729
730 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700731 public Account getAccount() {
732 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700733 }
734
Rohan Shah0f73d902013-04-19 17:06:37 -0700735 @Override
736 public void registerDrawerClosedObserver(final DataSetObserver observer) {
737 mDrawerObservers.registerObserver(observer);
738 }
739
740 @Override
741 public void unregisterDrawerClosedObserver(final DataSetObserver observer) {
742 mDrawerObservers.unregisterObserver(observer);
743 }
744
745 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700746 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
747 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
748 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700749 */
750 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700751 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
752 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700753 if (!isDrawerEnabled()) {
754 mDrawerObservers.notifyChanged();
755 return;
756 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700757 // If there are no new folders or accounts to switch to, just close the drawer
758 if (!hasNewFolderOrAccount) {
759 mDrawerContainer.closeDrawers();
760 return;
761 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700762 // Otherwise, start preloading the conversation list for the new folder.
763 if (nextFolder != null) {
764 preloadConvList(nextAccount, nextFolder);
765 }
766 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700767 final ConversationListFragment conversationList = getConversationListFragment();
768 if (conversationList != null) {
769 mListViewForAnimating = conversationList.getListView();
770 } else {
771 // There is no conversation list to animate, so just set it to null
772 mListViewForAnimating = null;
773 }
774
775 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
776 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
777 mHasNewAccountOrFolder = true;
778 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
779 } else {
780 // Drawer is already closed, notify observers that is the case.
781 mDrawerObservers.notifyChanged();
782 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700783 }
784
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700785 /**
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700786 * Load the conversation list early for the given folder. This happens when some UI element
787 * (usually the drawer) instructs the controller that an account change or folder change is
788 * imminent. While the UI element is animating, the controller can preload the conversation
789 * list for the default inbox of the account provided here or to the folder provided here.
790 *
791 * @param nextAccount The account which the app will switch to shortly, possibly null.
792 * @param nextFolder The folder which the app will switch to shortly, possibly null.
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700793 */
794 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
795 // Fire off the conversation list loader for this account already with a fake
796 // listener.
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700797 final Bundle args = new Bundle(2);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700798 if (nextAccount != null) {
799 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
800 } else {
801 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
802 }
803 if (nextFolder != null) {
804 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700805 } else {
806 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700807 }
808 mFolder = null;
809 final LoaderManager lm = mActivity.getLoaderManager();
810 lm.destroyLoader(LOADER_CONVERSATION_LIST);
811 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
812 }
813
Vikram Aggarwalaa941d72013-06-04 15:34:28 -0700814 /**
815 * Initiates the async request to create a fake search folder, which returns conversations that
816 * match the query term provided by the user. Returns immediately.
817 * @param intent Intent that the app was started with. This intent contains the search query.
818 */
Mindy Pereirae0828392012-03-08 10:38:40 -0800819 private void fetchSearchFolder(Intent intent) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700820 final Bundle args = new Bundle(1);
Mindy Pereiraab486362012-03-21 18:18:53 -0700821 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800822 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800823 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800824 }
825
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800826 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800827 public void onFolderChanged(Folder folder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700828 mDrawerContainer.closeDrawers();
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700829 changeFolder(folder, null);
830 }
831
832 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700833 * Sets the folder state without changing view mode and without creating a list fragment, if
834 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800835 * @param folder the folder whose list of conversations are to be shown
836 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700837 */
838 private void setListContext(Folder folder, String query) {
839 updateFolder(folder);
840 if (query != null) {
841 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
842 } else {
843 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
844 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700845 cancelRefreshTask();
846 }
847
848 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700849 * Changes the folder to the value provided here. This causes the view mode to change.
850 * @param folder the folder to change to
851 * @param query if non-null, this represents the search string that the folder represents.
852 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800853 private void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700854 if (!Objects.equal(mFolder, folder)) {
855 commitDestructiveActions(false);
856 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700857 if (folder != null && !folder.equals(mFolder)
858 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700859 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800860 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700861 // Touch the current folder: it is different, and it has been accessed.
862 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800863 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800864 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800865 }
866
Mindy Pereira13c12a62012-05-31 15:41:08 -0700867 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700868 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700869 onFolderChanged(folder);
870 }
871
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700872 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700873 * Adds a listener interested in change in the recent folders. If a class is storing a
874 * reference to the recent folders, it should listen on changes, so it can receive updates.
875 * Must happen in the UI thread.
876 */
877 @Override
878 public void registerRecentFolderObserver(DataSetObserver obs) {
879 mRecentFolderObservers.registerObserver(obs);
880 }
881
882 /**
883 * Removes a listener from receiving recent folder changes.
884 * Must happen in the UI thread.
885 */
886 @Override
887 public void unregisterRecentFolderObserver(DataSetObserver obs) {
888 mRecentFolderObservers.unregisterObserver(obs);
889 }
890
891 @Override
892 public RecentFolderList getRecentFolders() {
893 return mRecentFolderList;
894 }
895
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700896 @Override
897 public void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700898 boolean handled = false;
899 if (mFolderWatcher != null) {
900 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
901 if (inbox != null) {
902 onFolderChanged(inbox);
903 handled = true;
904 }
905 }
906 if (!handled) {
907 LogUtils.w(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
908 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
909 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700910 final int mode = mViewMode.getMode();
911 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700912 mViewMode.enterConversationListMode();
913 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700914 }
915
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700916 @Override
917 public void setFolderWatcher(FolderWatcher watcher) {
918 mFolderWatcher = watcher;
919 }
920
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700921 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700922 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
923 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
924 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800925 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700926 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800927 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700928 // We should never try to assign a null folder. But in the rare event that we do, we should
929 // only set the bit when we have a valid folder, and null is not valid.
930 if (newFolder == null) {
931 return;
932 }
933 // If the previous folder was null, or if the two folders represent different data, then we
934 // consider that the folder has changed.
935 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
936 mFolderChanged = true;
937 }
938 }
939
940 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700941 * Sets the current folder if it is different from the object provided here. This method does
942 * NOT notify the folder observers that a change has happened. Observers are notified when we
943 * get an updated folder from the loaders, which will happen as a consequence of this method
944 * (since this method starts/restarts the loaders).
945 * @param folder The folder to assign
946 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700947 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700948 if (folder == null || !folder.isInitialized()) {
949 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
950 return;
951 }
952 if (folder.equals(mFolder)) {
953 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
954 return;
955 }
956 final boolean wasNull = mFolder == null;
957 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
958 final LoaderManager lm = mActivity.getLoaderManager();
959 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
960 // ensure that the folder is different from the previous folder before marking the
961 // folder changed.
962 setHasFolderChanged(folder);
963 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700964
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700965 // We do not need to notify folder observers yet. Instead we start the loaders and
966 // when the load finishes, we will get an updated folder. Then, we notify the
967 // folderObservers in onLoadFinished.
968 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700969
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700970 // Only when we switch from one folder to another do we want to restart the
971 // folder and conversation list loaders (to trigger onCreateLoader).
972 // The first time this runs when the activity is [re-]initialized, we want to re-use the
973 // previous loader's instance and data upon configuration change (e.g. rotation).
974 // If there was not already an instance of the loader, init it.
975 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800976 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700977 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800978 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700979 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700980 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
981 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700982 // folders, we want to restart the loader to get the information
983 // for the newly selected folder
984 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800985 }
Paul Westbrooke2be97a2013-05-21 02:12:09 -0700986 final Bundle args = new Bundle(2);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700987 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700988 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
989 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800990 }
991
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800992 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700993 public Folder getFolder() {
994 return mFolder;
995 }
996
997 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700998 public Folder getHierarchyFolder() {
999 return mFolderListFolder;
1000 }
1001
1002 @Override
1003 public void setHierarchyFolder(Folder folder) {
1004 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001005 }
1006
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001007 /**
1008 * The mail activity calls other activities for two specific reasons:
1009 * <ul>
1010 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li>
1011 * <li>To update the password on a current account. The result {@link
1012 * #REAUTHENTICATE_REQUEST_CODE} is received.</li>
1013 * </ul>
1014 * @param requestCode
1015 * @param resultCode
1016 * @param data
1017 */
Mindy Pereira23aadfd2012-05-25 11:24:33 -07001018 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001019 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07001020 switch (requestCode) {
1021 case ADD_ACCOUNT_REQUEST_CODE:
1022 // We were waiting for the user to create an account
1023 if (resultCode == Activity.RESULT_OK) {
1024 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001025 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1026 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -07001027 } else {
1028 // The user failed to create an account, just exit the app
1029 mActivity.finish();
1030 }
1031 break;
1032 case REAUTHENTICATE_REQUEST_CODE:
1033 if (resultCode == Activity.RESULT_OK) {
1034 // The user successfully authenticated, attempt to refresh the list
1035 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
1036 if (refreshUri != null) {
1037 startAsyncRefreshTask(refreshUri);
1038 }
1039 }
1040 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -07001041 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001042 }
1043
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001044 /**
1045 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001046 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001047 */
1048 protected synchronized void informCursorVisiblity(boolean visible) {
1049 if (mConversationListCursor != null) {
1050 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
1051 // We have informed the cursor. Subsequent visibility changes should not tell it that
1052 // the folder has changed.
1053 mFolderChanged = false;
1054 }
1055 }
1056
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001057 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001058 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001059 informCursorVisiblity(visible);
Andy Huangc94d07f2013-06-03 16:19:35 -07001060 commitAutoAdvanceOperation();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001061 }
1062
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001063 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001064 * Called when a conversation is visible. Child classes must call the super class implementation
1065 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001066 */
1067 @Override
1068 public void onConversationVisibilityChanged(boolean visible) {
Andy Huangc94d07f2013-06-03 16:19:35 -07001069 commitAutoAdvanceOperation();
1070 }
1071
1072 /**
1073 * Commits any pending destructive action that was earlier deferred by an auto-advance
1074 * mode-change transition.
1075 */
1076 private void commitAutoAdvanceOperation() {
1077 if (mAutoAdvanceOp != null) {
1078 mAutoAdvanceOp.run();
1079 mAutoAdvanceOp = null;
1080 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001081 }
1082
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001083 /**
1084 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
1085 * to turn it on for shipped versions.
1086 */
1087 private void initializeDevLoggingService() {
1088 if (!MailLogService.DEBUG_ENABLED) {
1089 return;
1090 }
1091 // Check every 5 minutes.
1092 final int WAIT_TIME = 5 * 60 * 1000;
1093 // Start a runnable that periodically checks the log level and starts/stops the service.
1094 mLogServiceChecker = new Runnable() {
1095 /** True if currently logging. */
1096 private boolean mCurrentlyLogging = false;
1097
1098 /**
1099 * If the logging level has been changed since the previous run, start or stop the
1100 * service.
1101 */
1102 private void startOrStopService() {
1103 // If the log level is already high, start the service.
1104 final Intent i = new Intent(mContext, MailLogService.class);
1105 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
1106 if (mCurrentlyLogging == loggingEnabled) {
1107 // No change since previous run, just return;
1108 return;
1109 }
1110 if (loggingEnabled) {
1111 LogUtils.e(LOG_TAG, "Starting MailLogService");
1112 mContext.startService(i);
1113 } else {
1114 LogUtils.e(LOG_TAG, "Stopping MailLogService");
1115 mContext.stopService(i);
1116 }
1117 mCurrentlyLogging = loggingEnabled;
1118 }
1119
1120 @Override
1121 public void run() {
1122 startOrStopService();
1123 mHandler.postDelayed(this, WAIT_TIME);
1124 }
1125 };
1126 // Start the runnable right away.
1127 mHandler.post(mLogServiceChecker);
1128 }
1129
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07001130 /**
1131 * The application can be started from the following entry points:
1132 * <ul>
1133 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of
1134 * as “Starting the app”.</li>
1135 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li>
1136 * <li>Widget: Shows the contents of a synced label, and allows:
1137 * <ul>
1138 * <li>Viewing the list (tapping on the title)</li>
1139 * <li>Composing a new message (tapping on the new message icon in the title. This
1140 * launches the {@link ComposeActivity}.
1141 * </li>
1142 * <li>Viewing a single message (tapping on a list element)</li>
1143 * </ul>
1144 *
1145 * </li>
1146 * <li>Tapping on a notification:
1147 * <ul>
1148 * <li>Shows message list if more than one message</li>
1149 * <li>Shows the conversation if the notification is for a single message</li>
1150 * </ul>
1151 * </li>
1152 * <li>...and most importantly, the activity life cycle can tear down the application and
1153 * restart it:
1154 * <ul>
1155 * <li>Rotate the application: it is destroyed and recreated.</li>
1156 * <li>Navigate away, and return from recent applications.</li>
1157 * </ul>
1158 * </li>
1159 * <li>Add a new account: fires off an intent to add an account,
1160 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li>
1161 * <li>Re-authenticate your account: again returns in onActivityResult().</li>
1162 * <li>Composing can happen from many entry points: third party applications fire off an
1163 * intent to compose email, and launch directly into the {@link ComposeActivity}
1164 * .</li>
1165 * </ul>
1166 * {@inheritDoc}
1167 */
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08001168 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -08001169 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001170 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001171 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001172 // Allow shortcut keys to function for the ActionBar and menus.
1173 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001174 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001175 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001176 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001177 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001178
Andy Huang12b3ee42013-04-24 22:49:43 -07001179 mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer,
1180 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07001181 mDrawerListener = new MailDrawerListener();
1182 mDrawerContainer.setDrawerListener(mDrawerListener);
Andy Huang12b3ee42013-04-24 22:49:43 -07001183 mDrawerContainer.setDrawerShadow(
1184 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
1185
1186 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1187
Mindy Pereira161f50d2012-02-28 15:47:19 -08001188 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001189 // simplifies the amount of logic in the AbstractActivityController, but increases the
1190 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001191 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001192 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07001193 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001194 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -07001195 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -07001196
Andy Huang144bfe72013-06-11 13:27:52 -07001197 mDrawIdler.setRootView(mActivity.getWindow().getDecorView());
1198
Andy Huang632721e2012-04-11 16:57:26 -07001199 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001200 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001201 // that does not rely on restored fragments or loader data
1202 // any state restoration that relies on those can be done later in
1203 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1204 if (savedState != null) {
1205 if (savedState.containsKey(SAVED_ACCOUNT)) {
1206 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001207 }
1208 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001209 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001210 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001211 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001212 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001213 if (savedState.containsKey(SAVED_ACTION)) {
1214 mDialogAction = savedState.getInt(SAVED_ACTION);
1215 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001216 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001217 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001218 } else if (intent != null) {
1219 handleIntent(intent);
1220 }
Andy Huang632721e2012-04-11 16:57:26 -07001221 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001222 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1223 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001224 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001225 }
1226
1227 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001228 public void onPostCreate(Bundle savedState) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001229 // Sync the toggle state after onRestoreInstanceState has occurred.
1230 mDrawerToggle.syncState();
Andrew Sapperstein5747e152013-05-13 14:13:08 -07001231
Vikram Aggarwald70fe492013-06-04 12:52:07 -07001232 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout);
Paul Westbrook57246a42013-04-21 09:40:22 -07001233 }
1234
1235 @Override
1236 public void onConfigurationChanged(Configuration newConfig) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001237 mDrawerToggle.onConfigurationChanged(newConfig);
1238 }
1239
1240 /**
1241 * If drawer is open/visible (even partially), close it.
1242 */
1243 protected void closeDrawerIfOpen() {
1244 if (!isDrawerEnabled()) {
1245 return;
1246 }
1247 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1248 mDrawerContainer.closeDrawers();
1249 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001250 }
1251
1252 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001253 public void onStart() {
1254 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001255
1256 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang1ee96b22012-08-24 20:19:53 -07001257 }
1258
1259 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001260 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001261 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001262 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1263 if (fragment != null) {
1264 fragment.dismiss();
1265 }
mindypea04f932012-08-27 14:17:59 -07001266 // When the user places the app in the background by pressing "home",
1267 // dismiss the toast bar. However, since there is no way to determine if
1268 // home was pressed, just dismiss any existing toast bar when restarting
1269 // the app.
1270 if (mToastBar != null) {
1271 mToastBar.hide(false);
1272 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001273 }
1274
1275 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001276 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001277 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001278 }
1279
1280 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001281 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001282 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001283 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -08001284 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001285 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001286 }
1287
1288 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001289 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001290 return false;
1291 }
1292
mindyp17a8e782012-11-29 14:56:17 -08001293 public abstract boolean doesActionChangeConversationListVisibility(int action);
1294
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001295 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001296 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001297 /*
1298 * The action bar home/up action should open or close the drawer.
1299 * mDrawerToggle will take care of this.
1300 */
1301 if (mDrawerToggle.onOptionsItemSelected(item)) {
1302 return true;
1303 }
1304
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001305 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001306 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001307 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001308 /** This is NOT a batch action. */
1309 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001310 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001311 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001312 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001313 // doing before. Don't animate if we are launching a new screen.
1314 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Mindy Pereira28d5f722012-02-15 12:32:40 -08001315 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001316 case R.id.archive: {
1317 final boolean showDialog = (settings != null && settings.confirmArchive);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001318 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001319 break;
1320 }
Mindy Pereira01f30502012-08-14 10:30:51 -07001321 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001322 delete(R.id.remove_folder, target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001323 getDeferredRemoveFolder(target, mFolder, true, isBatch, true), isBatch);
Mindy Pereira01f30502012-08-14 10:30:51 -07001324 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001325 case R.id.delete: {
1326 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001327 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001328 break;
1329 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001330 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -07001331 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001332 confirmAndDelete(id, target, showDialog,
1333 R.plurals.confirm_discard_drafts_conversation);
Paul Westbrookef362542012-08-27 14:53:32 -07001334 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001335 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001336 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001337 updateConversation(Conversation.listOf(mCurrentConversation),
1338 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001339 break;
1340 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001341 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001342 delete(R.id.mark_not_important, target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001343 getDeferredAction(R.id.mark_not_important, target, isBatch), isBatch);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001344 } else {
1345 updateConversation(Conversation.listOf(mCurrentConversation),
1346 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1347 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001348 break;
1349 case R.id.mute:
Scott Kennedycaaeed32013-06-12 13:39:16 -07001350 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch), isBatch);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001351 break;
1352 case R.id.report_spam:
mindyp84f7d322012-10-01 17:14:40 -07001353 delete(R.id.report_spam, target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001354 getDeferredAction(R.id.report_spam, target, isBatch), isBatch);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001355 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07001356 case R.id.mark_not_spam:
mindyp84f7d322012-10-01 17:14:40 -07001357 // Currently, since spam messages are only shown in list with
1358 // other spam messages,
Paul Westbrook77eee622012-07-10 13:41:57 -07001359 // marking a message not as spam is a destructive action
mindyp84f7d322012-10-01 17:14:40 -07001360 delete(R.id.mark_not_spam, target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001361 getDeferredAction(R.id.mark_not_spam, target, isBatch), isBatch);
Paul Westbrook77eee622012-07-10 13:41:57 -07001362 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07001363 case R.id.report_phishing:
mindyp84f7d322012-10-01 17:14:40 -07001364 delete(R.id.report_phishing, target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001365 getDeferredAction(R.id.report_phishing, target, isBatch), isBatch);
Paul Westbrook76b20622012-07-12 11:45:43 -07001366 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001367 case android.R.id.home:
1368 onUpPressed();
1369 break;
Mindy Pereira9b875682012-02-15 18:10:54 -08001370 case R.id.compose:
1371 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1372 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001373 case R.id.refresh:
1374 requestFolderRefresh();
1375 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001376 case R.id.settings:
1377 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -08001378 break;
Paul Westbrooke5503552012-03-28 00:35:57 -07001379 case R.id.folder_options:
1380 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
1381 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -08001382 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -07001383 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -08001384 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001385 case R.id.feedback_menu_item:
Paul Westbrook83e6b572013-02-05 16:22:42 -08001386 Utils.sendFeedback(mActivity, mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001387 break;
Paul Westbrook18babd22012-04-09 22:17:08 -07001388 case R.id.manage_folders_item:
1389 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
1390 break;
Rohan Shahaab9bc72013-02-07 14:18:05 -08001391 case R.id.move_to:
1392 /* fall through */
Vikram Aggarwald503df42012-05-11 10:13:35 -07001393 case R.id.change_folder:
Mark Wei8f98ac02012-10-01 17:05:08 -07001394 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
1395 mActivity.getActivityContext(), mAccount, this,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001396 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
Rohan Shahaab9bc72013-02-07 14:18:05 -08001397 id == R.id.move_to);
Mark Wei9eb1c9a2012-10-01 12:54:50 -07001398 if (dialog != null) {
1399 dialog.show();
mindypa7e15452012-09-18 14:22:11 -07001400 }
Vikram Aggarwald503df42012-05-11 10:13:35 -07001401 break;
Scott Kennedydd2ec682013-06-03 19:16:13 -07001402 case R.id.move_to_inbox:
1403 new AsyncTask<Void, Void, Folder>() {
1404 @Override
1405 protected Folder doInBackground(final Void... params) {
1406 // Get the "move to" inbox
1407 return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
1408 true /* allowHidden */);
1409 }
1410
1411 @Override
1412 protected void onPostExecute(final Folder moveToInbox) {
1413 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
1414 // Add inbox
1415 ops.add(new FolderOperation(moveToInbox, true));
1416 assignFolder(ops, Conversation.listOf(mCurrentConversation), true,
1417 true /* showUndo */, false /* isMoveTo */);
1418 }
1419 }.execute((Void[]) null);
1420 break;
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001421 case R.id.empty_trash:
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001422 showEmptyDialog();
1423 break;
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001424 case R.id.empty_spam:
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001425 showEmptyDialog();
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001426 break;
Mindy Pereira9b875682012-02-15 18:10:54 -08001427 default:
1428 handled = false;
1429 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001430 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001431 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001432 }
1433
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001434 /**
1435 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1436 */
1437 private void showEmptyDialog() {
1438 if (mFolder != null) {
1439 final EmptyFolderDialogFragment fragment =
1440 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1441 fragment.setListener(this);
1442 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1443 }
1444 }
1445
1446 @Override
1447 public void onFolderEmptied() {
1448 emptyFolder();
1449 }
1450
1451 /**
1452 * Performs the work of emptying the currently visible folder.
1453 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001454 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001455 if (mConversationListCursor != null) {
1456 mConversationListCursor.emptyFolder();
1457 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001458 }
1459
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001460 private void attachEmptyFolderDialogFragmentListener() {
1461 final EmptyFolderDialogFragment fragment =
1462 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1463 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1464
1465 if (fragment != null) {
1466 fragment.setListener(this);
1467 }
1468 }
1469
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001470 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001471 * Toggles the drawer pullout. If it was open (Fully extended), the
1472 * drawer will be closed. Otherwise, the drawer will be opened. This should
1473 * only be called when used with a toggle item. Other cases should be handled
1474 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001475 */
1476 protected void toggleFolderListState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001477 if (!isDrawerEnabled()) {
1478 return;
1479 }
1480 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1481 mDrawerContainer.closeDrawers();
1482 } else {
1483 mDrawerContainer.openDrawer(mDrawerPullout);
1484 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001485 }
1486
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001487 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001488 public final boolean onUpPressed() {
1489 for (UpOrBackHandler h : mUpOrBackHandlers) {
1490 if (h.onUpPressed()) {
1491 return true;
1492 }
1493 }
1494 return handleUpPress();
1495 }
1496
1497 @Override
1498 public final boolean onBackPressed() {
1499 for (UpOrBackHandler h : mUpOrBackHandlers) {
1500 if (h.onBackPressed()) {
1501 return true;
1502 }
1503 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001504
1505 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1506 mDrawerContainer.closeDrawers();
1507 return true;
1508 }
1509
Andy Huangc1fb9a92013-02-11 13:09:12 -08001510 return handleBackPress();
1511 }
1512
1513 protected abstract boolean handleBackPress();
1514 protected abstract boolean handleUpPress();
1515
1516 @Override
1517 public void addUpOrBackHandler(UpOrBackHandler handler) {
1518 if (mUpOrBackHandlers.contains(handler)) {
1519 return;
1520 }
1521 mUpOrBackHandlers.addFirst(handler);
1522 }
1523
1524 @Override
1525 public void removeUpOrBackHandler(UpOrBackHandler handler) {
1526 mUpOrBackHandlers.remove(handler);
1527 }
1528
1529 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001530 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001531 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001532 refreshConversationList();
1533 }
1534
1535 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001536 public void updateConversation(Collection <Conversation> target, String columnName,
1537 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001538 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001539 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001540 }
1541
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001542 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001543 public void updateConversation(Collection <Conversation> target, String columnName,
1544 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001545 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001546 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001547 }
1548
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001549 @Override
1550 public void updateConversation(Collection <Conversation> target, String columnName,
1551 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001552 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001553 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001554 }
1555
Andy Huang839ada22012-07-20 15:48:40 -07001556 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001557 public void markConversationMessagesUnread(final Conversation conv,
1558 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001559 // The only caller of this method is the conversation view, from where marking unread should
1560 // *always* take you back to list mode.
1561 showConversation(null);
1562
Andy Huang839ada22012-07-20 15:48:40 -07001563 // locally mark conversation unread (the provider is supposed to propagate message unread
1564 // to conversation unread)
1565 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001566 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001567 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001568
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001569 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1570 @Override
1571 public void onLoadFinished() {
1572 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1573 originalConversationInfo);
1574 }
1575 });
1576 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001577 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001578 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1579 }
1580 }
1581
1582 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1583 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001584 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001585 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001586 final int numMessages = conv.getNumMessages();
1587 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1588 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001589
Andy Huang9e4ca792013-02-28 14:33:43 -08001590 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001591 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001592 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001593 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001594 // Conversations are neither marked read, nor viewed, and we don't want to show
1595 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001596 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Scott Kennedycaaeed32013-06-12 13:39:16 -07001597 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001598 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001599 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1600 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1601 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1602 info);
1603 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001604 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001605
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001606 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001607 if (originalConversationInfo != null) {
1608 mConversationListCursor.setConversationColumn(conv.uri,
1609 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1610 }
Andy Huang839ada22012-07-20 15:48:40 -07001611
1612 // applyBatch with each CPO as an UPDATE op on each affected message uri
1613 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1614 String authority = null;
1615 for (Uri messageUri : unreadMessageUris) {
1616 if (authority == null) {
1617 authority = messageUri.getAuthority();
1618 }
1619 ops.add(ContentProviderOperation.newUpdate(messageUri)
1620 .withValue(UIProvider.MessageColumns.READ, 0)
1621 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001622 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001623 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001624 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001625 new ContentProviderTask() {
1626 @Override
1627 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001628 if (result.exception != null) {
1629 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1630 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001631 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1632 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001633 }
Andy Huang839ada22012-07-20 15:48:40 -07001634 }
1635 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001636 }
Andy Huang839ada22012-07-20 15:48:40 -07001637 }
1638
1639 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001640 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1641 final boolean viewed) {
Scott Kennedy919d01a2013-05-07 16:13:29 -07001642 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray());
1643
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001644 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001645 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1646 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1647 targets.toArray());
1648 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001649 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1650 @Override
1651 public void onLoadFinished() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001652 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001653 }
1654 });
1655 } else {
1656 // We want to show the next conversation if we are marking unread.
Scott Kennedycaaeed32013-06-12 13:39:16 -07001657 markConversationsRead(targets, read, viewed, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001658 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001659 }
1660
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001661 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001662 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001663 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001664 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001665 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001666 final Runnable operation = new Runnable() {
1667 @Override
1668 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001669 markConversationsRead(targets, read, markViewed, showNext);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001670 }
1671 };
1672
Scott Kennedycaaeed32013-06-12 13:39:16 -07001673 if (!showNextConversation(targets, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001674 // This method will be called again if the user selects an autoadvance option
1675 return;
1676 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001677 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001678
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001679 final int size = targets.size();
1680 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1681 for (final Conversation target : targets) {
Paul Westbrooke2be97a2013-05-21 02:12:09 -07001682 final ContentValues value = new ContentValues(4);
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001683 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001684
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001685 // We never want to mark unseen here, but we do want to mark it seen
1686 if (read || markViewed) {
1687 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1688 }
1689
Paul Westbrook5109c512012-11-05 11:00:30 -08001690 // The mark read/unread/viewed operations do not show an undo bar
1691 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001692 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001693 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001694 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001695 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001696 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001697 boolean changed = info.markRead(read);
1698 if (changed) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001699 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
mindyp7f55c682012-10-04 11:38:27 -07001700 }
Andy Huang839ada22012-07-20 15:48:40 -07001701 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001702 opList.add(mConversationListCursor.getOperationForConversation(
1703 target, ConversationOperation.UPDATE, value));
1704 // Update the local conversation objects so they immediately change state.
1705 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001706 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001707 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001708 }
Andy Huang839ada22012-07-20 15:48:40 -07001709 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001710 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001711 }
1712
Andy Huang8f6b0062012-07-31 15:36:31 -07001713 /**
1714 * Auto-advance to a different conversation if the currently visible conversation in
1715 * conversation mode is affected (deleted, marked unread, etc.).
1716 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001717 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001718 *
1719 * @param target the set of conversations being deleted/marked unread
1720 */
mindyp9365a822012-09-12 09:09:09 -07001721 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001722 public void showNextConversation(final Collection<Conversation> target) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001723 showNextConversation(target, null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001724 }
1725
1726 /**
1727 * Auto-advance to a different conversation if the currently visible conversation in
1728 * conversation mode is affected (deleted, marked unread, etc.).
1729 *
1730 * <p>Does nothing if outside of conversation mode.</p>
Andy Huangc94d07f2013-06-03 16:19:35 -07001731 * <p>
1732 * Clients may pass an operation to execute on the target that this method will run after
1733 * auto-advance is complete. The operation, if provided, may run immediately, or it may run
1734 * later, or not at all. Reasons it may run later include:
1735 * <ul>
1736 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li>
1737 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the
1738 * mode change transition to finish</li>
1739 * </ul>
1740 * <p>If the current conversation is not in the target collection, this method will do nothing,
1741 * and will not execute the operation.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001742 *
1743 * @param target the set of conversations being deleted/marked unread
Andy Huangc94d07f2013-06-03 16:19:35 -07001744 * @param operation (optional) the operation to execute after advancing
1745 * @return <code>false</code> if this method handled or will execute the operation,
1746 * <code>true</code> otherwise.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001747 */
1748 private boolean showNextConversation(final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001749 final Runnable operation) {
Scott Kennedy8fb8e262012-11-28 15:48:03 -08001750 final int viewMode = mViewMode.getMode();
1751 final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1752 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
Andy Huang8f6b0062012-07-31 15:36:31 -07001753 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001754
Andy Huang8f6b0062012-07-31 15:36:31 -07001755 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001756 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1757
Scott Kennedycaaeed32013-06-12 13:39:16 -07001758 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001759 displayAutoAdvanceDialogAndPerformAction(operation);
1760 return false;
1761 } else {
1762 // If we don't have one set, but we're here, just take the default
Vikram Aggarwal82d37502013-01-10 16:18:49 -08001763 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1764 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001765
1766 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1767 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001768 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the
1769 // transition doesn't run (i.e. it "completes" immediately).
1770 mAutoAdvanceOp = operation;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001771 showConversation(next);
Andy Huangc94d07f2013-06-03 16:19:35 -07001772 return (mAutoAdvanceOp == null);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001773 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001774 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001775
1776 return true;
1777 }
1778
1779 /**
1780 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1781 * stored, and the specified operation is run.
1782 */
1783 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1784 final String[] autoAdvanceDisplayOptions =
1785 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1786 final String[] autoAdvanceOptionValues =
1787 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1788
1789 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1790 int initialIndex = 0;
1791 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1792 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1793 initialIndex = i;
1794 break;
1795 }
1796 }
1797
1798 final DialogInterface.OnClickListener listClickListener =
1799 new DialogInterface.OnClickListener() {
1800 @Override
1801 public void onClick(DialogInterface dialog, int whichItem) {
1802 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1803 final int autoAdvanceValueInt =
1804 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1805 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1806
1807 // Save the user's setting
1808 final ContentValues values = new ContentValues(1);
1809 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1810
1811 final ContentResolver resolver = mContext.getContentResolver();
1812 resolver.update(mAccount.updateSettingsUri, values, null, null);
1813
1814 // Dismiss the dialog, as clicking the items in the list doesn't close the
1815 // dialog.
1816 dialog.dismiss();
1817 if (operation != null) {
1818 operation.run();
1819 }
1820 }
1821 };
1822
1823 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1824 R.string.auto_advance_help_title)
1825 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1826 .setPositiveButton(null, null)
1827 .create()
1828 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001829 }
1830
Andy Huang839ada22012-07-20 15:48:40 -07001831 @Override
1832 public void starMessage(ConversationMessage msg, boolean starred) {
1833 if (msg.starred == starred) {
1834 return;
1835 }
1836
1837 msg.starred = starred;
1838
1839 // locally propagate the change to the owning conversation
1840 // (figure the provider will properly propagate the change when it commits it)
1841 //
1842 // when unstarring, only propagate the change if this was the only message starred
1843 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001844 final Conversation conv = msg.getConversation();
1845 if (conversationStarred != conv.starred) {
1846 conv.starred = conversationStarred;
1847 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001848 ConversationColumns.STARRED, conversationStarred);
1849 }
1850
1851 final ContentValues values = new ContentValues(1);
1852 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1853
1854 new ContentProviderTask.UpdateTask() {
1855 @Override
1856 protected void onPostExecute(Result result) {
1857 // TODO: handle errors?
1858 }
1859 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1860 }
1861
Andy Huang12b3ee42013-04-24 22:49:43 -07001862 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001863 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001864 if (mFolder == null) {
1865 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001866 }
Alice Yang37dda442013-03-26 22:48:53 -07001867 final ConversationListFragment convList = getConversationListFragment();
1868 if (convList == null) {
1869 // This could happen if this account is in initial sync (user
1870 // is seeing the "your mail will appear shortly" message)
1871 return;
1872 }
1873 convList.showSyncStatusBar();
1874
1875 if (mAsyncRefreshTask != null) {
1876 mAsyncRefreshTask.cancel(true);
1877 }
1878 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
1879 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001880 }
1881
Mindy Pereirafbe40192012-03-20 10:40:45 -07001882 /**
1883 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1884 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001885 * @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 -07001886 * @param target the conversations to act upon
1887 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1888 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07001889 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001890 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
1891 boolean showDialog, int confirmResource) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001892 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001893 if (showDialog) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001894 makeDialogListener(actionId, isBatch);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001895 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1896 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001897 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
1898 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001899 } else {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001900 delete(0, target, getDeferredAction(actionId, target, isBatch), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001901 }
1902 }
1903
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001904 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001905 public void delete(final int actionId, final Collection<Conversation> target,
Scott Kennedycaaeed32013-06-12 13:39:16 -07001906 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07001907 // Order of events is critical! The Conversation View Fragment must be
1908 // notified of the next conversation with showConversation(next) *before* the
1909 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001910 // fragment has a chance to delete the conversation, animating it away.
1911
mindyp84f7d322012-10-01 17:14:40 -07001912 // Update the conversation fragment if the current conversation is
1913 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001914 final Runnable operation = new Runnable() {
1915 @Override
1916 public void run() {
Scott Kennedycaaeed32013-06-12 13:39:16 -07001917 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001918 }
1919 };
1920
Scott Kennedycaaeed32013-06-12 13:39:16 -07001921 if (!showNextConversation(target, operation)) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001922 // This method will be called again if the user selects an autoadvance option
1923 return;
1924 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001925 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001926 // Batch selections are cleared in the end of the action, so not done for batch actions.
1927 if (!isBatch) {
1928 for (final Conversation conv : target) {
1929 if (mSelectedSet.contains(conv)) {
Vikram Aggarwal97ae7842013-04-22 16:29:12 -07001930 mSelectedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001931 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001932 }
1933 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001934 // The conversation list deletes and performs the action if it exists.
1935 final ConversationListFragment convListFragment = getConversationListFragment();
1936 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07001937 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001938 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001939 return;
1940 }
mindyp84f7d322012-10-01 17:14:40 -07001941 // No visible UI element handled it on our behalf. Perform the action
1942 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07001943 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07001944 action.performAction();
1945 }
1946
1947 /**
1948 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001949 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07001950 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001951 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07001952 action.performAction();
1953 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001954 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001955
1956 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001957 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1958 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001959 }
1960
1961 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001962 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001963 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001964 }
1965
Mindy Pereira68f2e222012-03-07 10:36:54 -08001966 @Override
1967 public void onPause() {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07001968 mHaveAccountList = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001969 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001970 }
1971
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001972 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001973 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001974 // Register the receiver that will prevent the status receiver from
1975 // displaying its notification icon as long as we're running.
1976 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1977 // that the notification was received for.
1978 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001979
1980 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001981
1982 attachEmptyFolderDialogFragmentListener();
Andrew Sapperstein23e33132013-05-07 18:26:49 -07001983
1984 // Invalidating the options menu so that when we make changes in settings,
1985 // the changes will always be updated in the action bar/options menu/
1986 mActivity.invalidateOptionsMenu();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001987 }
1988
1989 @Override
1990 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001991 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001992 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001993 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1994 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001995 if (mFolder != null) {
1996 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001997 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001998 // If this is a search activity, let's store the search query term as well.
1999 if (ConversationListContext.isSearchResult(mConvListContext)) {
2000 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
2001 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002002 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07002003 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
2004 }
Andy Huang4556a442012-03-30 16:42:05 -07002005 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002006 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07002007 }
Mindy Pereirad33674992012-06-25 16:26:30 -07002008 if (mToastBar.getVisibility() == View.VISIBLE) {
2009 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
2010 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002011 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002012 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002013 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002014 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002015 // 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 -08002016 if (mDialogAction != -1) {
2017 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002018 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002019 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002020 if (mDetachedConvUri != null) {
2021 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
2022 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002023
Scott Kennedyb10212e2013-02-22 16:27:00 -08002024 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andrew Sapperstein5747e152013-05-13 14:13:08 -07002025 mSafeToModifyFragments = false;
Andy Huang1ee96b22012-08-24 20:19:53 -07002026 }
2027
2028 /**
2029 * @see #mSafeToModifyFragments
2030 */
2031 protected boolean safeToModifyFragments() {
2032 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002033 }
2034
2035 @Override
Andy Huang313ac132013-03-04 23:40:58 -08002036 public void executeSearch(String query) {
Mindy Pereira68f2e222012-03-07 10:36:54 -08002037 Intent intent = new Intent();
2038 intent.setAction(Intent.ACTION_SEARCH);
2039 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
2040 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
2041 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07002042 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08002043 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002044 }
2045
2046 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002047 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08002048 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002049 }
2050
Andy Huang632721e2012-04-11 16:57:26 -07002051 @Override
2052 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06002053 // stop listening to the cursor on e.g. configuration changes
2054 if (mConversationListCursor != null) {
2055 mConversationListCursor.removeListener(this);
2056 }
Andy Huang144bfe72013-06-11 13:27:52 -07002057 mDrawIdler.setListener(null);
2058 mDrawIdler.setRootView(null);
Andy Huang632721e2012-04-11 16:57:26 -07002059 // unregister the ViewPager's observer on the conversation cursor
2060 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07002061 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002062 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07002063 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002064 mHandler.removeCallbacks(mLogServiceChecker);
2065 mLogServiceChecker = null;
Andy Huang632721e2012-04-11 16:57:26 -07002066 }
2067
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002068 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002069 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
2070 * or not. The individual controller is responsible for changing the icon based on the mode.
2071 */
2072 protected abstract void resetActionBarIcon();
2073
2074 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002075 * {@inheritDoc} Subclasses must override this to listen to mode changes
2076 * from the ViewMode. Subclasses <b>must</b> call the parent's
2077 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002078 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002079 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08002080 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002081 // When we step away from the conversation mode, we don't have a current conversation
2082 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
2083 if (!ViewMode.isConversationMode(newMode)) {
2084 setCurrentConversation(null);
2085 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08002086 // If the viewmode is not set, preserve existing icon.
2087 if (newMode != ViewMode.UNKNOWN) {
2088 resetActionBarIcon();
2089 }
Andy Huang12b3ee42013-04-24 22:49:43 -07002090
2091 if (isDrawerEnabled()) {
Scott Kennedy8a72b852013-05-02 14:18:50 -07002092 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(newMode));
2093 mDrawerContainer.setDrawerLockMode(getShouldAllowDrawerPull(newMode)
2094 ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
Andy Huang12b3ee42013-04-24 22:49:43 -07002095 closeDrawerIfOpen();
2096 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002097 }
2098
Scott Kennedy3b965d72013-06-25 14:36:55 -07002099 private static boolean getShouldShowDrawerIndicator(final int viewMode) {
Scott Kennedy8a72b852013-05-02 14:18:50 -07002100 // if search list/conv mode, disable indicator
2101 // only allow indicator at top level of app
2102 if (ViewMode.isSearchMode(viewMode)) {
2103 return false;
2104 } else {
2105 return viewMode == ViewMode.CONVERSATION_LIST || viewMode == ViewMode.FOLDER_LIST;
2106 }
2107 }
2108
Scott Kennedy3b965d72013-06-25 14:36:55 -07002109 private static boolean getShouldAllowDrawerPull(final int viewMode) {
Scott Kennedy8a72b852013-05-02 14:18:50 -07002110 // if search list/conv mode, disable drawer pull
2111 // allow drawer pull everywhere except conversation mode where the list is hidden
2112 if (ViewMode.isSearchMode(viewMode)) {
2113 return false;
2114 } else {
2115 return !(ViewMode.isConversationMode(viewMode)
2116 // TODO(ath): get this to work to allow drawer pull in 2-pane conv mode.
2117 /* && !isConversationListVisible() */);
2118 }
2119 }
2120
Andy Huang3825f3d2012-08-29 16:44:12 -07002121 public void disablePagerUpdates() {
2122 mPagerController.stopListening();
2123 }
2124
Andy Huang4e0158f2012-08-07 21:06:01 -07002125 public boolean isDestroyed() {
2126 return mDestroyed;
2127 }
2128
mindyp54f120f2012-08-28 13:10:33 -07002129 @Override
2130 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07002131 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002132 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07002133 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07002134 }
2135 }
2136
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08002137 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002138 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002139 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002140 // hasFocus already ensures that the window is in focus, so we don't need to call
2141 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07002142 if (hasFocus && convList != null && convList.isVisible()) {
2143 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002144 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002145 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002146 }
2147
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002148 /**
2149 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002150 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002151 */
Mindy Pereira75181e82012-04-18 08:17:13 -07002152 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07002153 if (account == null) {
2154 LogUtils.w(LOG_TAG, new Error(),
2155 "AAC ignoring null (presumably invalid) account restoration");
2156 return;
2157 }
Andy Huangb1148412012-05-19 00:16:30 -07002158 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002159 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002160 // Only change AAC state here. Do *not* modify any other object's state. The object
2161 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002162 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07002163 mActivity.invalidateOptionsMenu();
2164 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002165 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08002166 // The Mail instance can be null during test runs.
2167 final MailAppProvider instance = MailAppProvider.getInstance();
2168 if (instance != null) {
2169 instance.setLastViewedAccount(mAccount.uri.toString());
2170 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07002171 if (account.settings == null) {
2172 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
2173 return;
2174 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07002175 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07002176 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07002177 }
2178
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002179 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08002180 * Restore the state from the previous bundle. Subclasses should call this
2181 * method from the parent class, since it performs important UI
2182 * initialization.
2183 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002184 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002185 */
Andy Huang632721e2012-04-11 16:57:26 -07002186 @Override
2187 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002188 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07002189 if (savedState.containsKey(SAVED_CONVERSATION)) {
2190 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08002191 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07002192 if (conversation != null && conversation.position < 0) {
2193 // Set the position to 0 on this conversation, as we don't know where it is
2194 // in the list
2195 conversation.position = 0;
2196 }
Andy Huanged4fdf02012-07-26 17:12:50 -07002197 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002198 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002199
Mindy Pereirad33674992012-06-25 16:26:30 -07002200 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08002201 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07002202 if (op != null) {
2203 if (op.getType() == ToastBarOperation.UNDO) {
2204 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07002205 } else if (op.getType() == ToastBarOperation.ERROR) {
2206 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07002207 }
2208 }
2209 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08002210 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002211 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07002212 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002213 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07002214 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002215 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07002216 * Restore the state of selected conversations. This needs to be done after the correct mode
2217 * is set and the action bar is fully initialized. If not, several key pieces of state
2218 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07002219 */
Andy Huang4556a442012-03-30 16:42:05 -07002220 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002221 // Order is important!!!
2222 // The dialog listener needs to happen *after* the selected set is restored.
2223
2224 // If there has been an orientation change, and we need to recreate the listener for the
2225 // confirm dialog fragment (delete/archive/...), then do it here.
2226 if (mDialogAction != -1) {
2227 makeDialogListener(mDialogAction, mDialogFromSelectedSet);
2228 }
Andy Huang632721e2012-04-11 16:57:26 -07002229 }
2230
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002231 /**
2232 * Handle an intent to open the app. This method is called only when there is no saved state,
2233 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2234 * since it has not been previously set.
Vikram Aggarwal6aca6892013-06-04 13:53:27 -07002235 *
2236 * This method is called for a subset of the reasons mentioned in
2237 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from
2238 * notifications, widgets, and shortcuts.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002239 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002240 */
Andy Huang632721e2012-04-11 16:57:26 -07002241 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002242 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002243 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2244 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002245 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002246 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002247 if (mAccount == null) {
2248 return;
Andy Huang632721e2012-04-11 16:57:26 -07002249 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002250 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002251 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002252 mViewMode.enterConversationMode();
2253 } else {
2254 mViewMode.enterConversationListMode();
2255 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002256 // Put the folder and conversation, and ask the loader to create this folder.
2257 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002258
2259 final Uri folderUri;
2260 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
2261 folderUri = (Uri) intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
2262 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2263 final Folder folder =
2264 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
2265 folderUri = folder.uri;
2266 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002267 final Bundle extras = intent.getExtras();
2268 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2269 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002270 folderUri = mAccount.settings.defaultInbox;
2271 }
2272
Scott Kennedy60593352013-03-13 13:45:30 -07002273 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002274 args.putParcelable(Utils.EXTRA_CONVERSATION,
2275 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2276 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002277 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2278 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002279 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07002280 // Save this search query for future suggestions.
2281 final String query = intent.getStringExtra(SearchManager.QUERY);
2282 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002283 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07002284 mContext, authority, SuggestionsProvider.MODE);
2285 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002286 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2287 fetchSearchFolder(intent);
2288 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002289 mViewMode.enterSearchResultsConversationMode();
2290 } else {
2291 mViewMode.enterSearchResultsListMode();
2292 }
Andy Huang632721e2012-04-11 16:57:26 -07002293 } else {
2294 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2295 mActivity.finish();
2296 }
2297 }
2298 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002299 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002300 }
2301 }
2302
Andy Huang4556a442012-03-30 16:42:05 -07002303 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002304 * Returns true if we should enter conversation mode with search.
2305 */
2306 protected final boolean shouldEnterSearchConvMode() {
2307 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2308 }
2309
2310 /**
Andy Huang4556a442012-03-30 16:42:05 -07002311 * Copy any selected conversations stored in the saved bundle into our selection set,
2312 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2313 *
2314 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002315 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002316 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07002317 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002318 return;
2319 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002320 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002321 if (selectedSet == null || selectedSet.isEmpty()) {
2322 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002323 return;
2324 }
Andy Huang632721e2012-04-11 16:57:26 -07002325
2326 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002327 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002328 }
2329
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002330 private void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07002331 showConversation(conversation, false /* inLoaderCallbacks */);
2332 }
2333
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002334 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002335 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2336 * object, which is a signal to back out of conversation view mode.
2337 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002338 * @param conversation the conversation to be shown, or null if we want to back out to list
2339 * mode.
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002340 * @param inLoaderCallbacks true if the method is called as a result of
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002341 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002342 */
Andy Huang1ee96b22012-08-24 20:19:53 -07002343 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Andy Huang243c2362013-03-01 17:50:35 -08002344 if (conversation != null) {
2345 Utils.sConvLoadTimer.start();
2346 }
2347
Andy Huang54e925e2013-03-14 13:24:18 -07002348 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002349 // Set the current conversation just in case it wasn't already set.
2350 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002351 }
2352
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002353 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002354 * Children can override this method, but they must call super.showWaitForInitialization().
2355 * {@inheritDoc}
2356 */
2357 @Override
2358 public void showWaitForInitialization() {
2359 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002360 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002361 }
2362
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002363 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002364 final FragmentManager manager = mActivity.getFragmentManager();
2365 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002366 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002367 if (waitFragment != null) {
2368 waitFragment.updateAccount(mAccount);
2369 }
2370 }
2371
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002372 /**
2373 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2374 * method, though they must call the parent implementation <b>after</b> they do anything.
2375 */
2376 protected void hideWaitForInitialization() {
2377 mWaitFragment = null;
2378 }
2379
2380 /**
2381 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2382 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2383 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002384 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002385 */
2386 protected final WaitFragment getWaitFragment() {
2387 final FragmentManager manager = mActivity.getFragmentManager();
2388 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2389 if (waitFrag != null) {
2390 // The Fragment Manager knows better, so use its instance.
2391 mWaitFragment = waitFrag;
2392 }
2393 return mWaitFragment;
2394 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002395
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002396 /**
2397 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2398 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002399 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002400 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002401 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002402 if (waitFragment != null) {
2403 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002404 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002405 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2406 }
2407 return false;
2408 }
2409
2410 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002411 * Children can override this method, but they must call super.showConversationList().
2412 * {@inheritDoc}
2413 */
2414 @Override
2415 public void showConversationList(ConversationListContext listContext) {
2416 }
2417
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002418 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002419 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002420 final ConversationListFragment convListFragment = getConversationListFragment();
2421 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2422 convListFragment.getAnimatedAdapter().onConversationSelected();
2423 }
mindypaa55bc92012-08-24 09:49:56 -07002424 // Only animate destructive actions if we are going to be showing the
2425 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002426 commitDestructiveActions(mIsTablet);
Andy Huang1ee96b22012-08-24 20:19:53 -07002427 showConversation(conversation, inLoaderCallbacks);
2428 }
2429
2430 @Override
Andrew Sapperstein2f542872013-06-11 10:48:30 -07002431 public final void onCabModeEntered() {
2432 final ConversationListFragment convListFragment = getConversationListFragment();
2433 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2434 convListFragment.getAnimatedAdapter().onCabModeEntered();
2435 }
2436 }
2437
2438 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07002439 public Conversation getCurrentConversation() {
2440 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002441 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002442
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002443 /**
2444 * Set the current conversation. This is the conversation on which all actions are performed.
2445 * Do not modify mCurrentConversation except through this method, which makes it easy to
2446 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002447 * @param conversation new conversation to view. Passing null indicates that we are backing
2448 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002449 */
Andy Huang632721e2012-04-11 16:57:26 -07002450 @Override
2451 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002452 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002453 // we are going back to conversation list mode.
2454 if (mDetachedConvUri != null && (conversation == null
2455 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002456 clearDetachedMode();
2457 }
2458
2459 // Must happen *before* setting mCurrentConversation because this sets
2460 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002461 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002462 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002463
2464 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07002465 mActionBarView.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002466 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002467 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002468 }
2469
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002470 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002471 * {@link LoaderManager} currently has a bug in
2472 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2473 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2474 * this bug by destroying any loaders that may have been created as null (essentially because
2475 * they are optional loads, and may not apply to a particular account).
2476 * <p>
2477 * A simple null check before restarting a loader will not work, because that would not
2478 * give the controller a chance to invalidate UI corresponding the prior loader result.
2479 *
2480 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002481 * @param handler the LoaderCallback which will handle this loader ID.
2482 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2483 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002484 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002485 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002486 final LoaderManager lm = mActivity.getLoaderManager();
2487 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002488 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002489 }
2490
Andy Huang632721e2012-04-11 16:57:26 -07002491 @Override
2492 public void registerConversationListObserver(DataSetObserver observer) {
2493 mConversationListObservable.registerObserver(observer);
2494 }
2495
2496 @Override
2497 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002498 try {
2499 mConversationListObservable.unregisterObserver(observer);
2500 } catch (IllegalStateException e) {
2501 // Log instead of crash
2502 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2503 + "hasn't been registered");
2504 }
Andy Huang632721e2012-04-11 16:57:26 -07002505 }
2506
Andy Huang090db1e2012-07-25 13:25:28 -07002507 @Override
2508 public void registerFolderObserver(DataSetObserver observer) {
2509 mFolderObservable.registerObserver(observer);
2510 }
2511
2512 @Override
2513 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002514 try {
2515 mFolderObservable.unregisterObserver(observer);
2516 } catch (IllegalStateException e) {
2517 // Log instead of crash
2518 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2519 + "hasn't been registered");
2520 }
Andy Huang090db1e2012-07-25 13:25:28 -07002521 }
2522
Andy Huang9d3fd922012-09-26 22:23:58 -07002523 @Override
2524 public void registerConversationLoadedObserver(DataSetObserver observer) {
2525 mPagerController.registerConversationLoadedObserver(observer);
2526 }
2527
2528 @Override
2529 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002530 try {
2531 mPagerController.unregisterConversationLoadedObserver(observer);
2532 } catch (IllegalStateException e) {
2533 // Log instead of crash
2534 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2535 + "that hasn't been registered");
2536 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002537 }
2538
Vikram Aggarwal60069912012-07-24 14:26:09 -07002539 /**
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002540 * Returns true if the number of accounts is different, or if the current account has
2541 * changed. This method is meant to filter frequent changes to the list of
2542 * accounts, and only return true if the new list is substantially different from the existing
2543 * list. Returning true is safe here, it leads to more work in creating the
2544 * same account list again.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002545 * @param accountCursor the cursor which points to all the accounts.
2546 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002547 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002548 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002549 // Check to see if the current account hasn't been set, or the account cursor is empty
2550 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002551 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002552 }
2553
2554 // Check to see if the number of accounts are different, from the number we saw on the last
2555 // updated
2556 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2557 return true;
2558 }
2559
2560 // Check to see if the account list is different or if the current account is not found in
2561 // the cursor.
2562 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002563 do {
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002564 final Account account = accountCursor.getModel();
2565 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) {
2566 if (mAccount.settingsDiffer(account)) {
2567 // Settings changed, and we don't need to look any further.
2568 return true;
2569 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002570 foundCurrentAccount = true;
2571 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002572 // Is there a new account that we do not know about?
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07002573 if (!mCurrentAccountUris.contains(account.uri)) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002574 return true;
2575 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002576 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002577
2578 // As long as we found the current account, the list hasn't been updated
2579 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002580 }
2581
2582 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002583 * Updates accounts for the app. If the current account is missing, the first
2584 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002585 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002586 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002587 * @return true if the update was successful, false otherwise
2588 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002589 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002590 if (accounts == null || !accounts.moveToFirst()) {
2591 return false;
2592 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002593
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002594 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002595 // A match for the current account's URI in the list of accounts.
2596 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002597
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002598 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002599 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002600 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002601 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002602 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002603 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2604 currentFromList = account;
2605 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002606 }
2607
Vikram Aggarwal60069912012-07-24 14:26:09 -07002608 // 1. current account is already set and is in allAccounts:
2609 // 1a. It has changed -> load the updated account.
2610 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002611 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002612 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002613 // 4. otherwise just pick first
2614
Vikram Aggarwal60069912012-07-24 14:26:09 -07002615 boolean accountChanged = false;
2616 /// Assume case 4, initialize to first account, and see if we can find anything better.
2617 Account newAccount = allAccounts[0];
2618 if (currentFromList != null) {
2619 // Case 1: Current account exists but has changed
2620 if (!currentFromList.equals(mAccount)) {
2621 newAccount = currentFromList;
2622 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002623 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002624 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002625 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002626 // Case 2: Current account is not in allAccounts, the account needs to change.
2627 accountChanged = true;
2628 if (mAccount == null) {
2629 // Case 3: Check for last viewed account, and check if it exists in the list.
2630 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2631 if (lastAccountUri != null) {
2632 for (final Account account : allAccounts) {
2633 if (lastAccountUri.equals(account.uri.toString())) {
2634 newAccount = account;
2635 break;
2636 }
Andy Huang0d647352012-03-21 21:48:16 -07002637 }
2638 }
2639 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002640 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002641 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002642 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002643 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002644
Vikram Aggarwal60069912012-07-24 14:26:09 -07002645 // Whether we have updated the current account or not, we need to update the list of
2646 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002647 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002648 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002649 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002650 }
2651
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002652 private void disableNotifications() {
2653 mNewEmailReceiver.activate(mContext, this);
2654 }
2655
2656 private void enableNotifications() {
2657 mNewEmailReceiver.deactivate();
2658 }
2659
2660 private void disableNotificationsOnAccountChange(Account account) {
2661 // If the new mail suppression receiver is activated for a different account, we want to
2662 // activate it for the new account.
2663 if (mNewEmailReceiver.activated() &&
2664 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2665 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2666 mNewEmailReceiver.deactivate();
2667 mNewEmailReceiver.activate(mContext, this);
2668 }
2669 }
2670
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002671 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002672 * Destructive actions on Conversations. This class should only be created by controllers, and
2673 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2674 * Only the controllers should know what kind of destructive actions are being created.
2675 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002676 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002677 /**
2678 * The action to be performed. This is specified as the resource ID of the menu item
2679 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2680 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002681 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002682 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002683 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002684 /** Whether this destructive action has already been performed */
2685 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002686 /** Whether this is an action on the currently selected set. */
2687 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002688
Mindy Pereirafbe40192012-03-20 10:40:45 -07002689 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002690 * Create a listener object.
2691 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002692 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002693 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002694 * @param isBatch whether the conversations are in the currently selected batch set.
2695 */
2696 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002697 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002698 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002699 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002700 }
2701
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002702 /**
2703 * The action common to child classes. This performs the action specified in the constructor
2704 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002705 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002706 @Override
2707 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002708 if (isPerformed()) {
2709 return;
2710 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002711 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002712
2713 // Are we destroying the currently shown conversation? Show the next one.
2714 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002715 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2716 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002717 Conversation.toString(mTarget), mCurrentConversation);
2718 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002719
Paul Westbrooke1221d22012-08-19 11:09:07 -07002720 if (mConversationListCursor == null) {
2721 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2722 + "\nmTarget=%s\nCurrent=%s",
2723 Conversation.toString(mTarget), mCurrentConversation);
2724 return;
2725 }
2726
Mindy Pereirafbe40192012-03-20 10:40:45 -07002727 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002728 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002729 LogUtils.d(LOG_TAG, "Archiving");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002730 mConversationListCursor.archive(mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002731 break;
2732 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002733 LogUtils.d(LOG_TAG, "Deleting");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002734 mConversationListCursor.delete(mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002735 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002736 undoEnabled = false;
2737 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002738 break;
2739 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002740 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002741 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002742 for (Conversation c : mTarget) {
2743 c.localDeleteOnUpdate = true;
2744 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002745 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002746 mConversationListCursor.mute(mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002747 break;
2748 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002749 LogUtils.d(LOG_TAG, "Reporting spam");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002750 mConversationListCursor.reportSpam(mTarget);
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002751 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002752 case R.id.mark_not_spam:
2753 LogUtils.d(LOG_TAG, "Marking not spam");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002754 mConversationListCursor.reportNotSpam(mTarget);
Paul Westbrook77eee622012-07-10 13:41:57 -07002755 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002756 case R.id.report_phishing:
2757 LogUtils.d(LOG_TAG, "Reporting phishing");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002758 mConversationListCursor.reportPhishing(mTarget);
Paul Westbrook76b20622012-07-12 11:45:43 -07002759 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002760 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002761 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002762 // Star removal is destructive in the Starred folder.
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002763 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2764 false);
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002765 break;
2766 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002767 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002768 // Marking not important is destructive in a mailbox
2769 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002770 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002771 for (Conversation conv : mTarget) {
2772 conv.localDeleteOnUpdate = true;
2773 }
2774 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002775 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2776 UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002777 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002778 case R.id.discard_drafts:
2779 LogUtils.d(LOG_TAG, "Discarding draft messages");
2780 // Discarding draft messages is destructive in a "draft" mailbox
2781 if (mFolder != null && mFolder.isDraft()) {
2782 for (Conversation conv : mTarget) {
2783 conv.localDeleteOnUpdate = true;
2784 }
2785 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002786 mConversationListCursor.discardDrafts(mTarget);
Paul Westbrookef362542012-08-27 14:53:32 -07002787 // We don't support undoing discarding drafts
2788 undoEnabled = false;
2789 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002790 }
2791 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002792 mHandler.postDelayed(new Runnable() {
2793 @Override
2794 public void run() {
2795 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002796 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002797 }
2798 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002799 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002800 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002801 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002802 mSelectedSet.clear();
2803 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002804 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002805
2806 /**
2807 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002808 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002809 */
2810 private synchronized boolean isPerformed() {
2811 if (mCompleted) {
2812 return true;
2813 }
2814 mCompleted = true;
2815 return false;
2816 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002817 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002818
Vikram Aggarwald503df42012-05-11 10:13:35 -07002819 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2820 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002821 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002822 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002823 Collection<Conversation> target, boolean batch, boolean showUndo,
2824 final boolean isMoveTo) {
Mindy Pereira8db7e402012-07-13 10:32:47 -07002825 // Actions are destructive only when the current folder can be assigned
2826 // to (which is the same as being able to un-assign a conversation from the folder) and
2827 // when the list of folders contains the current folder.
2828 final boolean isDestructive = mFolder
2829 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2830 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002831 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2832 if (isDestructive) {
2833 for (final Conversation c : target) {
2834 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002835 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002836 }
mindypc84759c2012-08-29 09:51:53 -07002837 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002838 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002839 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002840 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002841 /*
2842 * If this is a MOVE operation, we want the action folder to be the destination folder.
2843 * Otherwise, we want it to be the current folder.
2844 *
2845 * A set of folder operations is a move if there are exactly two operations: an add and
2846 * a remove.
2847 */
2848 final Folder actionFolder;
2849 if (folderOps.size() != 2) {
2850 actionFolder = mFolder;
2851 } else {
2852 Folder addedFolder = null;
2853 boolean hasRemove = false;
2854 for (final FolderOperation folderOperation : folderOps) {
2855 if (folderOperation.mAdd) {
2856 addedFolder = folderOperation.mFolder;
2857 } else {
2858 hasRemove = true;
2859 }
2860 }
2861
2862 if (hasRemove && addedFolder != null) {
2863 actionFolder = addedFolder;
2864 } else {
2865 actionFolder = mFolder;
2866 }
2867 }
2868
mindypc84759c2012-08-29 09:51:53 -07002869 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002870 batch, showUndo, isMoveTo, actionFolder);
Scott Kennedycaaeed32013-06-12 13:39:16 -07002871 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002872 } else {
mindypc84759c2012-08-29 09:51:53 -07002873 folderChange = getFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002874 batch, showUndo, false /* isMoveTo */, mFolder);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002875 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002876 }
2877 }
2878
Mindy Pereira967ede62012-03-22 09:29:09 -07002879 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002880 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002881 if (isAnimating() || isDragging()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002882 LogUtils.i(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done");
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002883 return;
2884 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002885 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002886 if (mConversationListCursor.isRefreshRequired()) {
2887 mConversationListCursor.refresh();
2888 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002889 }
2890
mindyp5390fca2012-08-22 12:12:25 -07002891 @Override
2892 public void startDragMode() {
2893 mIsDragHappening = true;
2894 }
2895
2896 @Override
2897 public void stopDragMode() {
2898 mIsDragHappening = false;
2899 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002900 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: try sync");
mindyp5390fca2012-08-22 12:12:25 -07002901 onRefreshReady();
2902 }
2903
2904 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07002905 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: refresh");
mindyp5390fca2012-08-22 12:12:25 -07002906 mConversationListCursor.refresh();
2907 }
2908 }
2909
2910 private boolean isDragging() {
2911 return mIsDragHappening;
2912 }
2913
mindyp6f54e1b2012-10-09 09:54:08 -07002914 @Override
2915 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002916 boolean isAnimating = false;
2917 ConversationListFragment convListFragment = getConversationListFragment();
2918 if (convListFragment != null) {
Andy Huang48ccbc52013-06-05 20:30:47 -07002919 isAnimating = convListFragment.isAnimating();
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002920 }
2921 return isAnimating;
2922 }
2923
Marc Blankbf128eb2012-04-18 15:58:45 -07002924 /**
2925 * Called when the {@link ConversationCursor} is changed or has new data in it.
2926 * <p>
2927 * {@inheritDoc}
2928 */
2929 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002930 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002931 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2932 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06002933
2934 if (mDestroyed) {
2935 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2936 return;
2937 }
2938
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002939 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002940 // Swap cursors
2941 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002942 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002943 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002944 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002945 }
2946
2947 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002948 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002949 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002950 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002951 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002952 }
2953
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002954 /**
2955 * If the Conversation List Fragment is visible, updates the fragment.
2956 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002957 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002958 final ConversationListFragment convList = getConversationListFragment();
2959 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002960 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002961 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002962 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002963 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002964 }
2965 }
2966
2967 /**
2968 * This class handles throttled refresh of the conversation list
2969 */
2970 static class RefreshTimerTask extends TimerTask {
2971 final Handler mHandler;
2972 final AbstractActivityController mController;
2973
2974 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2975 mHandler = handler;
2976 mController = controller;
2977 }
2978
2979 @Override
2980 public void run() {
2981 mHandler.post(new Runnable() {
2982 @Override
2983 public void run() {
2984 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2985 mController.onRefreshRequired();
2986 }});
2987 }
2988 }
2989
2990 /**
2991 * Cancel the refresh task, if it's running
2992 */
2993 private void cancelRefreshTask () {
2994 if (mConversationListRefreshTask != null) {
2995 mConversationListRefreshTask.cancel();
2996 mConversationListRefreshTask = null;
2997 }
2998 }
2999
3000 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003001 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07003002 if (mConversationListCursor == null) {
3003 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
3004 return;
3005 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003006 if (mConversationListCursor.isRefreshReady()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003007 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003008 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07003009 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003010
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003011 if (mConversationListCursor.isRefreshRequired()) {
Andy Huangc1922a92013-05-13 14:33:05 -07003012 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07003013 mConversationListCursor.refresh();
3014 }
mindyp6f54e1b2012-10-09 09:54:08 -07003015 if (mRecentsDataUpdated) {
3016 mRecentsDataUpdated = false;
3017 mRecentFolderObservers.notifyChanged();
3018 }
Marc Blankbf128eb2012-04-18 15:58:45 -07003019 }
3020
3021 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07003022 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003023 // There are no selected conversations. Ensure that the listener and its associated actions
3024 // are blanked out.
3025 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07003026 }
3027
3028 @Override
3029 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003030 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08003031 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08003032 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07003033 }
Mindy Pereira967ede62012-03-22 09:29:09 -07003034 }
3035
Mindy Pereira967ede62012-03-22 09:29:09 -07003036 @Override
3037 public void onSetChanged(ConversationSelectionSet set) {
3038 // Do nothing. We don't care about changes to the set.
3039 }
3040
3041 @Override
3042 public ConversationSelectionSet getSelectedSet() {
3043 return mSelectedSet;
3044 }
3045
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003046 /**
3047 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
3048 */
3049 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07003050 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07003051 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07003052 if (mCabActionMenu != null) {
3053 mCabActionMenu.deactivate();
3054 }
3055 }
3056
3057 /**
3058 * Re-enable the CAB menu if required. The selection set is not changed.
3059 */
3060 protected void enableCabMode() {
3061 if (mCabActionMenu != null) {
3062 mCabActionMenu.activate();
3063 }
3064 }
3065
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07003066 /**
3067 * Unselect conversations and exit CAB mode.
3068 */
3069 protected final void exitCabMode() {
3070 mSelectedSet.clear();
3071 }
3072
Mindy Pereira967ede62012-03-22 09:29:09 -07003073 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003074 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07003075 if (mAccount == null) {
3076 // We cannot search if there is no account. Drop the request to the floor.
3077 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
3078 return;
3079 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003080 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
Andy Huang313ac132013-03-04 23:40:58 -08003081 || mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
3082 mActionBarView.expandSearch();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003083 } else {
3084 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07003085 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07003086 }
3087 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003088
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07003089 @Override
3090 public void exitSearchMode() {
3091 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
3092 mActivity.finish();
3093 }
3094 }
3095
Mindy Pereiraacf60392012-04-06 09:11:00 -07003096 /**
3097 * Supports dragging conversations to a folder.
3098 */
3099 @Override
3100 public boolean supportsDrag(DragEvent event, Folder folder) {
3101 return (folder != null
3102 && event != null
3103 && event.getClipDescription() != null
3104 && folder.supportsCapability
3105 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
3106 && folder.supportsCapability
3107 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
3108 && !mFolder.uri.equals(folder.uri));
3109 }
3110
3111 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07003112 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07003113 */
3114 @Override
3115 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07003116 if (!supportsDrag(event, folder)) {
3117 return;
3118 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003119 if (folder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003120 // Moving a conversation to the starred folder adds the star and
3121 // removes the current label
3122 handleDropInStarred(folder);
3123 return;
3124 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07003125 if (mFolder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08003126 handleDragFromStarred(folder);
3127 return;
3128 }
mindypa8492632012-09-24 09:27:54 -07003129 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08003130 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07003131 // Add the drop target folder.
3132 dragDropOperations.add(new FolderOperation(folder, true));
3133 // Remove the current folder unless the user is viewing "all".
3134 // That operation should just add the new folder.
3135 boolean isDestructive = !mFolder.isViewAll()
3136 && mFolder.supportsCapability
3137 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
3138 if (isDestructive) {
3139 dragDropOperations.add(new FolderOperation(mFolder, false));
3140 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003141 // Drag and drop is destructive: we remove conversations from the
3142 // current folder.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003143 final DestructiveAction action =
3144 getFolderChange(conversations, dragDropOperations, isDestructive,
3145 true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder);
mindypa8492632012-09-24 09:27:54 -07003146 if (isDestructive) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07003147 delete(0, conversations, action, true);
mindypa8492632012-09-24 09:27:54 -07003148 } else {
3149 action.performAction();
3150 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07003151 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07003152
mindypae7e6a02012-11-29 13:28:10 -08003153 private void handleDragFromStarred(Folder folder) {
3154 final Collection<Conversation> conversations = mSelectedSet.values();
3155 // The conversation list deletes and performs the action if it exists.
3156 final ConversationListFragment convListFragment = getConversationListFragment();
3157 // There should always be a convlistfragment, or the user could not have
3158 // dragged/ dropped conversations.
3159 if (convListFragment != null) {
3160 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
3161 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08003162 ArrayList<Uri> folderUris;
3163 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08003164 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003165 folderUris = new ArrayList<Uri>();
3166 adds = new ArrayList<Boolean>();
3167 folderUris.add(folder.uri);
3168 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003169 final HashMap<Uri, Folder> targetFolders =
3170 Folder.hashMapForFolders(target.getRawFolders());
mindypae7e6a02012-11-29 13:28:10 -08003171 targetFolders.put(folder.uri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08003172 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3173 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08003174 }
3175 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003176 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003177 }
3178 refreshConversationList();
3179 mSelectedSet.clear();
mindypae7e6a02012-11-29 13:28:10 -08003180 }
3181 }
3182
3183 private void handleDropInStarred(Folder folder) {
3184 final Collection<Conversation> conversations = mSelectedSet.values();
3185 // The conversation list deletes and performs the action if it exists.
3186 final ConversationListFragment convListFragment = getConversationListFragment();
3187 // There should always be a convlistfragment, or the user could not have
3188 // dragged/ dropped conversations.
3189 if (convListFragment != null) {
3190 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08003191 convListFragment.requestDelete(R.id.change_folder, conversations,
mindyp5cc0ab22012-12-11 08:47:35 -08003192 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08003193 }
3194 }
3195
3196 // When dragging conversations to the starred folder, remove from the
3197 // original folder and add a star
3198 private class DroppedInStarredAction implements DestructiveAction {
3199 private Collection<Conversation> mConversations;
3200 private Folder mInitialFolder;
mindyp5cc0ab22012-12-11 08:47:35 -08003201 private Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08003202
mindyp5cc0ab22012-12-11 08:47:35 -08003203 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
3204 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08003205 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08003206 mInitialFolder = initialFolder;
3207 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08003208 }
3209
3210 @Override
3211 public void performAction() {
3212 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003213 R.id.change_folder, ToastBarOperation.UNDO, true /* batch */, mInitialFolder);
mindypae7e6a02012-11-29 13:28:10 -08003214 onUndoAvailable(undoOp);
3215 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3216 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08003217 ArrayList<Uri> folderUris;
3218 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08003219 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08003220 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08003221 folderUris = new ArrayList<Uri>();
3222 adds = new ArrayList<Boolean>();
mindyp5cc0ab22012-12-11 08:47:35 -08003223 folderUris.add(mStarred.uri);
3224 adds.add(Boolean.TRUE);
mindypcb0b30e2012-11-30 10:16:35 -08003225 folderUris.add(mInitialFolder.uri);
3226 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08003227 final HashMap<Uri, Folder> targetFolders =
3228 Folder.hashMapForFolders(target.getRawFolders());
3229 targetFolders.put(mStarred.uri, mStarred);
mindypae7e6a02012-11-29 13:28:10 -08003230 targetFolders.remove(mInitialFolder.uri);
mindyp5cc0ab22012-12-11 08:47:35 -08003231 values.put(ConversationColumns.STARRED, true);
3232 operation = mConversationListCursor.getConversationFolderOperation(target,
3233 folderUris, adds, targetFolders.values(), values);
3234 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08003235 }
3236 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003237 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003238 }
3239 refreshConversationList();
3240 mSelectedSet.clear();
3241 }
3242 }
3243
Mindy Pereira0963ef82012-04-10 11:43:01 -07003244 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003245 public void onTouchEvent(MotionEvent event) {
3246 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003247 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07003248 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003249 }
3250 }
3251 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003252
Mark Weid243d452012-10-31 16:24:08 -07003253 protected abstract void hideOrRepositionToastBar(boolean animated);
3254
Andy Huang632721e2012-04-11 16:57:26 -07003255 @Override
Scott Kennedy3b965d72013-06-25 14:36:55 -07003256 public void onConversationSeen() {
3257 mPagerController.onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -07003258 }
3259
Andy Huang9d3fd922012-09-26 22:23:58 -07003260 @Override
3261 public boolean isInitialConversationLoading() {
3262 return mPagerController.isInitialConversationLoading();
3263 }
3264
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003265 /**
3266 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3267 * insufficient because that doesn't check if the window is currently in focus or not.
3268 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003269 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003270 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3271 }
3272
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003273 /**
3274 * This class handles callbacks that create a {@link ConversationCursor}.
3275 */
Andy Huangb1c34dc2012-04-17 16:36:19 -07003276 private class ConversationListLoaderCallbacks implements
3277 LoaderManager.LoaderCallbacks<ConversationCursor> {
3278
3279 @Override
3280 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003281 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3282 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
3283 if (account == null || folder == null) {
3284 return null;
3285 }
3286 return new ConversationCursorLoader((Activity) mActivity, account,
3287 folder.conversationListUri, folder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003288 }
3289
3290 @Override
3291 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang144bfe72013-06-11 13:27:52 -07003292 LogUtils.d(LOG_TAG,
3293 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s",
3294 data, loader, this);
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003295 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003296 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07003297 mConversationListLoadFinishedIgnored = true;
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003298 return;
3299 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003300 // Clear our all pending destructive actions before swapping the conversation cursor
3301 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003302 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003303 mConversationListCursor.addListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003304 mDrawIdler.setListener(mConversationListCursor);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003305 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003306 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003307 // Handle actions that were deferred until after the conversation list was loaded.
3308 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3309 callback.onLoadFinished();
3310 }
3311 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003312
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003313 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003314 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003315 // The conversation list is already listening to list changes and gets notified
3316 // in the mConversationListObservable.notifyChanged() line above. We only need to
3317 // check and inform the cursor of the change in visibility here.
3318 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003319 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003320 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003321 }
3322
3323 @Override
3324 public void onLoaderReset(Loader<ConversationCursor> loader) {
Andy Huang144bfe72013-06-11 13:27:52 -07003325 LogUtils.d(LOG_TAG,
3326 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s",
3327 mConversationListCursor, loader, this);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003328
3329 if (mConversationListCursor != null) {
3330 // Unregister the listener
3331 mConversationListCursor.removeListener(AbstractActivityController.this);
Andy Huang144bfe72013-06-11 13:27:52 -07003332 mDrawIdler.setListener(null);
Paul Westbrook9a70e912012-08-17 15:53:20 -07003333 mConversationListCursor = null;
3334
3335 // Inform anyone who is interested about the change
3336 mTracker.onCursorUpdated();
3337 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003338 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003339 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003340 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003341
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003342 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003343 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3344 */
3345 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3346 @Override
3347 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3348 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3349 switch (id) {
3350 case LOADER_FOLDER_CURSOR:
3351 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3352 final ObjectCursorLoader<Folder> loader = new
3353 ObjectCursorLoader<Folder>(
3354 mContext, mFolder.uri, everything, Folder.FACTORY);
3355 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3356 return loader;
3357 case LOADER_RECENT_FOLDERS:
3358 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003359 if (mAccount != null && mAccount.recentFolderListUri != null
3360 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003361 return new ObjectCursorLoader<Folder>(mContext,
3362 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3363 }
3364 break;
3365 case LOADER_ACCOUNT_INBOX:
3366 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3367 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3368 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3369 mAccount.folderListUri : defaultInbox;
3370 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3371 if (inboxUri != null) {
3372 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3373 everything, Folder.FACTORY);
3374 }
3375 break;
3376 case LOADER_SEARCH:
3377 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3378 return Folder.forSearchResults(mAccount,
3379 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
3380 mActivity.getActivityContext());
3381 case LOADER_FIRST_FOLDER:
3382 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3383 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3384 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3385 if (mConversationToShow != null && mConversationToShow.position < 0){
3386 mConversationToShow.position = 0;
3387 }
3388 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3389 everything, Folder.FACTORY);
3390 default:
3391 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3392 return null;
3393 }
3394 return null;
3395 }
3396
3397 @Override
3398 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3399 if (data == null) {
3400 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3401 }
3402 switch (loader.getId()) {
3403 case LOADER_FOLDER_CURSOR:
3404 if (data != null && data.moveToFirst()) {
3405 final Folder folder = data.getModel();
3406 setHasFolderChanged(folder);
3407 mFolder = folder;
3408 mFolderObservable.notifyChanged();
3409 } else {
3410 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
3411 mFolder != null ? mAccount.name : "");
3412 }
3413 break;
3414 case LOADER_RECENT_FOLDERS:
3415 // Few recent folders and we are running on a phone? Populate the default
3416 // recents. The number of default recent folders is at least 2: every provider
3417 // has at least two folders, and the recent folder count never decreases.
3418 // Having a single recent folder is an erroneous case, and we can gracefully
3419 // recover by populating default recents. The default recents will not stomp on
3420 // the existing value: it will be shown in addition to the default folders:
3421 // the max number of recent folders is more than 1+num(defaultRecents).
3422 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3423 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3424 @Override
3425 protected Void doInBackground(Uri... uri) {
3426 // Asking for an update on the URI and ignore the result.
3427 final ContentResolver resolver = mContext.getContentResolver();
3428 resolver.update(uri[0], null, null, null);
3429 return null;
3430 }
3431 }
3432 final Uri uri = mAccount.defaultRecentFolderListUri;
3433 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3434 new PopulateDefault().execute(uri);
3435 break;
3436 }
3437 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3438 mRecentFolderList.loadFromUiProvider(data);
3439 if (isAnimating()) {
3440 mRecentsDataUpdated = true;
3441 } else {
3442 mRecentFolderObservers.notifyChanged();
3443 }
3444 break;
3445 case LOADER_ACCOUNT_INBOX:
3446 if (data != null && !data.isClosed() && data.moveToFirst()) {
3447 final Folder inbox = data.getModel();
3448 onFolderChanged(inbox);
3449 // Just want to get the inbox, don't care about updates to it
3450 // as this will be tracked by the folder change listener.
3451 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3452 } else {
3453 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
3454 mAccount != null ? mAccount.name : "");
3455 }
3456 break;
3457 case LOADER_SEARCH:
3458 if (data != null && data.getCount() > 0) {
3459 data.moveToFirst();
3460 final Folder search = data.getModel();
3461 updateFolder(search);
3462 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3463 mActivity.getIntent()
3464 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3465 showConversationList(mConvListContext);
3466 mActivity.invalidateOptionsMenu();
3467 mHaveSearchResults = search.totalCount > 0;
3468 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3469 } else {
3470 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3471 }
3472 break;
3473 case LOADER_FIRST_FOLDER:
3474 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3475 return;
3476 }
3477 final Folder folder = data.getModel();
3478 boolean handled = false;
3479 if (folder != null) {
3480 onFolderChanged(folder);
3481 handled = true;
3482 }
3483 if (mConversationToShow != null) {
3484 // Open the conversation.
3485 showConversation(mConversationToShow);
3486 handled = true;
3487 }
3488 if (!handled) {
3489 // We have an account, but nothing else: load the default inbox.
3490 loadAccountInbox();
3491 }
3492 mConversationToShow = null;
3493 // And don't run this anymore.
3494 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3495 break;
3496 }
3497 }
3498
3499 @Override
3500 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3501 }
3502 }
3503
3504 /**
3505 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3506 */
3507 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3508 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3509 final CursorCreator<Account> mFactory = Account.FACTORY;
3510
3511 @Override
3512 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3513 switch (id) {
3514 case LOADER_ACCOUNT_CURSOR:
3515 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3516 return new ObjectCursorLoader<Account>(mContext,
3517 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3518 case LOADER_ACCOUNT_UPDATE_CURSOR:
3519 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3520 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3521 mFactory);
3522 default:
3523 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3524 break;
3525 }
3526 return null;
3527 }
3528
3529 @Override
3530 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3531 ObjectCursor<Account> data) {
3532 if (data == null) {
3533 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3534 }
3535 switch (loader.getId()) {
3536 case LOADER_ACCOUNT_CURSOR:
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003537 // We have received an update on the list of accounts.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003538 if (data == null) {
3539 // Nothing useful to do if we have no valid data.
3540 break;
3541 }
3542 if (data.getCount() == 0) {
3543 // If an empty cursor is returned, the MailAppProvider is indicating that
3544 // no accounts have been specified. We want to navigate to the
3545 // "add account" activity that will handle the intent returned by the
3546 // MailAppProvider
3547
3548 // If the MailAppProvider believes that all accounts have been loaded,
3549 // and the account list is still empty, we want to prompt the user to add
3550 // an account.
3551 final Bundle extras = data.getExtras();
3552 final boolean accountsLoaded =
3553 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3554
3555 if (accountsLoaded) {
3556 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3557 (mContext);
3558 if (noAccountIntent != null) {
3559 mActivity.startActivityForResult(noAccountIntent,
3560 ADD_ACCOUNT_REQUEST_CODE);
3561 }
3562 }
3563 } else {
3564 final boolean accountListUpdated = accountsUpdated(data);
Vikram Aggarwalc04cf7e2013-05-13 15:38:42 -07003565 if (!mHaveAccountList || accountListUpdated) {
3566 mHaveAccountList = updateAccounts(data);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003567 }
3568 }
3569 break;
3570 case LOADER_ACCOUNT_UPDATE_CURSOR:
3571 // We have received an update for current account.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003572 if (data != null && data.moveToFirst()) {
3573 final Account updatedAccount = data.getModel();
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003574 // Make sure that this is an update for the current account
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003575 if (updatedAccount.uri.equals(mAccount.uri)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003576 final Settings previousSettings = mAccount.settings;
3577
3578 // Update the controller's reference to the current account
3579 mAccount = updatedAccount;
3580 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3581 + "mAccount = %s", mAccount.uri);
3582
3583 // Only notify about a settings change if something differs
3584 if (!Objects.equal(mAccount.settings, previousSettings)) {
3585 mAccountObservers.notifyChanged();
3586 }
3587 perhapsEnterWaitMode();
3588 } else {
3589 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3590 + " %s", updatedAccount.uri, mAccount.uri);
3591 // We need to restart the loader, so the correct account information
3592 // will be returned.
3593 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3594 }
3595 }
3596 break;
3597 }
3598 }
3599
3600 @Override
3601 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
Vikram Aggarwald70fe492013-06-04 12:52:07 -07003602 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003603 }
3604 }
3605
3606 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003607 * Updates controller state based on search results and shows first conversation if required.
3608 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003609 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003610 if (mCurrentConversation == null) {
3611 // Shown for search results in two-pane mode only.
3612 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3613 && mConversationListCursor.getCount() > 0;
3614 if (!shouldShowFirstConversation()) {
3615 return;
3616 }
3617 mConversationListCursor.moveToPosition(0);
3618 final Conversation conv = new Conversation(mConversationListCursor);
3619 conv.position = 0;
3620 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003621 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003622 }
3623
3624 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003625 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3626 * next destructive action..
3627 * @param nextAction the next destructive action to be performed. This can be null.
3628 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003629 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003630 // If there is a pending action, perform that first.
3631 if (mPendingDestruction != null) {
3632 mPendingDestruction.performAction();
3633 }
3634 mPendingDestruction = nextAction;
3635 }
3636
3637 /**
3638 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003639 * 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 -07003640 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003641 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003642 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003643 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003644 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3645 // destructive action from the controller and for the controller to own the action. This is
3646 // a half-way solution while refactoring DestructiveAction.
3647 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003648 }
3649
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003650 @Override
3651 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003652 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003653 registerDestructiveAction(da);
3654 return da;
3655 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003656
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003657 @Override
3658 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07003659 return getDeferredAction(action, mSelectedSet.values(), true);
3660 }
3661
3662 /**
3663 * Get a destructive action for a menu action. This is a temporary method,
3664 * to control the profusion of {@link DestructiveAction} classes that are
3665 * created. Please do not copy this paradigm.
3666 * @param action the resource ID of the menu action: R.id.delete, for
3667 * example
3668 * @param target the conversations to act upon.
3669 * @return a {@link DestructiveAction} that performs the specified action.
3670 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003671 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
mindypf0656a12012-10-01 08:30:57 -07003672 boolean batch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003673 return new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003674 }
3675
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003676 /**
3677 * Class to change the folders that are assigned to a set of conversations. This is destructive
3678 * because the user can remove the current folder from the conversation, in which case it has
3679 * to be animated away from the current folder.
3680 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003681 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003682 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003683 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003684 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003685 /** Whether this destructive action has already been performed */
3686 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003687 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003688 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003689 private int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003690 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003691
3692 /**
3693 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003694 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003695 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003696 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003697 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003698 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003699 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003700 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003701 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003702 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003703 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003704 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003705 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003706 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003707 }
3708
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003709 @Override
3710 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003711 if (isPerformed()) {
3712 return;
3713 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07003714 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08003715 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003716 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003717 onUndoAvailable(undoOp);
3718 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003719 // For each conversation, for each operation, add/ remove the
3720 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003721 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3722 ArrayList<Uri> folderUris;
3723 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003724 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003725 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3726 .getRawFolders());
3727 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003728 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003729 if (mIsDestructive) {
3730 target.localDeleteOnUpdate = true;
3731 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003732 for (FolderOperation op : mFolderOps) {
mindypcb0b30e2012-11-30 10:16:35 -08003733 folderUris.add(op.mFolder.uri);
3734 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003735 if (op.mAdd) {
3736 targetFolders.put(op.mFolder.uri, op.mFolder);
3737 } else {
3738 targetFolders.remove(op.mFolder.uri);
3739 }
3740 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003741 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3742 folderUris, adds, targetFolders.values()));
mindyp389f0b22012-08-29 11:12:54 -07003743 }
3744 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003745 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003746 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003747 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003748 if (mIsSelectedSet) {
3749 mSelectedSet.clear();
3750 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003751 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003752
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003753 /**
3754 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003755 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003756 */
3757 private synchronized boolean isPerformed() {
3758 if (mCompleted) {
3759 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003760 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003761 mCompleted = true;
3762 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003763 }
3764 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003765
mindypc84759c2012-08-29 09:51:53 -07003766 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3767 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003768 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
mindypc84759c2012-08-29 09:51:53 -07003769 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003770 isBatch, showUndo, isMoveTo, actionFolder);
mindypc84759c2012-08-29 09:51:53 -07003771 registerDestructiveAction(da);
3772 return da;
3773 }
3774
3775 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003776 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003777 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
3778 return new FolderDestruction(target, folders, isDestructive, isBatch, showUndo,
3779 isMoveTo ? R.id.move_folder : R.id.change_folder, actionFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003780 }
3781
3782 @Override
3783 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3784 Folder toRemove, boolean isDestructive, boolean isBatch,
3785 boolean showUndo) {
3786 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3787 folderOps.add(new FolderOperation(toRemove, false));
3788 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003789 showUndo, R.id.remove_folder, mFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003790 }
3791
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003792 @Override
3793 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003794 final ConversationListFragment convList = getConversationListFragment();
3795 if (convList == null) {
3796 return;
3797 }
3798 convList.requestListRefresh();
3799 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003800
3801 protected final ActionClickedListener getUndoClickedListener(
3802 final AnimatedAdapter listAdapter) {
3803 return new ActionClickedListener() {
3804 @Override
3805 public void onActionClicked() {
3806 if (mAccount.undoUri != null) {
3807 // NOTE: We might want undo to return the messages affected, in which case
3808 // the resulting cursor might be interesting...
3809 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3810 // commands to undo
3811 if (mConversationListCursor != null) {
3812 mConversationListCursor.undo(
3813 mActivity.getActivityContext(), mAccount.undoUri);
3814 }
3815 if (listAdapter != null) {
3816 listAdapter.setUndo(true);
3817 }
3818 }
3819 }
3820 };
3821 }
3822
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003823 /**
3824 * Shows an error toast in the bottom when a folder was not fetched successfully.
3825 * @param folder the folder which could not be fetched.
3826 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3827 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003828 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003829 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003830
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003831 final ActionClickedListener listener;
3832 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003833 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003834 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003835 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003836 // The sync request that caused this failure.
3837 final int syncRequest = lastSyncResult >> 4;
3838 // Show: User explicitly pressed the refresh button and there is no connection
3839 // Show: The first time the user enters the app and there is no connection
3840 // TODO(viki): Implement this.
3841 // Reference: http://b/7202801
3842 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3843 // Don't show: Already in the app; user switches to a synced label
3844 // Don't show: In a live label and a background sync fails
3845 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3846 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3847 if (avoidToast) {
3848 return;
3849 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003850 listener = getRetryClickedListener(folder);
3851 actionTextResourceId = R.string.retry;
3852 break;
3853 case UIProvider.LastSyncResult.AUTH_ERROR:
3854 listener = getSignInClickedListener();
3855 actionTextResourceId = R.string.signin;
3856 break;
3857 case UIProvider.LastSyncResult.SECURITY_ERROR:
3858 return; // Currently we do nothing for security errors.
3859 case UIProvider.LastSyncResult.STORAGE_ERROR:
3860 listener = getStorageErrorClickedListener();
3861 actionTextResourceId = R.string.info;
3862 break;
3863 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3864 listener = getInternalErrorClickedListener();
3865 actionTextResourceId = R.string.report;
3866 break;
3867 default:
3868 return;
3869 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003870 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07003871 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003872 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003873 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003874 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003875 replaceVisibleToast,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003876 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003877 }
3878
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003879 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003880 return new ActionClickedListener() {
3881 @Override
3882 public void onActionClicked() {
3883 final Uri uri = folder.refreshUri;
3884
3885 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003886 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003887 }
3888 }
3889 };
3890 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003891
3892 private ActionClickedListener getSignInClickedListener() {
3893 return new ActionClickedListener() {
3894 @Override
3895 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003896 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003897 }
3898 };
3899 }
3900
3901 private ActionClickedListener getStorageErrorClickedListener() {
3902 return new ActionClickedListener() {
3903 @Override
3904 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003905 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003906 }
3907 };
3908 }
3909
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003910 private void showStorageErrorDialog() {
3911 DialogFragment fragment = (DialogFragment)
3912 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3913 if (fragment == null) {
3914 fragment = SyncErrorDialogFragment.newInstance();
3915 }
3916 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3917 }
3918
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003919 private ActionClickedListener getInternalErrorClickedListener() {
3920 return new ActionClickedListener() {
3921 @Override
3922 public void onActionClicked() {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003923 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003924 }
3925 };
3926 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003927
3928 @Override
3929 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3930 Uri uri = null;
3931 switch (errorStatus) {
3932 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3933 if (folder != null && folder.refreshUri != null) {
3934 uri = folder.refreshUri;
3935 }
3936 break;
3937 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003938 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003939 return;
3940 case UIProvider.LastSyncResult.SECURITY_ERROR:
3941 return; // Currently we do nothing for security errors.
3942 case UIProvider.LastSyncResult.STORAGE_ERROR:
3943 showStorageErrorDialog();
3944 return;
3945 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08003946 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003947 return;
3948 default:
3949 return;
3950 }
3951
3952 if (uri != null) {
3953 startAsyncRefreshTask(uri);
3954 }
3955 }
3956
3957 @Override
3958 public void onFooterViewLoadMoreClick(Folder folder) {
3959 if (folder != null && folder.loadMoreUri != null) {
3960 startAsyncRefreshTask(folder.loadMoreUri);
3961 }
3962 }
3963
3964 private void startAsyncRefreshTask(Uri uri) {
3965 if (mFolderSyncTask != null) {
3966 mFolderSyncTask.cancel(true);
3967 }
3968 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3969 mFolderSyncTask.execute();
3970 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07003971
3972 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07003973 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003974 final Intent authenticationIntent =
3975 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3976 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3977 }
3978 }
mindypca87de42012-09-28 15:02:39 -07003979
3980 @Override
3981 public void onAccessibilityStateChanged() {
3982 // Clear the cache of objects.
3983 ConversationItemViewModel.onAccessibilityUpdated();
3984 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003985 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07003986 if (frag != null) {
3987 AnimatedAdapter adapter = frag.getAnimatedAdapter();
3988 if (adapter != null) {
3989 adapter.notifyDataSetInvalidated();
3990 }
3991 }
3992 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003993
3994 @Override
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07003995 public void makeDialogListener (final int action, final boolean isBatch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003996 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003997 if (isBatch) {
3998 target = mSelectedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003999 } else {
4000 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
4001 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004002 }
4003 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004004 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08004005 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004006 mDialogListener = new AlertDialog.OnClickListener() {
4007 @Override
4008 public void onClick(DialogInterface dialog, int which) {
Scott Kennedycaaeed32013-06-12 13:39:16 -07004009 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004010 // Afterwards, let's remove references to the listener and the action.
4011 setListener(null, -1);
4012 }
4013 };
4014 }
4015
4016 @Override
4017 public AlertDialog.OnClickListener getListener() {
4018 return mDialogListener;
4019 }
4020
4021 /**
4022 * Sets the listener for the positive action on a confirmation dialog. Since only a single
4023 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
4024 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08004025 * @param listener the listener that will perform the task for this dialog's positive action.
4026 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08004027 */
4028 private void setListener(AlertDialog.OnClickListener listener, final int action){
4029 mDialogListener = listener;
4030 mDialogAction = action;
4031 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08004032
4033 @Override
4034 public VeiledAddressMatcher getVeiledAddressMatcher() {
4035 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004036 }
4037
4038 @Override
4039 public void setDetachedMode() {
4040 // Tell the conversation list not to select anything.
4041 final ConversationListFragment frag = getConversationListFragment();
4042 if (frag != null) {
4043 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004044 } else if (mIsTablet) {
4045 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004046 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
4047 }
4048 mDetachedConvUri = mCurrentConversation.uri;
4049 }
4050
4051 private void clearDetachedMode() {
4052 // Tell the conversation list to go back to its usual selection behavior.
4053 final ConversationListFragment frag = getConversationListFragment();
4054 if (frag != null) {
4055 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08004056 } else if (mIsTablet) {
4057 // How did we ever land here? Detached mode, and no CLF on tablet???
4058 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08004059 }
4060 mDetachedConvUri = null;
4061 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004062
4063 private class MailDrawerListener implements DrawerLayout.DrawerListener {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004064 private int mDrawerState;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004065 private float mOldSlideOffset;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004066
4067 public MailDrawerListener() {
4068 mDrawerState = DrawerLayout.STATE_IDLE;
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004069 mOldSlideOffset = 0.f;
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004070 }
4071
Andy Huang12b3ee42013-04-24 22:49:43 -07004072 @Override
4073 public void onDrawerOpened(View drawerView) {
4074 mDrawerToggle.onDrawerOpened(drawerView);
4075 }
4076
4077 @Override
4078 public void onDrawerClosed(View drawerView) {
4079 mDrawerToggle.onDrawerClosed(drawerView);
4080 if (mHasNewAccountOrFolder) {
4081 refreshDrawer();
4082 }
4083 }
4084
4085 /**
4086 * As part of the overriden function, it will animate the alpha of the conversation list
4087 * view along with the drawer sliding when we're in the process of switching accounts or
4088 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
4089 */
4090 @Override
4091 public void onDrawerSlide(View drawerView, float slideOffset) {
4092 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
4093 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
4094 mListViewForAnimating.setAlpha(slideOffset);
4095 }
Andrew Sappersteinfc5be1c2013-05-15 17:48:10 -07004096
4097 // This code handles when to change the visibility of action items
4098 // based on drawer state. The basic logic is that right when we
4099 // open the drawer, we hide the action items. We show the action items
4100 // when the drawer closes. However, due to the animation of the drawer closing,
4101 // to make the reshowing of the action items feel right, we make the items visible
4102 // slightly sooner.
4103 //
4104 // However, to make the animating behavior work properly, we have to know whether
4105 // we're animating open or closed. Only if we're animating closed do we want to
4106 // show the action items early. We save the last slide offset so that we can compare
4107 // the current slide offset to it to determine if we're opening or closing.
4108 if (mDrawerState == DrawerLayout.STATE_SETTLING) {
4109 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) {
4110 mHideMenuItems = false;
4111 mActivity.invalidateOptionsMenu();
4112 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) {
4113 mHideMenuItems = true;
4114 mActivity.invalidateOptionsMenu();
4115 }
4116 } else {
4117 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) {
4118 mHideMenuItems = false;
4119 mActivity.invalidateOptionsMenu();
4120 } else if (!mHideMenuItems && slideOffset > 0.f) {
4121 mHideMenuItems = true;
4122 mActivity.invalidateOptionsMenu();
4123 }
4124 }
4125
4126 mOldSlideOffset = slideOffset;
Andy Huang12b3ee42013-04-24 22:49:43 -07004127 }
4128
4129 /**
4130 * This condition here should only be called when the drawer is stuck in a weird state
4131 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
4132 * and, more importantly, unlock the drawer when this is the case.
4133 */
4134 @Override
4135 public void onDrawerStateChanged(int newState) {
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004136 mDrawerState = newState;
4137 mDrawerToggle.onDrawerStateChanged(mDrawerState);
4138 if (mDrawerState == DrawerLayout.STATE_IDLE) {
4139 if (mHasNewAccountOrFolder) {
4140 refreshDrawer();
4141 }
4142 if (mConversationListLoadFinishedIgnored) {
4143 mConversationListLoadFinishedIgnored = false;
4144 final Bundle args = new Bundle();
4145 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
4146 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
4147 mActivity.getLoaderManager().initLoader(
4148 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
4149 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004150 }
4151 }
4152
4153 /**
4154 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
4155 * conversation list, and finish end actions. Also, make
4156 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
4157 */
4158 public void refreshDrawer() {
4159 mHasNewAccountOrFolder = false;
4160 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4161 ConversationListFragment conversationList = getConversationListFragment();
4162 if (conversationList != null) {
4163 conversationList.clear();
4164 }
4165 mDrawerObservers.notifyChanged();
4166 }
Andrew Sappersteina3ce6782013-05-09 15:14:49 -07004167
4168 /**
4169 * Returns the most recent update of the {@link DrawerLayout}'s state provided
4170 * by {@link #onDrawerStateChanged(int)}.
4171 * @return The {@link DrawerLayout}'s current state. One of
4172 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
4173 * or {@link DrawerLayout#STATE_SETTLING}.
4174 */
4175 public int getDrawerState() {
4176 return mDrawerState;
4177 }
Andy Huang12b3ee42013-04-24 22:49:43 -07004178 }
4179
Scott Kennedy8a72b852013-05-02 14:18:50 -07004180 @Override
4181 public boolean isDrawerPullEnabled() {
4182 return getShouldAllowDrawerPull(mViewMode.getMode());
4183 }
Andrew Sapperstein5747e152013-05-13 14:13:08 -07004184
4185 @Override
4186 public boolean shouldHideMenuItems() {
4187 return mHideMenuItems;
4188 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08004189}