blob: 81ac1db6a48d7bdd5e1cf25b88886f9aba35f89b [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;
Paul Westbrookbf232c32012-04-18 03:17:41 -070069import com.android.mail.browse.ConversationPagerController;
Andy Huang839ada22012-07-20 15:48:40 -070070import com.android.mail.browse.MessageCursor.ConversationMessage;
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;
Paul Westbrookb334c902012-06-25 11:42:46 -070095import com.android.mail.utils.LogTag;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -080096import com.android.mail.utils.LogUtils;
Scott Kennedycb85aea2013-02-25 13:08:32 -080097import com.android.mail.utils.NotificationActionUtils;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -070098import com.android.mail.utils.Observable;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -080099import com.android.mail.utils.Utils;
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800100import com.android.mail.utils.VeiledAddressMatcher;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -0700101
Paul Westbrookca08fc12012-07-31 12:01:15 -0700102import com.google.common.base.Objects;
Paul Westbrook77eee622012-07-10 13:41:57 -0700103import com.google.common.collect.ImmutableList;
Mindy Pereiraacf60392012-04-06 09:11:00 -0700104import com.google.common.collect.Lists;
Marc Blank167faa82012-03-21 13:11:53 -0700105import com.google.common.collect.Sets;
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800106
Marc Blank167faa82012-03-21 13:11:53 -0700107import java.util.ArrayList;
Andy Huang9e4ca792013-02-28 14:33:43 -0800108import java.util.Arrays;
Mindy Pereirafbe40192012-03-20 10:40:45 -0700109import java.util.Collection;
Andy Huang839ada22012-07-20 15:48:40 -0700110import java.util.Collections;
Andy Huangc1fb9a92013-02-11 13:09:12 -0800111import java.util.Deque;
Mindy Pereira8db7e402012-07-13 10:32:47 -0700112import java.util.HashMap;
Vikram Aggarwalcc754b12012-08-30 14:04:21 -0700113import java.util.List;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800114import java.util.Set;
Marc Blankbf128eb2012-04-18 15:58:45 -0700115import java.util.TimerTask;
Paul Westbrook23b74b92012-02-29 11:36:12 -0800116
117
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800118/**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800119 * This is an abstract implementation of the Activity Controller. This class
120 * knows how to respond to menu items, state changes, layout changes, etc. It
121 * weaves together the views and listeners, dispatching actions to the
122 * respective underlying classes.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800123 * <p>
Mindy Pereira161f50d2012-02-28 15:47:19 -0800124 * Even though this class is abstract, it should provide default implementations
125 * for most, if not all the methods in the ActivityController interface. This
126 * makes the task of the subclasses easier: OnePaneActivityController and
127 * TwoPaneActivityController can be concise when the common functionality is in
128 * AbstractActivityController.
129 * </p>
130 * <p>
131 * In the Gmail codebase, this was called BaseActivityController
132 * </p>
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800133 */
Andrew Sappersteined5b52d2013-04-30 13:40:18 -0700134public abstract class AbstractActivityController implements ActivityController,
135 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800136 // Keys for serialization of various information in Bundles.
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700137 /** Tag for {@link #mAccount} */
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800138 private static final String SAVED_ACCOUNT = "saved-account";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700139 /** Tag for {@link #mFolder} */
Mindy Pereira5e478d22012-03-26 18:04:58 -0700140 private static final String SAVED_FOLDER = "saved-folder";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700141 /** Tag for {@link #mCurrentConversation} */
Mindy Pereira26f23fc2012-03-27 10:26:04 -0700142 private static final String SAVED_CONVERSATION = "saved-conversation";
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -0700143 /** Tag for {@link #mSelectedSet} */
144 private static final String SAVED_SELECTED_SET = "saved-selected-set";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700145 /** Tag for {@link ActionableToastBar#getOperation()} */
Mindy Pereirad33674992012-06-25 16:26:30 -0700146 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700147 /** Tag for {@link #mFolderListFolder} */
148 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700149 /** Tag for {@link ConversationListContext#searchQuery} */
150 private static final String SAVED_QUERY = "saved-query";
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800151 /** Tag for {@link #mDialogAction} */
152 private static final String SAVED_ACTION = "saved-action";
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800153 /** Tag for {@link #mDialogFromSelectedSet} */
154 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800155 /** Tag for {@link #mDetachedConvUri} */
156 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800157
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700158 /** Tag used when loading a wait fragment */
159 protected static final String TAG_WAIT = "wait-fragment";
160 /** Tag used when loading a conversation list fragment. */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700161 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700162 /** Tag used when loading a folder list fragment. */
163 protected static final String TAG_FOLDER_LIST = "tag-folder-list";
164
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700165 /** Key to store an account in a bundle */
166 private final String BUNDLE_ACCOUNT_KEY = "account";
167 /** Key to store a folder in a bundle */
168 private final String BUNDLE_FOLDER_KEY = "folder";
169
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800170 protected Account mAccount;
Mindy Pereira12a4d802012-06-22 09:24:43 -0700171 protected Folder mFolder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700172 /** True when {@link #mFolder} is first shown to the user. */
173 private boolean mFolderChanged = false;
Andy Huang6681e542012-06-14 14:36:45 -0700174 protected MailActionBarView mActionBarView;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700175 protected final ControllableActivity mActivity;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800176 protected final Context mContext;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700177 private final FragmentManager mFragmentManager;
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800178 protected final RecentFolderList mRecentFolderList;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800179 protected ConversationListContext mConvListContext;
Mindy Pereira9b875682012-02-15 18:10:54 -0800180 protected Conversation mCurrentConversation;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800181 /**
182 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
183 */
184 private Uri mDetachedConvUri;
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800185
Paul Westbrook6ead20d2012-03-19 14:48:14 -0700186 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
187 private SuppressNotificationReceiver mNewEmailReceiver = null;
188
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800189 /** Handler for all our local runnables. */
Mindy Pereirafbe40192012-03-20 10:40:45 -0700190 protected Handler mHandler = new Handler();
Mindy Pereirafa995b42012-07-25 12:06:13 -0700191
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800192 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800193 * The current mode of the application. All changes in mode are initiated by
194 * the activity controller. View mode changes are propagated to classes that
195 * attach themselves as listeners of view mode changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800196 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800197 protected final ViewMode mViewMode;
Vikram Aggarwal80aeac52012-02-07 15:27:20 -0800198 protected ContentResolver mResolver;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -0800199 protected boolean isLoaderInitialized = false;
Mindy Pereirab7b33e02012-02-21 15:32:19 -0800200 private AsyncRefreshTask mAsyncRefreshTask;
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800201
Andy Huang4e0158f2012-08-07 21:06:01 -0700202 private boolean mDestroyed;
203
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800204 /** True if running on tablet */
205 private final boolean mIsTablet;
206
Andy Huang1ee96b22012-08-24 20:19:53 -0700207 /**
208 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
209 * transactions? (including back stack manipulation)
210 * <p>
211 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
212 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
213 * and onResume.
214 */
215 private boolean mSafeToModifyFragments = true;
216
Paul Westbrook23b74b92012-02-29 11:36:12 -0800217 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
Mindy Pereira967ede62012-03-22 09:29:09 -0700218 protected ConversationCursor mConversationListCursor;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700219 private final DataSetObservable mConversationListObservable = new Observable("List");
Marc Blankbf128eb2012-04-18 15:58:45 -0700220
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800221 /** Runnable that checks the logging level to enable/disable the logging service. */
222 private Runnable mLogServiceChecker = null;
Vikram Aggarwalde60c9d2013-04-10 12:58:56 -0700223 /** List of all accounts currently known to the controller. This is never null. */
224 private Account[] mAllAccounts = new Account[0];
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800225
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700226 private FolderWatcher mFolderWatcher;
227
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800228 /**
229 * Interface for actions that are deferred until after a load completes. This is for handling
230 * user actions which affect cursors (e.g. marking messages read or unread) that happen before
231 * that cursor is loaded.
232 */
233 private interface LoadFinishedCallback {
234 void onLoadFinished();
235 }
236
237 /** The deferred actions to execute when mConversationListCursor load completes. */
238 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
239 new ArrayList<LoadFinishedCallback>();
240
Marc Blankbf128eb2012-04-18 15:58:45 -0700241 private RefreshTimerTask mConversationListRefreshTask;
Marc Blanke1d1b072012-04-13 17:29:16 -0700242
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700243 /** Listeners that are interested in changes to the current account. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700244 private final DataSetObservable mAccountObservers = new Observable("Account");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700245 /** Listeners that are interested in changes to the recent folders. */
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700246 private final DataSetObservable mRecentFolderObservers = new Observable("RecentFolder");
247 /** Listeners that are interested in changes to the list of all accounts. */
248 private final DataSetObservable mAllAccountObservers = new Observable("AllAccounts");
Vikram Aggarwal50ff0e52013-03-14 13:58:02 -0700249 /** Listeners that are interested in changes to the current folder. */
250 private final DataSetObservable mFolderObservable = new Observable("CurrentFolder");
Rohan Shah0f73d902013-04-19 17:06:37 -0700251 /** Listeners that are interested in changes to the drawer state. */
252 private final DataSetObservable mDrawerObservers = new Observable("Drawer");
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700253
Mindy Pereira967ede62012-03-22 09:29:09 -0700254 /**
255 * Selected conversations, if any.
256 */
Andy Huang4556a442012-03-30 16:42:05 -0700257 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
Mindy Pereiradac00542012-03-01 10:50:33 -0800258
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700259 private final int mFolderItemUpdateDelayMs;
260
Vikram Aggarwal135fd022012-04-11 14:44:15 -0700261 /** Keeps track of selected and unselected conversations */
Paul Westbrook937c94f2012-08-16 13:01:18 -0700262 final protected ConversationPositionTracker mTracker;
Vikram Aggarwalaf1ee0c2012-04-12 17:13:13 -0700263
Vikram Aggarwale128fc22012-04-04 12:33:34 -0700264 /**
265 * Action menu associated with the selected set.
266 */
267 SelectedConversationsActionMenu mCabActionMenu;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700268 protected ActionableToastBar mToastBar;
Andy Huang632721e2012-04-11 16:57:26 -0700269 protected ConversationPagerController mPagerController;
Mindy Pereiraab486362012-03-21 18:18:53 -0700270
Andy Huangb1c34dc2012-04-17 16:36:19 -0700271 // this is split out from the general loader dispatcher because its loader doesn't return a
272 // basic Cursor
273 private final ConversationListLoaderCallbacks mListCursorCallbacks =
274 new ConversationListLoaderCallbacks();
275
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800276 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */
277 private final FolderLoads mFolderCallbacks = new FolderLoads();
278 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */
279 private final AccountLoads mAccountCallbacks = new AccountLoads();
280
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800281 /**
282 * Matched addresses that must be shielded from users because they are temporary. Even though
283 * this is instantiated from settings, this matcher is valid for all accounts, and is expected
284 * to live past the life of an account.
285 */
286 private final VeiledAddressMatcher mVeiledMatcher;
287
Paul Westbrookb334c902012-06-25 11:42:46 -0700288 protected static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800289 /** Constants used to differentiate between the types of loaders. */
290 private static final int LOADER_ACCOUNT_CURSOR = 0;
Vikram Aggarwalfa4b47e2012-03-09 13:02:46 -0800291 private static final int LOADER_FOLDER_CURSOR = 2;
292 private static final int LOADER_RECENT_FOLDERS = 3;
Mindy Pereira967ede62012-03-22 09:29:09 -0700293 private static final int LOADER_CONVERSATION_LIST = 4;
Mindy Pereiraab486362012-03-21 18:18:53 -0700294 private static final int LOADER_ACCOUNT_INBOX = 5;
295 private static final int LOADER_SEARCH = 6;
Paul Westbrook2d50bcd2012-04-10 11:53:47 -0700296 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800297 /** Loader for showing the initial folder/conversation at app start. */
298 public static final int LOADER_FIRST_FOLDER = 8;
299
Vikram Aggarwal7a5d95a2012-07-27 16:24:54 -0700300 /**
301 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
302 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
303 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
304 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
305 * other class that uses this activity's LoaderManager. If another class needs activity-level
306 * loaders, consider consolidating the loaders in a central location: a UI-less fragment
307 * perhaps.
308 */
309 public static final int LAST_LOADER_ID = 100;
Scott Kennedy7c8325d2013-02-28 10:46:10 -0800310 /**
311 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or
312 * fragments, and within an activity, loader IDs need to be unique. Currently,
313 * {@link SectionedInboxTeaserView} is the only class that uses the
314 * {@link ConversationListFragment}'s LoaderManager.
315 */
316 public static final int LAST_FRAGMENT_LOADER_ID = 1000;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800317
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700318 private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
Paul Westbrook122f7c22012-08-20 17:50:31 -0700319 private static final int REAUTHENTICATE_REQUEST_CODE = 2;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700320
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700321 /** The pending destructive action to be carried out before swapping the conversation cursor.*/
322 private DestructiveAction mPendingDestruction;
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -0700323 protected AsyncRefreshTask mFolderSyncTask;
Mindy Pereirac975e842012-07-16 09:15:00 -0700324 private Folder mFolderListFolder;
mindyp5390fca2012-08-22 12:12:25 -0700325 private boolean mIsDragHappening;
mindypead50392012-08-23 11:03:53 -0700326 private int mShowUndoBarDelay;
mindyp6f54e1b2012-10-09 09:54:08 -0700327 private boolean mRecentsDataUpdated;
Vikram Aggarwala3f43d42012-10-25 16:21:30 -0700328 /** A wait fragment we added, if any. */
329 private WaitFragment mWaitFragment;
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -0800330 /** True if we have results from a search query */
331 private boolean mHaveSearchResults = false;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800332 /** If a confirmation dialog is being show, the listener for the positive action. */
333 private OnClickListener mDialogListener;
334 /**
335 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This
336 * is used to create a new {@link #mDialogListener} on orientation changes.
337 */
338 private int mDialogAction = -1;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -0800339 /**
340 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
341 * and false if it acts on the currently selected conversation
342 */
343 private boolean mDialogFromSelectedSet;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -0800344
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800345 /** Which conversation to show, if started from widget/notification. */
346 private Conversation mConversationToShow = null;
347
Andy Huangc1fb9a92013-02-11 13:09:12 -0800348 private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
349
Andy Huang12b3ee42013-04-24 22:49:43 -0700350 protected DrawerLayout mDrawerContainer;
351 protected View mDrawerPullout;
352 protected ActionBarDrawerToggle mDrawerToggle;
353 protected ListView mListViewForAnimating;
354 protected boolean mHasNewAccountOrFolder;
355 protected final MailDrawerListener mDrawerListener = new MailDrawerListener();
356
Andrew Sapperstein00179f12012-08-09 15:15:40 -0700357 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
Vikram Aggarwale8a85322012-04-24 09:01:38 -0700358
Scott Kennedycb85aea2013-02-25 13:08:32 -0800359 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
360 @Override
361 public void onChanged() {
362 super.onChanged();
363
364 if (mConversationListCursor != null) {
365 mConversationListCursor.handleNotificationActions();
366 }
367 }
368 };
369
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800370 public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
371 mActivity = activity;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700372 mFragmentManager = mActivity.getFragmentManager();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800373 mViewMode = viewMode;
374 mContext = activity.getApplicationContext();
Vikram Aggarwal025eba82012-05-08 10:45:30 -0700375 mRecentFolderList = new RecentFolderList(mContext);
Paul Westbrook937c94f2012-08-16 13:01:18 -0700376 mTracker = new ConversationPositionTracker(this);
Mindy Pereira967ede62012-03-22 09:29:09 -0700377 // Allow the fragment to observe changes to its own selection set. No other object is
378 // aware of the selected set.
379 mSelectedSet.addObserver(this);
Paul Westbrookc7a070f2012-04-12 01:46:41 -0700380
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800381 final Resources r = mContext.getResources();
382 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
383 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -0800384 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -0800385 mIsTablet = Utils.useTabletUI(r);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800386 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800387
388 @Override
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800389 public Account getCurrentAccount() {
390 return mAccount;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800391 }
392
393 @Override
394 public ConversationListContext getCurrentListContext() {
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800395 return mConvListContext;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800396 }
397
398 @Override
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800399 public String getHelpContext() {
Paul Westbrook30745b62012-08-19 14:10:32 -0700400 final int mode = mViewMode.getMode();
401 final int helpContextResId;
402 switch (mode) {
403 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
404 helpContextResId = R.string.wait_help_context;
405 break;
406 default:
407 helpContextResId = R.string.main_help_context;
408 }
409 return mContext.getString(helpContextResId);
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800410 }
411
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800412 @Override
Vikram Aggarwalef7c9922012-04-23 12:35:04 -0700413 public final ConversationCursor getConversationListCursor() {
Mindy Pereira967ede62012-03-22 09:29:09 -0700414 return mConversationListCursor;
415 }
416
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700417 /**
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700418 * Check if the fragment is attached to an activity and has a root view.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800419 * @param in fragment to be checked
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700420 * @return true if the fragment is valid, false otherwise
421 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800422 private static boolean isValidFragment(Fragment in) {
423 return !(in == null || in.getActivity() == null || in.getView() == null);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700424 }
425
426 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700427 * Get the conversation list fragment for this activity. If the conversation list fragment is
428 * not attached, this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700429 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700430 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
431 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
432 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
433 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
434 * need the fragment immediately after adding it, consider making the fragment an observer of
435 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700436 */
437 protected ConversationListFragment getConversationListFragment() {
438 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700439 if (isValidFragment(fragment)) {
440 return (ConversationListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700441 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700442 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700443 }
444
445 /**
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700446 * Returns the folder list fragment attached with this activity. If no such fragment is attached
447 * this method returns null.
Andy Huang839ada22012-07-20 15:48:40 -0700448 *
Vikram Aggarwal49e0e992012-09-21 13:53:15 -0700449 * Caution! This method returns the {@link FolderListFragment} after the fragment has been
450 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
451 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
452 * this call returns a non-null value, depending on the {@link FragmentManager}. If you
453 * need the fragment immediately after adding it, consider making the fragment an observer of
454 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700455 */
456 protected FolderListFragment getFolderListFragment() {
457 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700458 if (isValidFragment(fragment)) {
459 return (FolderListFragment) fragment;
Andy Huang9585d782012-04-16 19:45:04 -0700460 }
Vikram Aggarwal34a65012012-04-17 12:39:06 -0700461 return null;
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -0700462 }
463
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800464 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800465 * Initialize the action bar. This is not visible to OnePaneController and
466 * TwoPaneController so they cannot override this behavior.
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800467 */
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700468 private void initializeActionBar() {
469 final ActionBar actionBar = mActivity.getActionBar();
Andy Huang5895f7b2012-06-01 17:07:20 -0700470 if (actionBar == null) {
471 return;
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700472 }
Andy Huang5895f7b2012-06-01 17:07:20 -0700473
474 // be sure to inherit from the ActionBar theme when inflating
475 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
Mindy Pereira82faec72012-06-14 17:21:50 -0700476 final boolean isSearch = mActivity.getIntent() != null
477 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
478 mActionBarView = (MailActionBarView) inflater.inflate(
479 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
Vikram Aggarwaldac89fa2013-03-05 16:43:09 -0800480 mActionBarView.initialize(mActivity, this, actionBar);
Rohan Shah1dd054f2013-04-01 11:23:44 -0700481
Andy Huang12b3ee42013-04-24 22:49:43 -0700482 // init the action bar to allow the 'up' affordance.
483 // any configurations that disallow 'up' should do that later.
484 mActionBarView.setBackButton();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700485 }
486
487 /**
488 * Attach the action bar to the activity.
489 */
490 private void attachActionBar() {
491 final ActionBar actionBar = mActivity.getActionBar();
492 if (actionBar != null && mActionBarView != null) {
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -0800493 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
Vikram Aggarwal26b0bfd2013-03-29 13:05:08 -0700494 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700495 // Show a custom view and home icon, keep the title and subttitle
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -0700496 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
497 | ActionBar.DISPLAY_SHOW_HOME;
Vikram Aggarwalbc462ca2013-03-15 10:41:03 -0700498 actionBar.setDisplayOptions(mask, mask);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800499 }
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700500 mViewMode.addListener(mActionBarView);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800501 }
502
503 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -0800504 * Returns whether the conversation list fragment is visible or not.
505 * Different layouts will have their own notion on the visibility of
506 * fragments, so this method needs to be overriden.
507 *
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800508 */
509 protected abstract boolean isConversationListVisible();
510
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700511 /**
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700512 * If required, starts wait mode for the current account.
Vikram Aggarwal34f7b232012-10-17 13:32:23 -0700513 */
514 final void perhapsEnterWaitMode() {
515 // If the account is not initialized, then show the wait fragment, since nothing can be
516 // shown.
517 if (mAccount.isAccountInitializationRequired()) {
518 showWaitForInitialization();
519 return;
520 }
521
522 final boolean inWaitingMode = inWaitMode();
523 final boolean isSyncRequired = mAccount.isAccountSyncRequired();
524 if (isSyncRequired) {
525 if (inWaitingMode) {
526 // Update the WaitFragment's account object
527 updateWaitMode();
528 } else {
529 // Transition to waiting mode
530 showWaitForInitialization();
531 }
532 } else if (inWaitingMode) {
533 // Dismiss waiting mode
534 hideWaitForInitialization();
535 }
536 }
537
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800538 @Override
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700539 public void switchToDefaultInboxOrChangeAccount(Account account) {
540 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account);
Vikram Aggarwal66bc6ed2012-07-31 09:59:55 -0700541 final boolean firstLoad = mAccount == null;
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700542 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri);
Andrew Sapperstein252684a2013-04-17 14:43:26 -0700543 // if the active account has been clicked in the drawer, go to default inbox
544 if (switchToDefaultInbox) {
545 loadAccountInbox();
546 return;
547 }
Andrew Sapperstein4cbb0da2013-04-19 11:28:02 -0700548
549 changeAccount(account);
550 }
551
552 @Override
553 public void changeAccount(Account account) {
554 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account);
555 // Is the account or account settings different from the existing account?
556 final boolean firstLoad = mAccount == null;
557 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
558
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800559 // If nothing has changed, return early without wasting any more time.
560 if (!accountChanged && !account.settingsDiffer(mAccount)) {
561 return;
562 }
563 // We also don't want to do anything if the new account is null
564 if (account == null) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -0700565 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called.");
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800566 return;
567 }
568 final String accountName = account.name;
569 mHandler.post(new Runnable() {
570 @Override
571 public void run() {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800572 MailActivity.setNfcMessage(accountName);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700573 }
Vikram Aggarwal472e5362012-11-05 14:17:23 -0800574 });
575 if (accountChanged) {
576 commitDestructiveActions(false);
577 }
578 // Change the account here
579 setAccount(account);
580 // And carry out associated actions.
581 cancelRefreshTask();
582 if (accountChanged) {
583 loadAccountInbox();
584 }
585 // Check if we need to force setting up an account before proceeding.
586 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
587 // Launch the intent!
588 final Intent intent = new Intent(Intent.ACTION_EDIT);
589 intent.setData(mAccount.settings.setupIntentUri);
590 mActivity.startActivity(intent);
Mindy Pereira28d5f722012-02-15 12:32:40 -0800591 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800592 }
593
Vikram Aggarwale6340bc2012-03-26 15:57:09 -0700594 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700595 * Adds a listener interested in change in the current account. If a class is storing a
596 * reference to the current account, it should listen on changes, so it can receive updates to
597 * settings. Must happen in the UI thread.
598 */
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800599 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700600 public void registerAccountObserver(DataSetObserver obs) {
601 mAccountObservers.registerObserver(obs);
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800602 }
603
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700604 /**
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700605 * Removes a listener from receiving current account changes.
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700606 * Must happen in the UI thread.
607 */
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700608 @Override
609 public void unregisterAccountObserver(DataSetObserver obs) {
610 mAccountObservers.unregisterObserver(obs);
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700611 }
612
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700613 @Override
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -0700614 public void registerAllAccountObserver(DataSetObserver observer) {
615 mAllAccountObservers.registerObserver(observer);
616 }
617
618 @Override
619 public void unregisterAllAccountObserver(DataSetObserver observer) {
620 mAllAccountObservers.unregisterObserver(observer);
621 }
622
623 @Override
624 public Account[] getAllAccounts() {
625 return mAllAccounts;
626 }
627
628 @Override
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700629 public Account getAccount() {
630 return mAccount;
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700631 }
632
Rohan Shah0f73d902013-04-19 17:06:37 -0700633 @Override
634 public void registerDrawerClosedObserver(final DataSetObserver observer) {
635 mDrawerObservers.registerObserver(observer);
636 }
637
638 @Override
639 public void unregisterDrawerClosedObserver(final DataSetObserver observer) {
640 mDrawerObservers.unregisterObserver(observer);
641 }
642
643 /**
Andy Huang12b3ee42013-04-24 22:49:43 -0700644 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in
645 * the drawer to the left edge, disabling events, and refreshing it once it's either closed
646 * or put in an idle state.
Rohan Shah0f73d902013-04-19 17:06:37 -0700647 */
648 @Override
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700649 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount,
650 Folder nextFolder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700651 if (!isDrawerEnabled()) {
652 mDrawerObservers.notifyChanged();
653 return;
654 }
Andy Huang12b3ee42013-04-24 22:49:43 -0700655 // If there are no new folders or accounts to switch to, just close the drawer
656 if (!hasNewFolderOrAccount) {
657 mDrawerContainer.closeDrawers();
658 return;
659 }
Vikram Aggarwal2f9d3942013-05-03 12:31:39 -0700660 // Otherwise, start preloading the conversation list for the new folder.
661 if (nextFolder != null) {
662 preloadConvList(nextAccount, nextFolder);
663 }
664 // Remember if the conversation list view is animating
Andy Huang12b3ee42013-04-24 22:49:43 -0700665 final ConversationListFragment conversationList = getConversationListFragment();
666 if (conversationList != null) {
667 mListViewForAnimating = conversationList.getListView();
668 } else {
669 // There is no conversation list to animate, so just set it to null
670 mListViewForAnimating = null;
671 }
672
673 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
674 // Lets the drawer listener update the drawer contents and notify the FolderListFragment
675 mHasNewAccountOrFolder = true;
676 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
677 } else {
678 // Drawer is already closed, notify observers that is the case.
679 mDrawerObservers.notifyChanged();
680 }
Rohan Shah0f73d902013-04-19 17:06:37 -0700681 }
682
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700683 /**
684 * Load the conversation list early for the given folder.
685 * @param nextFolder
686 */
687 protected void preloadConvList(Account nextAccount, Folder nextFolder) {
688 // Fire off the conversation list loader for this account already with a fake
689 // listener.
690 final Bundle args = new Bundle();
691 if (nextAccount != null) {
692 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount);
693 } else {
694 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
695 }
696 if (nextFolder != null) {
697 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder);
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700698 } else {
699 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder");
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700700 }
701 mFolder = null;
702 final LoaderManager lm = mActivity.getLoaderManager();
703 lm.destroyLoader(LOADER_CONVERSATION_LIST);
704 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
705 }
706
Mindy Pereirae0828392012-03-08 10:38:40 -0800707 private void fetchSearchFolder(Intent intent) {
Vikram Aggarwal2b703c62012-09-18 13:54:15 -0700708 final Bundle args = new Bundle();
Mindy Pereiraab486362012-03-21 18:18:53 -0700709 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
Mindy Pereirae0828392012-03-08 10:38:40 -0800710 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800711 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks);
Mindy Pereirae0828392012-03-08 10:38:40 -0800712 }
713
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800714 @Override
Mindy Pereira28e0c342012-02-17 15:05:13 -0800715 public void onFolderChanged(Folder folder) {
Andy Huang12b3ee42013-04-24 22:49:43 -0700716 mDrawerContainer.closeDrawers();
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700717 changeFolder(folder, null);
718 }
719
720 /**
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700721 * Sets the folder state without changing view mode and without creating a list fragment, if
722 * possible.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800723 * @param folder the folder whose list of conversations are to be shown
724 * @param query the query string for a list of conversations matching a search
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700725 */
726 private void setListContext(Folder folder, String query) {
727 updateFolder(folder);
728 if (query != null) {
729 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
730 } else {
731 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
732 }
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700733 cancelRefreshTask();
734 }
735
736 /**
Vikram Aggarwalf3341402012-08-07 10:09:38 -0700737 * Changes the folder to the value provided here. This causes the view mode to change.
738 * @param folder the folder to change to
739 * @param query if non-null, this represents the search string that the folder represents.
740 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800741 private void changeFolder(Folder folder, String query) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700742 if (!Objects.equal(mFolder, folder)) {
743 commitDestructiveActions(false);
744 }
Mindy Pereiraa1f99982012-07-16 16:32:15 -0700745 if (folder != null && !folder.equals(mFolder)
746 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
Vikram Aggarwal203dc002012-08-23 13:59:04 -0700747 setListContext(folder, query);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800748 showConversationList(mConvListContext);
Vikram Aggarwal58ccd692013-03-28 11:29:22 -0700749 // Touch the current folder: it is different, and it has been accessed.
750 mRecentFolderList.touchFolder(mFolder, mAccount);
Mindy Pereira28e0c342012-02-17 15:05:13 -0800751 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -0800752 resetActionBarIcon();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800753 }
754
Mindy Pereira13c12a62012-05-31 15:41:08 -0700755 @Override
Mindy Pereira505df5f2012-06-19 17:57:17 -0700756 public void onFolderSelected(Folder folder) {
Mindy Pereira13c12a62012-05-31 15:41:08 -0700757 onFolderChanged(folder);
758 }
759
Vikram Aggarwal792ccba2012-03-27 13:46:57 -0700760 /**
Vikram Aggarwal58cad2e2012-08-28 16:18:23 -0700761 * Adds a listener interested in change in the recent folders. If a class is storing a
762 * reference to the recent folders, it should listen on changes, so it can receive updates.
763 * Must happen in the UI thread.
764 */
765 @Override
766 public void registerRecentFolderObserver(DataSetObserver obs) {
767 mRecentFolderObservers.registerObserver(obs);
768 }
769
770 /**
771 * Removes a listener from receiving recent folder changes.
772 * Must happen in the UI thread.
773 */
774 @Override
775 public void unregisterRecentFolderObserver(DataSetObserver obs) {
776 mRecentFolderObservers.unregisterObserver(obs);
777 }
778
779 @Override
780 public RecentFolderList getRecentFolders() {
781 return mRecentFolderList;
782 }
783
Vikram Aggarwaleb7d3292012-04-20 17:07:20 -0700784 @Override
785 public void loadAccountInbox() {
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700786 boolean handled = false;
787 if (mFolderWatcher != null) {
788 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount);
789 if (inbox != null) {
790 onFolderChanged(inbox);
791 handled = true;
792 }
793 }
794 if (!handled) {
795 LogUtils.w(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount);
796 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY);
797 }
Vikram Aggarwal8cbf2812013-04-11 17:23:45 -0700798 final int mode = mViewMode.getMode();
799 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
Andy Huange6459422013-04-01 16:32:18 -0700800 mViewMode.enterConversationListMode();
801 }
Mindy Pereiraf6acdad2012-03-15 11:21:13 -0700802 }
803
Vikram Aggarwal77ee0ce2013-04-28 15:32:36 -0700804 @Override
805 public void setFolderWatcher(FolderWatcher watcher) {
806 mFolderWatcher = watcher;
807 }
808
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700809 /**
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700810 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
811 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
812 * mFolder.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800813 * @param newFolder the new folder we are switching to.
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700814 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800815 private void setHasFolderChanged(final Folder newFolder) {
Vikram Aggarwald00229d2012-09-20 12:31:44 -0700816 // We should never try to assign a null folder. But in the rare event that we do, we should
817 // only set the bit when we have a valid folder, and null is not valid.
818 if (newFolder == null) {
819 return;
820 }
821 // If the previous folder was null, or if the two folders represent different data, then we
822 // consider that the folder has changed.
823 if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
824 mFolderChanged = true;
825 }
826 }
827
828 /**
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700829 * Sets the current folder if it is different from the object provided here. This method does
830 * NOT notify the folder observers that a change has happened. Observers are notified when we
831 * get an updated folder from the loaders, which will happen as a consequence of this method
832 * (since this method starts/restarts the loaders).
833 * @param folder The folder to assign
834 */
Mindy Pereira11e35962012-06-01 14:49:46 -0700835 private void updateFolder(Folder folder) {
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700836 if (folder == null || !folder.isInitialized()) {
837 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
838 return;
839 }
840 if (folder.equals(mFolder)) {
841 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
842 return;
843 }
844 final boolean wasNull = mFolder == null;
845 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
846 final LoaderManager lm = mActivity.getLoaderManager();
847 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to
848 // ensure that the folder is different from the previous folder before marking the
849 // folder changed.
850 setHasFolderChanged(folder);
851 mFolder = folder;
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700852
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700853 // We do not need to notify folder observers yet. Instead we start the loaders and
854 // when the load finishes, we will get an updated folder. Then, we notify the
855 // folderObservers in onLoadFinished.
856 mActionBarView.setFolder(mFolder);
Andy Huangb1c34dc2012-04-17 16:36:19 -0700857
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700858 // Only when we switch from one folder to another do we want to restart the
859 // folder and conversation list loaders (to trigger onCreateLoader).
860 // The first time this runs when the activity is [re-]initialized, we want to re-use the
861 // previous loader's instance and data upon configuration change (e.g. rotation).
862 // If there was not already an instance of the loader, init it.
863 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800864 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700865 } else {
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800866 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks);
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700867 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700868 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) {
869 // If there was an existing folder AND we have changed
Vikram Aggarwal1672ff82012-09-21 10:15:22 -0700870 // folders, we want to restart the loader to get the information
871 // for the newly selected folder
872 lm.destroyLoader(LOADER_CONVERSATION_LIST);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800873 }
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700874 final Bundle args = new Bundle();
Vikram Aggarwal2dc85042013-05-01 13:57:09 -0700875 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -0700876 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
877 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
Mindy Pereira5cb0c3e2012-02-22 15:22:47 -0800878 }
879
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800880 @Override
Andy Huang090db1e2012-07-25 13:25:28 -0700881 public Folder getFolder() {
882 return mFolder;
883 }
884
885 @Override
Mindy Pereirac975e842012-07-16 09:15:00 -0700886 public Folder getHierarchyFolder() {
887 return mFolderListFolder;
888 }
889
890 @Override
891 public void setHierarchyFolder(Folder folder) {
892 mFolderListFolder = folder;
Mindy Pereira23aadfd2012-05-25 11:24:33 -0700893 }
894
895 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800896 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Paul Westbrook122f7c22012-08-20 17:50:31 -0700897 switch (requestCode) {
898 case ADD_ACCOUNT_REQUEST_CODE:
899 // We were waiting for the user to create an account
900 if (resultCode == Activity.RESULT_OK) {
901 // restart the loader to get the updated list of accounts
Vikram Aggarwal177097f2013-03-08 11:19:53 -0800902 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
903 mAccountCallbacks);
Paul Westbrook122f7c22012-08-20 17:50:31 -0700904 } else {
905 // The user failed to create an account, just exit the app
906 mActivity.finish();
907 }
908 break;
909 case REAUTHENTICATE_REQUEST_CODE:
910 if (resultCode == Activity.RESULT_OK) {
911 // The user successfully authenticated, attempt to refresh the list
912 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
913 if (refreshUri != null) {
914 startAsyncRefreshTask(refreshUri);
915 }
916 }
917 break;
Paul Westbrook2388c5d2012-03-25 12:29:11 -0700918 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800919 }
920
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700921 /**
922 * Inform the conversation cursor that there has been a visibility change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -0800923 * @param visible true if the conversation list is visible, false otherwise.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700924 */
925 protected synchronized void informCursorVisiblity(boolean visible) {
926 if (mConversationListCursor != null) {
927 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
928 // We have informed the cursor. Subsequent visibility changes should not tell it that
929 // the folder has changed.
930 mFolderChanged = false;
931 }
932 }
933
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800934 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800935 public void onConversationListVisibilityChanged(boolean visible) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -0700936 informCursorVisiblity(visible);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -0800937 }
938
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800939 /**
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -0700940 * Called when a conversation is visible. Child classes must call the super class implementation
941 * before performing local computation.
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800942 */
943 @Override
944 public void onConversationVisibilityChanged(boolean visible) {
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800945 }
946
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800947 /**
948 * Initialize development time logging. This can potentially log a lot of PII, and we don't want
949 * to turn it on for shipped versions.
950 */
951 private void initializeDevLoggingService() {
952 if (!MailLogService.DEBUG_ENABLED) {
953 return;
954 }
955 // Check every 5 minutes.
956 final int WAIT_TIME = 5 * 60 * 1000;
957 // Start a runnable that periodically checks the log level and starts/stops the service.
958 mLogServiceChecker = new Runnable() {
959 /** True if currently logging. */
960 private boolean mCurrentlyLogging = false;
961
962 /**
963 * If the logging level has been changed since the previous run, start or stop the
964 * service.
965 */
966 private void startOrStopService() {
967 // If the log level is already high, start the service.
968 final Intent i = new Intent(mContext, MailLogService.class);
969 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough();
970 if (mCurrentlyLogging == loggingEnabled) {
971 // No change since previous run, just return;
972 return;
973 }
974 if (loggingEnabled) {
975 LogUtils.e(LOG_TAG, "Starting MailLogService");
976 mContext.startService(i);
977 } else {
978 LogUtils.e(LOG_TAG, "Stopping MailLogService");
979 mContext.stopService(i);
980 }
981 mCurrentlyLogging = loggingEnabled;
982 }
983
984 @Override
985 public void run() {
986 startOrStopService();
987 mHandler.postDelayed(this, WAIT_TIME);
988 }
989 };
990 // Start the runnable right away.
991 mHandler.post(mLogServiceChecker);
992 }
993
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -0800994 @Override
Vikram Aggarwald7a12cd2012-02-03 09:36:20 -0800995 public boolean onCreate(Bundle savedState) {
Vikram Aggarwalabd24d82012-04-26 13:23:14 -0700996 initializeActionBar();
Vikram Aggarwal59f741f2013-03-01 15:55:40 -0800997 initializeDevLoggingService();
Vikram Aggarwala55b36c2012-01-13 11:45:02 -0800998 // Allow shortcut keys to function for the ActionBar and menus.
999 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
Vikram Aggarwal80aeac52012-02-07 15:27:20 -08001000 mResolver = mActivity.getContentResolver();
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001001 mNewEmailReceiver = new SuppressNotificationReceiver();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001002 mRecentFolderList.initialize(mActivity);
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08001003 mVeiledMatcher.initialize(this);
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001004
Andy Huang12b3ee42013-04-24 22:49:43 -07001005 mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer,
1006 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
1007 mDrawerContainer.setDrawerListener(new MailDrawerListener());
1008 mDrawerContainer.setDrawerShadow(
1009 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
1010
1011 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled());
1012
Mindy Pereira161f50d2012-02-28 15:47:19 -08001013 // All the individual UI components listen for ViewMode changes. This
Mindy Pereirab849dfb2012-03-07 18:13:15 -08001014 // simplifies the amount of logic in the AbstractActivityController, but increases the
1015 // possibility of timing-related bugs.
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001016 mViewMode.addListener(this);
Andy Huang632721e2012-04-11 16:57:26 -07001017 mPagerController = new ConversationPagerController(mActivity, this);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07001018 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
Vikram Aggarwalada51782012-04-26 14:18:31 -07001019 attachActionBar();
Mark Wei9eb1c9a2012-10-01 12:54:50 -07001020 FolderSelectionDialog.setDialogDismissed();
Andy Huang632721e2012-04-11 16:57:26 -07001021
1022 final Intent intent = mActivity.getIntent();
Vikram Aggarwalabd24d82012-04-26 13:23:14 -07001023 // Immediately handle a clean launch with intent, and any state restoration
Andy Huang632721e2012-04-11 16:57:26 -07001024 // that does not rely on restored fragments or loader data
1025 // any state restoration that relies on those can be done later in
1026 // onRestoreInstanceState, once fragments are up and loader data is re-delivered
1027 if (savedState != null) {
1028 if (savedState.containsKey(SAVED_ACCOUNT)) {
1029 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
Andy Huang632721e2012-04-11 16:57:26 -07001030 }
1031 if (savedState.containsKey(SAVED_FOLDER)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001032 final Folder folder = savedState.getParcelable(SAVED_FOLDER);
Vikram Aggarwal203dc002012-08-23 13:59:04 -07001033 final String query = savedState.getString(SAVED_QUERY, null);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001034 setListContext(folder, query);
Andy Huang632721e2012-04-11 16:57:26 -07001035 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001036 if (savedState.containsKey(SAVED_ACTION)) {
1037 mDialogAction = savedState.getInt(SAVED_ACTION);
1038 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001039 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001040 mViewMode.handleRestore(savedState);
Andy Huang632721e2012-04-11 16:57:26 -07001041 } else if (intent != null) {
1042 handleIntent(intent);
1043 }
Andy Huang632721e2012-04-11 16:57:26 -07001044 // Create the accounts loader; this loads the account switch spinner.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001045 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
1046 mAccountCallbacks);
Andy Huang632721e2012-04-11 16:57:26 -07001047 return true;
Andy Huangb1c34dc2012-04-17 16:36:19 -07001048 }
1049
1050 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001051 public void onPostCreate(Bundle savedState) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001052 // Sync the toggle state after onRestoreInstanceState has occurred.
1053 mDrawerToggle.syncState();
Paul Westbrook57246a42013-04-21 09:40:22 -07001054 }
1055
1056 @Override
1057 public void onConfigurationChanged(Configuration newConfig) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001058 mDrawerToggle.onConfigurationChanged(newConfig);
1059 }
1060
1061 /**
1062 * If drawer is open/visible (even partially), close it.
1063 */
1064 protected void closeDrawerIfOpen() {
1065 if (!isDrawerEnabled()) {
1066 return;
1067 }
1068 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1069 mDrawerContainer.closeDrawers();
1070 }
Paul Westbrook57246a42013-04-21 09:40:22 -07001071 }
1072
1073 @Override
Andy Huang1ee96b22012-08-24 20:19:53 -07001074 public void onStart() {
1075 mSafeToModifyFragments = true;
Scott Kennedycb85aea2013-02-25 13:08:32 -08001076
1077 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
Andy Huang1ee96b22012-08-24 20:19:53 -07001078 }
1079
1080 @Override
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001081 public void onRestart() {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001082 final DialogFragment fragment = (DialogFragment)
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001083 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
1084 if (fragment != null) {
1085 fragment.dismiss();
1086 }
mindypea04f932012-08-27 14:17:59 -07001087 // When the user places the app in the background by pressing "home",
1088 // dismiss the toast bar. However, since there is no way to determine if
1089 // home was pressed, just dismiss any existing toast bar when restarting
1090 // the app.
1091 if (mToastBar != null) {
1092 mToastBar.hide(false);
1093 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07001094 }
1095
1096 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001097 public Dialog onCreateDialog(int id, Bundle bundle) {
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001098 return null;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001099 }
1100
1101 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001102 public final boolean onCreateOptionsMenu(Menu menu) {
Vikram Aggarwale5e917c2012-09-20 16:27:41 -07001103 final MenuInflater inflater = mActivity.getMenuInflater();
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001104 inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
Mindy Pereira68f2e222012-03-07 10:36:54 -08001105 mActionBarView.onCreateOptionsMenu(menu);
Mindy Pereira28d5f722012-02-15 12:32:40 -08001106 return true;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001107 }
1108
1109 @Override
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07001110 public final boolean onKeyDown(int keyCode, KeyEvent event) {
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001111 return false;
1112 }
1113
mindyp17a8e782012-11-29 14:56:17 -08001114 public abstract boolean doesActionChangeConversationListVisibility(int action);
1115
Vikram Aggarwalb9e1a352012-01-24 15:23:38 -08001116 @Override
Paul Westbrook57246a42013-04-21 09:40:22 -07001117 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huang12b3ee42013-04-24 22:49:43 -07001118 /*
1119 * The action bar home/up action should open or close the drawer.
1120 * mDrawerToggle will take care of this.
1121 */
1122 if (mDrawerToggle.onOptionsItemSelected(item)) {
1123 return true;
1124 }
1125
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001126 final int id = item.getItemId();
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07001127 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
Mindy Pereira9b875682012-02-15 18:10:54 -08001128 boolean handled = true;
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001129 /** This is NOT a batch action. */
1130 final boolean isBatch = false;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001131 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwal112cd162012-06-18 10:51:13 -07001132 final Settings settings = (mAccount == null) ? null : mAccount.settings;
mindyp84f7d322012-10-01 17:14:40 -07001133 // The user is choosing a new action; commit whatever they had been
mindyp17a8e782012-11-29 14:56:17 -08001134 // doing before. Don't animate if we are launching a new screen.
1135 commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
Mindy Pereira28d5f722012-02-15 12:32:40 -08001136 switch (id) {
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001137 case R.id.archive: {
1138 final boolean showDialog = (settings != null && settings.confirmArchive);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001139 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001140 break;
1141 }
Mindy Pereira01f30502012-08-14 10:30:51 -07001142 case R.id.remove_folder:
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001143 delete(R.id.remove_folder, target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001144 getDeferredRemoveFolder(target, mFolder, true, isBatch, true), isBatch);
Mindy Pereira01f30502012-08-14 10:30:51 -07001145 break;
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001146 case R.id.delete: {
1147 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001148 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001149 break;
1150 }
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001151 case R.id.discard_drafts: {
Paul Westbrookef362542012-08-27 14:53:32 -07001152 final boolean showDialog = (settings != null && settings.confirmDelete);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001153 confirmAndDelete(id, target, showDialog,
1154 R.plurals.confirm_discard_drafts_conversation);
Paul Westbrookef362542012-08-27 14:53:32 -07001155 break;
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001156 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001157 case R.id.mark_important:
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001158 updateConversation(Conversation.listOf(mCurrentConversation),
1159 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001160 break;
1161 case R.id.mark_not_important:
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001162 if (mFolder != null && mFolder.isImportantOnly()) {
Vikram Aggarwala8e43182012-09-13 12:55:10 -07001163 delete(R.id.mark_not_important, target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001164 getDeferredAction(R.id.mark_not_important, target, isBatch), isBatch);
Mindy Pereira0d03ef82012-08-15 09:05:48 -07001165 } else {
1166 updateConversation(Conversation.listOf(mCurrentConversation),
1167 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1168 }
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001169 break;
1170 case R.id.mute:
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001171 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch), isBatch);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001172 break;
1173 case R.id.report_spam:
mindyp84f7d322012-10-01 17:14:40 -07001174 delete(R.id.report_spam, target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001175 getDeferredAction(R.id.report_spam, target, isBatch), isBatch);
Mindy Pereiraba68fda2012-05-24 15:53:06 -07001176 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07001177 case R.id.mark_not_spam:
mindyp84f7d322012-10-01 17:14:40 -07001178 // Currently, since spam messages are only shown in list with
1179 // other spam messages,
Paul Westbrook77eee622012-07-10 13:41:57 -07001180 // marking a message not as spam is a destructive action
mindyp84f7d322012-10-01 17:14:40 -07001181 delete(R.id.mark_not_spam, target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001182 getDeferredAction(R.id.mark_not_spam, target, isBatch), isBatch);
Paul Westbrook77eee622012-07-10 13:41:57 -07001183 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07001184 case R.id.report_phishing:
mindyp84f7d322012-10-01 17:14:40 -07001185 delete(R.id.report_phishing, target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001186 getDeferredAction(R.id.report_phishing, target, isBatch), isBatch);
Paul Westbrook76b20622012-07-12 11:45:43 -07001187 break;
Mindy Pereiraf5acda42012-02-15 20:13:59 -08001188 case android.R.id.home:
1189 onUpPressed();
1190 break;
Mindy Pereira9b875682012-02-15 18:10:54 -08001191 case R.id.compose:
1192 ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
1193 break;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001194 case R.id.refresh:
1195 requestFolderRefresh();
1196 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001197 case R.id.settings:
1198 Utils.showSettings(mActivity.getActivityContext(), mAccount);
Paul Westbrook2861b6a2012-02-15 15:25:34 -08001199 break;
Paul Westbrooke5503552012-03-28 00:35:57 -07001200 case R.id.folder_options:
1201 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
1202 break;
Paul Westbrook94e440d2012-02-24 11:03:47 -08001203 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -07001204 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
Paul Westbrook94e440d2012-02-24 11:03:47 -08001205 break;
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001206 case R.id.feedback_menu_item:
Paul Westbrook83e6b572013-02-05 16:22:42 -08001207 Utils.sendFeedback(mActivity, mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001208 break;
Paul Westbrook18babd22012-04-09 22:17:08 -07001209 case R.id.manage_folders_item:
1210 Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
1211 break;
Rohan Shahaab9bc72013-02-07 14:18:05 -08001212 case R.id.move_to:
1213 /* fall through */
Vikram Aggarwald503df42012-05-11 10:13:35 -07001214 case R.id.change_folder:
Mark Wei8f98ac02012-10-01 17:05:08 -07001215 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
1216 mActivity.getActivityContext(), mAccount, this,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001217 Conversation.listOf(mCurrentConversation), isBatch, mFolder,
Rohan Shahaab9bc72013-02-07 14:18:05 -08001218 id == R.id.move_to);
Mark Wei9eb1c9a2012-10-01 12:54:50 -07001219 if (dialog != null) {
1220 dialog.show();
mindypa7e15452012-09-18 14:22:11 -07001221 }
Vikram Aggarwald503df42012-05-11 10:13:35 -07001222 break;
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001223 case R.id.empty_trash:
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001224 showEmptyDialog();
1225 break;
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001226 case R.id.empty_spam:
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001227 showEmptyDialog();
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001228 break;
Mindy Pereira9b875682012-02-15 18:10:54 -08001229 default:
1230 handled = false;
1231 break;
Mindy Pereira28d5f722012-02-15 12:32:40 -08001232 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001233 return handled;
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001234 }
1235
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001236 /**
1237 * Opens an {@link EmptyFolderDialogFragment} for the current folder.
1238 */
1239 private void showEmptyDialog() {
1240 if (mFolder != null) {
1241 final EmptyFolderDialogFragment fragment =
1242 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type);
1243 fragment.setListener(this);
1244 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG);
1245 }
1246 }
1247
1248 @Override
1249 public void onFolderEmptied() {
1250 emptyFolder();
1251 }
1252
1253 /**
1254 * Performs the work of emptying the currently visible folder.
1255 */
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001256 private void emptyFolder() {
Yorke Lee1d0f1f82013-04-26 14:10:27 -07001257 if (mConversationListCursor != null) {
1258 mConversationListCursor.emptyFolder();
1259 }
Scott Kennedy7ee089e2013-03-25 17:05:44 -04001260 }
1261
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001262 private void attachEmptyFolderDialogFragmentListener() {
1263 final EmptyFolderDialogFragment fragment =
1264 (EmptyFolderDialogFragment) mActivity.getFragmentManager()
1265 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG);
1266
1267 if (fragment != null) {
1268 fragment.setListener(this);
1269 }
1270 }
1271
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001272 /**
Andy Huang12b3ee42013-04-24 22:49:43 -07001273 * Toggles the drawer pullout. If it was open (Fully extended), the
1274 * drawer will be closed. Otherwise, the drawer will be opened. This should
1275 * only be called when used with a toggle item. Other cases should be handled
1276 * explicitly with just closeDrawers() or openDrawer(View drawerView);
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001277 */
1278 protected void toggleFolderListState() {
Andy Huang12b3ee42013-04-24 22:49:43 -07001279 if (!isDrawerEnabled()) {
1280 return;
1281 }
1282 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
1283 mDrawerContainer.closeDrawers();
1284 } else {
1285 mDrawerContainer.openDrawer(mDrawerPullout);
1286 }
Rohan Shah8e65c6d2013-03-07 16:47:25 -08001287 }
1288
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001289 @Override
Andy Huangc1fb9a92013-02-11 13:09:12 -08001290 public final boolean onUpPressed() {
1291 for (UpOrBackHandler h : mUpOrBackHandlers) {
1292 if (h.onUpPressed()) {
1293 return true;
1294 }
1295 }
1296 return handleUpPress();
1297 }
1298
1299 @Override
1300 public final boolean onBackPressed() {
1301 for (UpOrBackHandler h : mUpOrBackHandlers) {
1302 if (h.onBackPressed()) {
1303 return true;
1304 }
1305 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001306
1307 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) {
1308 mDrawerContainer.closeDrawers();
1309 return true;
1310 }
1311
Andy Huangc1fb9a92013-02-11 13:09:12 -08001312 return handleBackPress();
1313 }
1314
1315 protected abstract boolean handleBackPress();
1316 protected abstract boolean handleUpPress();
1317
1318 @Override
1319 public void addUpOrBackHandler(UpOrBackHandler handler) {
1320 if (mUpOrBackHandlers.contains(handler)) {
1321 return;
1322 }
1323 mUpOrBackHandlers.addFirst(handler);
1324 }
1325
1326 @Override
1327 public void removeUpOrBackHandler(UpOrBackHandler handler) {
1328 mUpOrBackHandlers.remove(handler);
1329 }
1330
1331 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001332 public void updateConversation(Collection<Conversation> target, ContentValues values) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001333 mConversationListCursor.updateValues(target, values);
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001334 refreshConversationList();
1335 }
1336
1337 @Override
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001338 public void updateConversation(Collection <Conversation> target, String columnName,
1339 boolean value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001340 mConversationListCursor.updateBoolean(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001341 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001342 }
1343
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001344 @Override
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001345 public void updateConversation(Collection <Conversation> target, String columnName,
1346 int value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001347 mConversationListCursor.updateInt(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001348 refreshConversationList();
Mindy Pereirac9d59182012-03-22 16:06:46 -07001349 }
1350
Vikram Aggarwal531488e2012-05-29 16:36:52 -07001351 @Override
1352 public void updateConversation(Collection <Conversation> target, String columnName,
1353 String value) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001354 mConversationListCursor.updateString(target, columnName, value);
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001355 refreshConversationList();
Vikram Aggarwal54452ae2012-03-13 15:29:00 -07001356 }
1357
Andy Huang839ada22012-07-20 15:48:40 -07001358 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001359 public void markConversationMessagesUnread(final Conversation conv,
1360 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
Andy Huang8f6b0062012-07-31 15:36:31 -07001361 // The only caller of this method is the conversation view, from where marking unread should
1362 // *always* take you back to list mode.
1363 showConversation(null);
1364
Andy Huang839ada22012-07-20 15:48:40 -07001365 // locally mark conversation unread (the provider is supposed to propagate message unread
1366 // to conversation unread)
1367 conv.read = false;
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001368 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001369 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
Paul Westbrook1faf93d2012-10-16 08:58:07 -07001370
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001371 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1372 @Override
1373 public void onLoadFinished() {
1374 doMarkConversationMessagesUnread(conv, unreadMessageUris,
1375 originalConversationInfo);
1376 }
1377 });
1378 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001379 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001380 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1381 }
1382 }
1383
1384 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1385 byte[] originalConversationInfo) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001386 // Only do a granular 'mark unread' if a subset of messages are unread
Andy Huang28e31e22012-07-26 16:33:15 -07001387 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
Mindy Pereira0972e072012-08-01 17:43:06 -07001388 final int numMessages = conv.getNumMessages();
1389 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1390 && unreadCount < numMessages);
Andy Huang28e31e22012-07-26 16:33:15 -07001391
Andy Huang9e4ca792013-02-28 14:33:43 -08001392 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)"
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001393 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
Andy Huang9e4ca792013-02-28 14:33:43 -08001394 conv, numMessages, unreadCount, subsetIsUnread);
Andy Huang28e31e22012-07-26 16:33:15 -07001395 if (!subsetIsUnread) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001396 // Conversations are neither marked read, nor viewed, and we don't want to show
1397 // the next conversation.
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001398 LogUtils.d(LOG_TAG, ". . doing full mark unread");
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001399 markConversationsRead(Collections.singletonList(conv), false, false, false);
Andy Huang839ada22012-07-20 15:48:40 -07001400 } else {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001401 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1402 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1403 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1404 info);
1405 }
Andy Huangdaa06ab2012-07-24 10:46:44 -07001406 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
Andy Huang839ada22012-07-20 15:48:40 -07001407
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001408 // Locally update conversation's conversationInfo to revert to original version
Andy Huang28e31e22012-07-26 16:33:15 -07001409 if (originalConversationInfo != null) {
1410 mConversationListCursor.setConversationColumn(conv.uri,
1411 ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1412 }
Andy Huang839ada22012-07-20 15:48:40 -07001413
1414 // applyBatch with each CPO as an UPDATE op on each affected message uri
1415 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1416 String authority = null;
1417 for (Uri messageUri : unreadMessageUris) {
1418 if (authority == null) {
1419 authority = messageUri.getAuthority();
1420 }
1421 ops.add(ContentProviderOperation.newUpdate(messageUri)
1422 .withValue(UIProvider.MessageColumns.READ, 0)
1423 .build());
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001424 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
Andy Huang839ada22012-07-20 15:48:40 -07001425 }
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001426 LogUtils.d(LOG_TAG, ". . operations = %s", ops);
Andy Huang839ada22012-07-20 15:48:40 -07001427 new ContentProviderTask() {
1428 @Override
1429 protected void onPostExecute(Result result) {
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001430 if (result.exception != null) {
1431 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR.");
1432 } else {
Andy Huang9e4ca792013-02-28 14:33:43 -08001433 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s",
1434 Arrays.toString(result.results));
Vikram Aggarwald9895b62013-02-27 16:52:27 -08001435 }
Andy Huang839ada22012-07-20 15:48:40 -07001436 }
1437 }.run(mResolver, authority, ops);
Andy Huang839ada22012-07-20 15:48:40 -07001438 }
Andy Huang839ada22012-07-20 15:48:40 -07001439 }
1440
1441 @Override
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001442 public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1443 final boolean viewed) {
1444 if (mConversationListCursor == null) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001445 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1446 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1447 targets.toArray());
1448 }
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001449 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1450 @Override
1451 public void onLoadFinished() {
1452 markConversationsRead(targets, read, viewed, true);
1453 }
1454 });
1455 } else {
1456 // We want to show the next conversation if we are marking unread.
1457 markConversationsRead(targets, read, viewed, true);
1458 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001459 }
1460
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001461 private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1462 final boolean markViewed, final boolean showNext) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001463 LogUtils.d(LOG_TAG, "performing markConversationsRead");
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001464 // Auto-advance if requested and the current conversation is being marked unread
Andy Huang8f6b0062012-07-31 15:36:31 -07001465 if (showNext && !read) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001466 final Runnable operation = new Runnable() {
1467 @Override
1468 public void run() {
1469 markConversationsRead(targets, read, markViewed, showNext);
1470 }
1471 };
1472
1473 if (!showNextConversation(targets, operation)) {
1474 // This method will be called again if the user selects an autoadvance option
1475 return;
1476 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001477 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001478
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001479 final int size = targets.size();
1480 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1481 for (final Conversation target : targets) {
1482 final ContentValues value = new ContentValues();
1483 value.put(ConversationColumns.READ, read);
Paul Westbrook5109c512012-11-05 11:00:30 -08001484
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001485 // We never want to mark unseen here, but we do want to mark it seen
1486 if (read || markViewed) {
1487 value.put(ConversationColumns.SEEN, Boolean.TRUE);
1488 }
1489
Paul Westbrook5109c512012-11-05 11:00:30 -08001490 // The mark read/unread/viewed operations do not show an undo bar
1491 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001492 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001493 value.put(ConversationColumns.VIEWED, true);
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -07001494 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001495 final ConversationInfo info = target.conversationInfo;
Andy Huang839ada22012-07-20 15:48:40 -07001496 if (info != null) {
mindyp7f55c682012-10-04 11:38:27 -07001497 boolean changed = info.markRead(read);
1498 if (changed) {
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08001499 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
mindyp7f55c682012-10-04 11:38:27 -07001500 }
Andy Huang839ada22012-07-20 15:48:40 -07001501 }
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001502 opList.add(mConversationListCursor.getOperationForConversation(
1503 target, ConversationOperation.UPDATE, value));
1504 // Update the local conversation objects so they immediately change state.
1505 target.read = read;
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001506 if (markViewed) {
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001507 target.markViewed();
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001508 }
Andy Huang839ada22012-07-20 15:48:40 -07001509 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07001510 mConversationListCursor.updateBulkValues(opList);
Andy Huang839ada22012-07-20 15:48:40 -07001511 }
1512
Andy Huang8f6b0062012-07-31 15:36:31 -07001513 /**
1514 * Auto-advance to a different conversation if the currently visible conversation in
1515 * conversation mode is affected (deleted, marked unread, etc.).
1516 *
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001517 * <p>Does nothing if outside of conversation mode.</p>
Andy Huang8f6b0062012-07-31 15:36:31 -07001518 *
1519 * @param target the set of conversations being deleted/marked unread
1520 */
mindyp9365a822012-09-12 09:09:09 -07001521 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001522 public void showNextConversation(final Collection<Conversation> target) {
1523 showNextConversation(target, null);
1524 }
1525
1526 /**
1527 * Auto-advance to a different conversation if the currently visible conversation in
1528 * conversation mode is affected (deleted, marked unread, etc.).
1529 *
1530 * <p>Does nothing if outside of conversation mode.</p>
1531 *
1532 * @param target the set of conversations being deleted/marked unread
Yu Ping Hu7c909c72013-01-18 11:58:01 -08001533 * @param operation if auto-advance setting is unset, this operation is run after the user
1534 * is prompted to select a setting.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001535 * @return <code>false</code> if we aborted because the user has not yet specified a default
1536 * action, <code>true</code> otherwise
1537 */
1538 private boolean showNextConversation(final Collection<Conversation> target,
1539 final Runnable operation) {
Scott Kennedy8fb8e262012-11-28 15:48:03 -08001540 final int viewMode = mViewMode.getMode();
1541 final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1542 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
Andy Huang8f6b0062012-07-31 15:36:31 -07001543 && Conversation.contains(target, mCurrentConversation);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001544
Andy Huang8f6b0062012-07-31 15:36:31 -07001545 if (currentConversationInView) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001546 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1547
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08001548 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001549 displayAutoAdvanceDialogAndPerformAction(operation);
1550 return false;
1551 } else {
1552 // If we don't have one set, but we're here, just take the default
Vikram Aggarwal82d37502013-01-10 16:18:49 -08001553 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1554 AutoAdvance.DEFAULT : autoAdvanceSetting;
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001555
1556 final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1557 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1558 showConversation(next);
1559 return true;
1560 }
Andy Huang8f6b0062012-07-31 15:36:31 -07001561 }
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001562
1563 return true;
1564 }
1565
1566 /**
1567 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1568 * stored, and the specified operation is run.
1569 */
1570 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1571 final String[] autoAdvanceDisplayOptions =
1572 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1573 final String[] autoAdvanceOptionValues =
1574 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1575
1576 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1577 int initialIndex = 0;
1578 for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1579 if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1580 initialIndex = i;
1581 break;
1582 }
1583 }
1584
1585 final DialogInterface.OnClickListener listClickListener =
1586 new DialogInterface.OnClickListener() {
1587 @Override
1588 public void onClick(DialogInterface dialog, int whichItem) {
1589 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1590 final int autoAdvanceValueInt =
1591 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1592 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1593
1594 // Save the user's setting
1595 final ContentValues values = new ContentValues(1);
1596 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1597
1598 final ContentResolver resolver = mContext.getContentResolver();
1599 resolver.update(mAccount.updateSettingsUri, values, null, null);
1600
1601 // Dismiss the dialog, as clicking the items in the list doesn't close the
1602 // dialog.
1603 dialog.dismiss();
1604 if (operation != null) {
1605 operation.run();
1606 }
1607 }
1608 };
1609
1610 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1611 R.string.auto_advance_help_title)
1612 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1613 .setPositiveButton(null, null)
1614 .create()
1615 .show();
Andy Huang8f6b0062012-07-31 15:36:31 -07001616 }
1617
Andy Huang839ada22012-07-20 15:48:40 -07001618 @Override
1619 public void starMessage(ConversationMessage msg, boolean starred) {
1620 if (msg.starred == starred) {
1621 return;
1622 }
1623
1624 msg.starred = starred;
1625
1626 // locally propagate the change to the owning conversation
1627 // (figure the provider will properly propagate the change when it commits it)
1628 //
1629 // when unstarring, only propagate the change if this was the only message starred
1630 final boolean conversationStarred = starred || msg.isConversationStarred();
Andy Huangcd12e822012-11-08 19:50:57 -08001631 final Conversation conv = msg.getConversation();
1632 if (conversationStarred != conv.starred) {
1633 conv.starred = conversationStarred;
1634 mConversationListCursor.setConversationColumn(conv.uri,
Andy Huang839ada22012-07-20 15:48:40 -07001635 ConversationColumns.STARRED, conversationStarred);
1636 }
1637
1638 final ContentValues values = new ContentValues(1);
1639 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1640
1641 new ContentProviderTask.UpdateTask() {
1642 @Override
1643 protected void onPostExecute(Result result) {
1644 // TODO: handle errors?
1645 }
1646 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1647 }
1648
Andy Huang12b3ee42013-04-24 22:49:43 -07001649 @Override
Alice Yang486e63e2013-04-05 13:01:50 -07001650 public void requestFolderRefresh() {
Alice Yang37dda442013-03-26 22:48:53 -07001651 if (mFolder == null) {
1652 return;
Mindy Pereira28e0c342012-02-17 15:05:13 -08001653 }
Alice Yang37dda442013-03-26 22:48:53 -07001654 final ConversationListFragment convList = getConversationListFragment();
1655 if (convList == null) {
1656 // This could happen if this account is in initial sync (user
1657 // is seeing the "your mail will appear shortly" message)
1658 return;
1659 }
1660 convList.showSyncStatusBar();
1661
1662 if (mAsyncRefreshTask != null) {
1663 mAsyncRefreshTask.cancel(true);
1664 }
1665 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
1666 mAsyncRefreshTask.execute();
Mindy Pereira28e0c342012-02-17 15:05:13 -08001667 }
1668
Mindy Pereirafbe40192012-03-20 10:40:45 -07001669 /**
1670 * Confirm (based on user's settings) and delete a conversation from the conversation list and
1671 * from the database.
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001672 * @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 -07001673 * @param target the conversations to act upon
1674 * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1675 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
Mindy Pereirafbe40192012-03-20 10:40:45 -07001676 */
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001677 private void confirmAndDelete(int actionId, final Collection<Conversation> target,
1678 boolean showDialog, int confirmResource) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001679 final boolean isBatch = false;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001680 if (showDialog) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001681 makeDialogListener(actionId, isBatch);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07001682 final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1683 target.size());
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001684 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
1685 c.displayDialog(mActivity.getFragmentManager());
Mindy Pereirafbe40192012-03-20 10:40:45 -07001686 } else {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001687 delete(0, target, getDeferredAction(actionId, target, isBatch), isBatch);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001688 }
1689 }
1690
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07001691 @Override
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001692 public void delete(final int actionId, final Collection<Conversation> target,
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001693 final DestructiveAction action, final boolean isBatch) {
mindyp84f7d322012-10-01 17:14:40 -07001694 // Order of events is critical! The Conversation View Fragment must be
1695 // notified of the next conversation with showConversation(next) *before* the
1696 // conversation list
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001697 // fragment has a chance to delete the conversation, animating it away.
1698
mindyp84f7d322012-10-01 17:14:40 -07001699 // Update the conversation fragment if the current conversation is
1700 // deleted.
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001701 final Runnable operation = new Runnable() {
1702 @Override
1703 public void run() {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001704 delete(actionId, target, action, isBatch);
Scott Kennedy0d0f8b02012-10-12 15:18:18 -07001705 }
1706 };
1707
1708 if (!showNextConversation(target, operation)) {
1709 // This method will be called again if the user selects an autoadvance option
1710 return;
1711 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001712 // If the conversation is in the selected set, remove it from the set.
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001713 // Batch selections are cleared in the end of the action, so not done for batch actions.
1714 if (!isBatch) {
1715 for (final Conversation conv : target) {
1716 if (mSelectedSet.contains(conv)) {
Vikram Aggarwal97ae7842013-04-22 16:29:12 -07001717 mSelectedSet.toggle(conv);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07001718 }
Vikram Aggarwalab1b5b62013-04-16 15:45:50 -07001719 }
1720 }
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001721 // The conversation list deletes and performs the action if it exists.
1722 final ConversationListFragment convListFragment = getConversationListFragment();
1723 if (convListFragment != null) {
Alice Yang193e05a2013-05-05 14:12:08 -07001724 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08001725 convListFragment.requestDelete(actionId, target, action);
Vikram Aggarwal192fac12012-07-25 16:44:55 -07001726 return;
1727 }
mindyp84f7d322012-10-01 17:14:40 -07001728 // No visible UI element handled it on our behalf. Perform the action
1729 // ourself.
Alice Yang193e05a2013-05-05 14:12:08 -07001730 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves");
Vikram Aggarwald503df42012-05-11 10:13:35 -07001731 action.performAction();
1732 }
1733
1734 /**
1735 * Requests that the action be performed and the UI state is updated to reflect the new change.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001736 * @param action the action to be performed, specified as a menu id: R.id.archive, ...
Vikram Aggarwald503df42012-05-11 10:13:35 -07001737 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001738 private void requestUpdate(final DestructiveAction action) {
Vikram Aggarwald503df42012-05-11 10:13:35 -07001739 action.performAction();
1740 refreshConversationList();
Vikram Aggarwal75daee52012-04-30 11:13:09 -07001741 }
Vikram Aggarwal6fbc87a2012-03-15 15:24:00 -07001742
1743 @Override
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001744 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1745 // TODO(viki): Auto-generated method stub
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001746 }
1747
1748 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001749 public boolean onPrepareOptionsMenu(Menu menu) {
Andy Huangd736a382012-08-29 13:08:58 -07001750 return mActionBarView.onPrepareOptionsMenu(menu);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001751 }
1752
Mindy Pereira68f2e222012-03-07 10:36:54 -08001753 @Override
1754 public void onPause() {
1755 isLoaderInitialized = false;
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001756 enableNotifications();
Paul Westbrook94e440d2012-02-24 11:03:47 -08001757 }
1758
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001759 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001760 public void onResume() {
Paul Westbrook6ead20d2012-03-19 14:48:14 -07001761 // Register the receiver that will prevent the status receiver from
1762 // displaying its notification icon as long as we're running.
1763 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1764 // that the notification was received for.
1765 disableNotifications();
Andy Huang1ee96b22012-08-24 20:19:53 -07001766
1767 mSafeToModifyFragments = true;
Andrew Sappersteined5b52d2013-04-30 13:40:18 -07001768
1769 attachEmptyFolderDialogFragmentListener();
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001770 }
1771
1772 @Override
1773 public void onSaveInstanceState(Bundle outState) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001774 mViewMode.handleSaveInstanceState(outState);
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001775 if (mAccount != null) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08001776 outState.putParcelable(SAVED_ACCOUNT, mAccount);
1777 }
Mindy Pereira5e478d22012-03-26 18:04:58 -07001778 if (mFolder != null) {
1779 outState.putParcelable(SAVED_FOLDER, mFolder);
Vikram Aggarwal8b152632012-02-03 14:58:45 -08001780 }
Vikram Aggarwalf3341402012-08-07 10:09:38 -07001781 // If this is a search activity, let's store the search query term as well.
1782 if (ConversationListContext.isSearchResult(mConvListContext)) {
1783 outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1784 }
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001785 if (mCurrentConversation != null && mViewMode.isConversationMode()) {
Mindy Pereira26f23fc2012-03-27 10:26:04 -07001786 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1787 }
Andy Huang4556a442012-03-30 16:42:05 -07001788 if (!mSelectedSet.isEmpty()) {
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07001789 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
Andy Huang4556a442012-03-30 16:42:05 -07001790 }
Mindy Pereirad33674992012-06-25 16:26:30 -07001791 if (mToastBar.getVisibility() == View.VISIBLE) {
1792 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1793 }
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001794 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001795 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001796 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001797 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001798 // 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 -08001799 if (mDialogAction != -1) {
1800 outState.putInt(SAVED_ACTION, mDialogAction);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001801 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08001802 }
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001803 if (mDetachedConvUri != null) {
1804 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
1805 }
Andy Huang1ee96b22012-08-24 20:19:53 -07001806 mSafeToModifyFragments = false;
Scott Kennedyb10212e2013-02-22 16:27:00 -08001807 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
Andy Huang1ee96b22012-08-24 20:19:53 -07001808 }
1809
1810 /**
1811 * @see #mSafeToModifyFragments
1812 */
1813 protected boolean safeToModifyFragments() {
1814 return mSafeToModifyFragments;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001815 }
1816
1817 @Override
Andy Huang313ac132013-03-04 23:40:58 -08001818 public void executeSearch(String query) {
Mindy Pereira68f2e222012-03-07 10:36:54 -08001819 Intent intent = new Intent();
1820 intent.setAction(Intent.ACTION_SEARCH);
1821 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1822 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1823 intent.setComponent(mActivity.getComponentName());
Vikram Aggarwalb17cbc02012-04-06 15:41:46 -07001824 mActionBarView.collapseSearch();
Mindy Pereira68f2e222012-03-07 10:36:54 -08001825 mActivity.startActivity(intent);
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001826 }
1827
1828 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001829 public void onStop() {
Scott Kennedycb85aea2013-02-25 13:08:32 -08001830 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001831 }
1832
Andy Huang632721e2012-04-11 16:57:26 -07001833 @Override
1834 public void onDestroy() {
Andy Huangb2ef9c12012-12-18 12:58:41 -06001835 // stop listening to the cursor on e.g. configuration changes
1836 if (mConversationListCursor != null) {
1837 mConversationListCursor.removeListener(this);
1838 }
Andy Huang632721e2012-04-11 16:57:26 -07001839 // unregister the ViewPager's observer on the conversation cursor
1840 mPagerController.onDestroy();
Mindy Pereira641de652012-08-02 15:21:50 -07001841 mActionBarView.onDestroy();
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001842 mRecentFolderList.destroy();
Andy Huang4e0158f2012-08-07 21:06:01 -07001843 mDestroyed = true;
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08001844 mHandler.removeCallbacks(mLogServiceChecker);
1845 mLogServiceChecker = null;
Andy Huang632721e2012-04-11 16:57:26 -07001846 }
1847
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001848 /**
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001849 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
1850 * or not. The individual controller is responsible for changing the icon based on the mode.
1851 */
1852 protected abstract void resetActionBarIcon();
1853
1854 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001855 * {@inheritDoc} Subclasses must override this to listen to mode changes
1856 * from the ViewMode. Subclasses <b>must</b> call the parent's
1857 * onViewModeChanged since the parent will handle common state changes.
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001858 */
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001859 @Override
Vikram Aggarwalfa131a22012-02-02 13:56:22 -08001860 public void onViewModeChanged(int newMode) {
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07001861 // When we step away from the conversation mode, we don't have a current conversation
1862 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1863 if (!ViewMode.isConversationMode(newMode)) {
1864 setCurrentConversation(null);
1865 }
Vikram Aggarwal93dc2022012-11-05 13:36:57 -08001866 // If the viewmode is not set, preserve existing icon.
1867 if (newMode != ViewMode.UNKNOWN) {
1868 resetActionBarIcon();
1869 }
Andy Huang12b3ee42013-04-24 22:49:43 -07001870
1871 if (isDrawerEnabled()) {
Scott Kennedy8a72b852013-05-02 14:18:50 -07001872 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(newMode));
1873 mDrawerContainer.setDrawerLockMode(getShouldAllowDrawerPull(newMode)
1874 ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
Andy Huang12b3ee42013-04-24 22:49:43 -07001875 closeDrawerIfOpen();
1876 }
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001877 }
1878
Scott Kennedy8a72b852013-05-02 14:18:50 -07001879 private boolean getShouldShowDrawerIndicator(final int viewMode) {
1880 // if search list/conv mode, disable indicator
1881 // only allow indicator at top level of app
1882 if (ViewMode.isSearchMode(viewMode)) {
1883 return false;
1884 } else {
1885 return viewMode == ViewMode.CONVERSATION_LIST || viewMode == ViewMode.FOLDER_LIST;
1886 }
1887 }
1888
1889 private boolean getShouldAllowDrawerPull(final int viewMode) {
1890 // if search list/conv mode, disable drawer pull
1891 // allow drawer pull everywhere except conversation mode where the list is hidden
1892 if (ViewMode.isSearchMode(viewMode)) {
1893 return false;
1894 } else {
1895 return !(ViewMode.isConversationMode(viewMode)
1896 // TODO(ath): get this to work to allow drawer pull in 2-pane conv mode.
1897 /* && !isConversationListVisible() */);
1898 }
1899 }
1900
Andy Huang3825f3d2012-08-29 16:44:12 -07001901 public void disablePagerUpdates() {
1902 mPagerController.stopListening();
1903 }
1904
Andy Huang4e0158f2012-08-07 21:06:01 -07001905 public boolean isDestroyed() {
1906 return mDestroyed;
1907 }
1908
mindyp54f120f2012-08-28 13:10:33 -07001909 @Override
1910 public void commitDestructiveActions(boolean animate) {
mindypc6adce32012-08-22 18:46:42 -07001911 ConversationListFragment fragment = getConversationListFragment();
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001912 if (fragment != null) {
mindypc6adce32012-08-22 18:46:42 -07001913 fragment.commitDestructiveActions(animate);
Mindy Pereira1e2573b2012-04-17 14:34:36 -07001914 }
1915 }
1916
Vikram Aggarwala55b36c2012-01-13 11:45:02 -08001917 @Override
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001918 public void onWindowFocusChanged(boolean hasFocus) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001919 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08001920 // hasFocus already ensures that the window is in focus, so we don't need to call
1921 // AAC.isFragmentVisible(convList) here.
Paul Westbrook9f119c72012-04-24 16:10:59 -07001922 if (hasFocus && convList != null && convList.isVisible()) {
1923 // The conversation list is visible.
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07001924 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07001925 }
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001926 }
1927
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001928 /**
1929 * Set the account, and carry out all the account-related changes that rely on this.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001930 * @param account new account to set to.
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001931 */
Mindy Pereira75181e82012-04-18 08:17:13 -07001932 private void setAccount(Account account) {
Andy Huangb9ca9792012-05-18 15:31:49 -07001933 if (account == null) {
1934 LogUtils.w(LOG_TAG, new Error(),
1935 "AAC ignoring null (presumably invalid) account restoration");
1936 return;
1937 }
Andy Huangb1148412012-05-19 00:16:30 -07001938 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001939 mAccount = account;
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001940 // Only change AAC state here. Do *not* modify any other object's state. The object
1941 // should listen on account changes.
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001942 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY);
Vikram Aggarwalca716f12012-08-20 11:11:48 -07001943 mActivity.invalidateOptionsMenu();
1944 disableNotificationsOnAccountChange(mAccount);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08001945 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Vikram Aggarwal2fe343f2013-01-14 09:00:25 -08001946 // The Mail instance can be null during test runs.
1947 final MailAppProvider instance = MailAppProvider.getInstance();
1948 if (instance != null) {
1949 instance.setLastViewedAccount(mAccount.uri.toString());
1950 }
Vikram Aggarwal91e87372012-05-18 15:36:04 -07001951 if (account.settings == null) {
1952 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1953 return;
1954 }
Vikram Aggarwal7c401b72012-08-13 16:43:47 -07001955 mAccountObservers.notifyChanged();
Vikram Aggarwal34f7b232012-10-17 13:32:23 -07001956 perhapsEnterWaitMode();
Mindy Pereira75181e82012-04-18 08:17:13 -07001957 }
1958
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001959 /**
Mindy Pereira161f50d2012-02-28 15:47:19 -08001960 * Restore the state from the previous bundle. Subclasses should call this
1961 * method from the parent class, since it performs important UI
1962 * initialization.
1963 *
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08001964 * @param savedState previous state
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001965 */
Andy Huang632721e2012-04-11 16:57:26 -07001966 @Override
1967 public void onRestoreInstanceState(Bundle savedState) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08001968 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
Andy Huang632721e2012-04-11 16:57:26 -07001969 if (savedState.containsKey(SAVED_CONVERSATION)) {
1970 // Open the conversation.
Andy Huang2bc8bc12012-11-12 17:24:25 -08001971 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
Paul Westbrook534e4a22012-04-25 03:46:29 -07001972 if (conversation != null && conversation.position < 0) {
1973 // Set the position to 0 on this conversation, as we don't know where it is
1974 // in the list
1975 conversation.position = 0;
1976 }
Andy Huanged4fdf02012-07-26 17:12:50 -07001977 showConversation(conversation);
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08001978 }
Mindy Pereira967ede62012-03-22 09:29:09 -07001979
Mindy Pereirad33674992012-06-25 16:26:30 -07001980 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
Andy Huang2bc8bc12012-11-12 17:24:25 -08001981 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
Mindy Pereirad33674992012-06-25 16:26:30 -07001982 if (op != null) {
1983 if (op.getType() == ToastBarOperation.UNDO) {
1984 onUndoAvailable(op);
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07001985 } else if (op.getType() == ToastBarOperation.ERROR) {
1986 onError(mFolder, true);
Mindy Pereirad33674992012-06-25 16:26:30 -07001987 }
1988 }
1989 }
Scott Kennedyb10212e2013-02-22 16:27:00 -08001990 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001991 final ConversationListFragment convListFragment = getConversationListFragment();
Mindy Pereirad33674992012-06-25 16:26:30 -07001992 if (convListFragment != null) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07001993 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
Mindy Pereirad33674992012-06-25 16:26:30 -07001994 }
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08001995 /*
Mindy Pereira967ede62012-03-22 09:29:09 -07001996 * Restore the state of selected conversations. This needs to be done after the correct mode
1997 * is set and the action bar is fully initialized. If not, several key pieces of state
1998 * information will be missing, and the split views may not be initialized correctly.
Mindy Pereira967ede62012-03-22 09:29:09 -07001999 */
Andy Huang4556a442012-03-30 16:42:05 -07002000 restoreSelectedConversations(savedState);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08002001 // Order is important!!!
2002 // The dialog listener needs to happen *after* the selected set is restored.
2003
2004 // If there has been an orientation change, and we need to recreate the listener for the
2005 // confirm dialog fragment (delete/archive/...), then do it here.
2006 if (mDialogAction != -1) {
2007 makeDialogListener(mDialogAction, mDialogFromSelectedSet);
2008 }
Andy Huang632721e2012-04-11 16:57:26 -07002009 }
2010
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002011 /**
2012 * Handle an intent to open the app. This method is called only when there is no saved state,
2013 * so we need to set state that wasn't set before. It is correct to change the viewmode here
2014 * since it has not been previously set.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002015 * @param intent intent passed to the activity.
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002016 */
Andy Huang632721e2012-04-11 16:57:26 -07002017 private void handleIntent(Intent intent) {
Andy Huange6459422013-04-01 16:32:18 -07002018 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction());
Andy Huang632721e2012-04-11 16:57:26 -07002019 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
2020 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002021 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
Andy Huang632721e2012-04-11 16:57:26 -07002022 }
Andy Huangb9ca9792012-05-18 15:31:49 -07002023 if (mAccount == null) {
2024 return;
Andy Huang632721e2012-04-11 16:57:26 -07002025 }
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002026 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
Vikram Aggarwal649b9ea2012-08-27 12:15:20 -07002027 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
Vikram Aggarwal1a249e02012-08-03 16:19:33 -07002028 mViewMode.enterConversationMode();
2029 } else {
2030 mViewMode.enterConversationListMode();
2031 }
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002032 // Put the folder and conversation, and ask the loader to create this folder.
2033 final Bundle args = new Bundle();
Scott Kennedy48cfe462013-04-10 11:32:02 -07002034
2035 final Uri folderUri;
2036 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) {
2037 folderUri = (Uri) intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI);
2038 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
2039 final Folder folder =
2040 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER));
2041 folderUri = folder.uri;
2042 } else {
Scott Kennedy72727ef2013-05-01 18:10:55 -07002043 final Bundle extras = intent.getExtras();
2044 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s",
2045 extras == null ? "null" : extras.toString());
Scott Kennedy48cfe462013-04-10 11:32:02 -07002046 folderUri = mAccount.settings.defaultInbox;
2047 }
2048
Scott Kennedy60593352013-03-13 13:45:30 -07002049 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002050 args.putParcelable(Utils.EXTRA_CONVERSATION,
2051 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
2052 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args);
Andy Huang632721e2012-04-11 16:57:26 -07002053 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
2054 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002055 mHaveSearchResults = false;
Andy Huang632721e2012-04-11 16:57:26 -07002056 // Save this search query for future suggestions.
2057 final String query = intent.getStringExtra(SearchManager.QUERY);
2058 final String authority = mContext.getString(R.string.suggestions_authority);
Vikram Aggarwal2b703c62012-09-18 13:54:15 -07002059 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
Andy Huang632721e2012-04-11 16:57:26 -07002060 mContext, authority, SuggestionsProvider.MODE);
2061 suggestions.saveRecentQuery(query, null);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002062 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
2063 fetchSearchFolder(intent);
2064 if (shouldEnterSearchConvMode()) {
Mindy Pereiraac254822012-06-18 10:46:43 -07002065 mViewMode.enterSearchResultsConversationMode();
2066 } else {
2067 mViewMode.enterSearchResultsListMode();
2068 }
Andy Huang632721e2012-04-11 16:57:26 -07002069 } else {
2070 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing");
2071 mActivity.finish();
2072 }
2073 }
2074 if (mAccount != null) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002075 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY);
Scott Kennedyb39aaf52013-03-06 19:17:22 -08002076 }
2077 }
2078
Andy Huang4556a442012-03-30 16:42:05 -07002079 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002080 * Returns true if we should enter conversation mode with search.
2081 */
2082 protected final boolean shouldEnterSearchConvMode() {
2083 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
2084 }
2085
2086 /**
Andy Huang4556a442012-03-30 16:42:05 -07002087 * Copy any selected conversations stored in the saved bundle into our selection set,
2088 * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
2089 *
2090 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002091 private void restoreSelectedConversations(Bundle savedState) {
Mindy Pereira967ede62012-03-22 09:29:09 -07002092 if (savedState == null) {
Andy Huang4556a442012-03-30 16:42:05 -07002093 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002094 return;
2095 }
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002096 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
Andy Huang4556a442012-03-30 16:42:05 -07002097 if (selectedSet == null || selectedSet.isEmpty()) {
2098 mSelectedSet.clear();
Mindy Pereira967ede62012-03-22 09:29:09 -07002099 return;
2100 }
Andy Huang632721e2012-04-11 16:57:26 -07002101
2102 // putAll will take care of calling our registered onSetPopulated method
Vikram Aggarwalcabd3f22012-04-19 10:14:41 -07002103 mSelectedSet.putAll(selectedSet);
Mindy Pereira967ede62012-03-22 09:29:09 -07002104 }
2105
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002106 private void showConversation(Conversation conversation) {
Andy Huang1ee96b22012-08-24 20:19:53 -07002107 showConversation(conversation, false /* inLoaderCallbacks */);
2108 }
2109
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002110 /**
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002111 * Show the conversation provided in the arguments. It is safe to pass a null conversation
2112 * object, which is a signal to back out of conversation view mode.
2113 * Child classes must call super.showConversation() <b>before</b> their own implementations.
Vikram Aggarwal59f741f2013-03-01 15:55:40 -08002114 * @param conversation the conversation to be shown, or null if we want to back out to list
2115 * mode.
Vikram Aggarwal49e0e992012-09-21 13:53:15 -07002116 * @param inLoaderCallbacks true if the method is called as a result of
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002117 * onLoadFinished(Loader, Cursor) on any callback.
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002118 */
Andy Huang1ee96b22012-08-24 20:19:53 -07002119 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
Andy Huang243c2362013-03-01 17:50:35 -08002120 if (conversation != null) {
2121 Utils.sConvLoadTimer.start();
2122 }
2123
Andy Huang54e925e2013-03-14 13:24:18 -07002124 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation);
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002125 // Set the current conversation just in case it wasn't already set.
2126 setCurrentConversation(conversation);
Vikram Aggarwalec5cbf72012-03-08 15:10:35 -08002127 }
2128
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002129 /**
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002130 * Children can override this method, but they must call super.showWaitForInitialization().
2131 * {@inheritDoc}
2132 */
2133 @Override
2134 public void showWaitForInitialization() {
2135 mViewMode.enterWaitingForInitializationMode();
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002136 mWaitFragment = WaitFragment.newInstance(mAccount);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002137 }
2138
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002139 private void updateWaitMode() {
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002140 final FragmentManager manager = mActivity.getFragmentManager();
2141 final WaitFragment waitFragment =
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002142 (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002143 if (waitFragment != null) {
2144 waitFragment.updateAccount(mAccount);
2145 }
2146 }
2147
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002148 /**
2149 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
2150 * method, though they must call the parent implementation <b>after</b> they do anything.
2151 */
2152 protected void hideWaitForInitialization() {
2153 mWaitFragment = null;
2154 }
2155
2156 /**
2157 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is
2158 * far superior to using the value of mWaitFragment, which might be invalid or might refer
2159 * to a fragment after it has been destroyed.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002160 * @return a wait fragment that is already attached to the activity, if one exists
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002161 */
2162 protected final WaitFragment getWaitFragment() {
2163 final FragmentManager manager = mActivity.getFragmentManager();
2164 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
2165 if (waitFrag != null) {
2166 // The Fragment Manager knows better, so use its instance.
2167 mWaitFragment = waitFrag;
2168 }
2169 return mWaitFragment;
2170 }
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002171
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002172 /**
2173 * Returns true if we are waiting for the account to sync, and cannot show any folders or
2174 * conversation for the current account yet.
Vikram Aggarwal48b2a6c2012-05-29 14:09:27 -07002175 */
Vikram Aggarwaldd6a7ce2012-10-22 15:45:57 -07002176 private boolean inWaitMode() {
Vikram Aggarwala3f43d42012-10-25 16:21:30 -07002177 final WaitFragment waitFragment = getWaitFragment();
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002178 if (waitFragment != null) {
2179 final Account fragmentAccount = waitFragment.getAccount();
Paul Westbrook339004b2012-11-05 17:13:51 -08002180 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
Paul Westbrook2d50bcd2012-04-10 11:53:47 -07002181 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
2182 }
2183 return false;
2184 }
2185
2186 /**
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002187 * Children can override this method, but they must call super.showConversationList().
2188 * {@inheritDoc}
2189 */
2190 @Override
2191 public void showConversationList(ConversationListContext listContext) {
2192 }
2193
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -08002194 @Override
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002195 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
Alice Yangfe8e0812013-04-22 13:37:26 -07002196 final ConversationListFragment convListFragment = getConversationListFragment();
2197 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) {
2198 convListFragment.getAnimatedAdapter().onConversationSelected();
2199 }
mindypaa55bc92012-08-24 09:49:56 -07002200 // Only animate destructive actions if we are going to be showing the
2201 // conversation list when we show the next conversation.
Vikram Aggarwalbcb16b92013-01-28 18:05:03 -08002202 commitDestructiveActions(mIsTablet);
Andy Huang1ee96b22012-08-24 20:19:53 -07002203 showConversation(conversation, inLoaderCallbacks);
2204 }
2205
2206 @Override
2207 public Conversation getCurrentConversation() {
2208 return mCurrentConversation;
Vikram Aggarwal7d602882012-02-07 15:01:20 -08002209 }
Mindy Pereira555140c2012-02-15 14:55:29 -08002210
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002211 /**
2212 * Set the current conversation. This is the conversation on which all actions are performed.
2213 * Do not modify mCurrentConversation except through this method, which makes it easy to
2214 * perform common actions associated with changing the current conversation.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002215 * @param conversation new conversation to view. Passing null indicates that we are backing
2216 * out to conversation list mode.
Vikram Aggarwalc67182d2012-04-03 14:35:06 -07002217 */
Andy Huang632721e2012-04-11 16:57:26 -07002218 @Override
2219 public void setCurrentConversation(Conversation conversation) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002220 // The controller should come out of detached mode if a new conversation is viewed, or if
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08002221 // we are going back to conversation list mode.
2222 if (mDetachedConvUri != null && (conversation == null
2223 || !mDetachedConvUri.equals(conversation.uri))) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08002224 clearDetachedMode();
2225 }
2226
2227 // Must happen *before* setting mCurrentConversation because this sets
2228 // conversation.position if a cursor is available.
Andy Huang8883f222012-11-12 19:25:00 -08002229 mTracker.initialize(conversation);
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07002230 mCurrentConversation = conversation;
Andy Huang7d646122012-09-05 19:41:44 -07002231
2232 if (mCurrentConversation != null) {
Yorke Leef807ba72012-09-20 17:18:05 -07002233 mActionBarView.setCurrentConversation(mCurrentConversation);
Yorke Leef807ba72012-09-20 17:18:05 -07002234 mActivity.invalidateOptionsMenu();
Andy Huang7d646122012-09-05 19:41:44 -07002235 }
Mindy Pereira5040f1a2012-03-20 10:14:06 -07002236 }
2237
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002238 /**
Andy Huangf9a73482012-03-13 15:54:02 -07002239 * {@link LoaderManager} currently has a bug in
2240 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
2241 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
2242 * this bug by destroying any loaders that may have been created as null (essentially because
2243 * they are optional loads, and may not apply to a particular account).
2244 * <p>
2245 * A simple null check before restarting a loader will not work, because that would not
2246 * give the controller a chance to invalidate UI corresponding the prior loader result.
2247 *
2248 * @param id loader ID to safely restart
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002249 * @param handler the LoaderCallback which will handle this loader ID.
2250 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no
2251 * arguments need to be specified.
Andy Huangf9a73482012-03-13 15:54:02 -07002252 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002253 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) {
Andy Huangf9a73482012-03-13 15:54:02 -07002254 final LoaderManager lm = mActivity.getLoaderManager();
2255 lm.destroyLoader(id);
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002256 lm.restartLoader(id, args, handler);
Vikram Aggarwal94c94de2012-04-04 15:38:28 -07002257 }
2258
Andy Huang632721e2012-04-11 16:57:26 -07002259 @Override
2260 public void registerConversationListObserver(DataSetObserver observer) {
2261 mConversationListObservable.registerObserver(observer);
2262 }
2263
2264 @Override
2265 public void unregisterConversationListObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002266 try {
2267 mConversationListObservable.unregisterObserver(observer);
2268 } catch (IllegalStateException e) {
2269 // Log instead of crash
2270 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that "
2271 + "hasn't been registered");
2272 }
Andy Huang632721e2012-04-11 16:57:26 -07002273 }
2274
Andy Huang090db1e2012-07-25 13:25:28 -07002275 @Override
2276 public void registerFolderObserver(DataSetObserver observer) {
2277 mFolderObservable.registerObserver(observer);
2278 }
2279
2280 @Override
2281 public void unregisterFolderObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002282 try {
2283 mFolderObservable.unregisterObserver(observer);
2284 } catch (IllegalStateException e) {
2285 // Log instead of crash
2286 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that "
2287 + "hasn't been registered");
2288 }
Andy Huang090db1e2012-07-25 13:25:28 -07002289 }
2290
Andy Huang9d3fd922012-09-26 22:23:58 -07002291 @Override
2292 public void registerConversationLoadedObserver(DataSetObserver observer) {
2293 mPagerController.registerConversationLoadedObserver(observer);
2294 }
2295
2296 @Override
2297 public void unregisterConversationLoadedObserver(DataSetObserver observer) {
Alice Yang647aefb2013-03-04 19:13:11 -08002298 try {
2299 mPagerController.unregisterConversationLoadedObserver(observer);
2300 } catch (IllegalStateException e) {
2301 // Log instead of crash
2302 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer "
2303 + "that hasn't been registered");
2304 }
Andy Huang9d3fd922012-09-26 22:23:58 -07002305 }
2306
Vikram Aggarwal60069912012-07-24 14:26:09 -07002307 /**
2308 * Returns true if the number of accounts is different, or if the current account has been
2309 * removed from the device
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002310 * @param accountCursor the cursor which points to all the accounts.
2311 * @return true if the number of accounts is changed or current account missing from the list.
Vikram Aggarwal60069912012-07-24 14:26:09 -07002312 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002313 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) {
Paul Westbrook23b74b92012-02-29 11:36:12 -08002314 // Check to see if the current account hasn't been set, or the account cursor is empty
2315 if (mAccount == null || !accountCursor.moveToFirst()) {
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002316 return true;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002317 }
2318
2319 // Check to see if the number of accounts are different, from the number we saw on the last
2320 // updated
2321 if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2322 return true;
2323 }
2324
2325 // Check to see if the account list is different or if the current account is not found in
2326 // the cursor.
2327 boolean foundCurrentAccount = false;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002328 do {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08002329 final Uri accountUri = Uri.parse(accountCursor.getString(
2330 accountCursor.getColumnIndex(UIProvider.AccountColumns.URI)));
Paul Westbrook23b74b92012-02-29 11:36:12 -08002331 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
2332 foundCurrentAccount = true;
2333 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002334 // Is there a new account that we do not know about?
Paul Westbrook23b74b92012-02-29 11:36:12 -08002335 if (!mCurrentAccountUris.contains(accountUri)) {
2336 return true;
2337 }
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002338 } while (accountCursor.moveToNext());
Paul Westbrook23b74b92012-02-29 11:36:12 -08002339
2340 // As long as we found the current account, the list hasn't been updated
2341 return !foundCurrentAccount;
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002342 }
2343
2344 /**
Vikram Aggarwal60069912012-07-24 14:26:09 -07002345 * Updates accounts for the app. If the current account is missing, the first
2346 * account in the list is set to the current account (we <em>have</em> to choose something).
Mindy Pereira161f50d2012-02-28 15:47:19 -08002347 *
Vikram Aggarwal6c511582012-02-27 10:59:47 -08002348 * @param accounts cursor into the AccountCache
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002349 * @return true if the update was successful, false otherwise
2350 */
Vikram Aggarwal177097f2013-03-08 11:19:53 -08002351 private boolean updateAccounts(ObjectCursor<Account> accounts) {
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002352 if (accounts == null || !accounts.moveToFirst()) {
2353 return false;
2354 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002355
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002356 final Account[] allAccounts = Account.getAllAccounts(accounts);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002357 // A match for the current account's URI in the list of accounts.
2358 Account currentFromList = null;
Paul Westbrook23b74b92012-02-29 11:36:12 -08002359
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002360 // Save the uris for the accounts and find the current account in the updated cursor.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002361 mCurrentAccountUris.clear();
Vikram Aggarwal636c1a12012-09-22 16:02:40 -07002362 for (final Account account : allAccounts) {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002363 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
Paul Westbrook23b74b92012-02-29 11:36:12 -08002364 mCurrentAccountUris.add(account.uri);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002365 if (mAccount != null && account.uri.equals(mAccount.uri)) {
2366 currentFromList = account;
2367 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002368 }
2369
Vikram Aggarwal60069912012-07-24 14:26:09 -07002370 // 1. current account is already set and is in allAccounts:
2371 // 1a. It has changed -> load the updated account.
2372 // 2b. It is unchanged -> no-op
Andy Huang0d647352012-03-21 21:48:16 -07002373 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
Vikram Aggarwal60069912012-07-24 14:26:09 -07002374 // 3. saved preference has an account -> pick that one
Andy Huang0d647352012-03-21 21:48:16 -07002375 // 4. otherwise just pick first
2376
Vikram Aggarwal60069912012-07-24 14:26:09 -07002377 boolean accountChanged = false;
2378 /// Assume case 4, initialize to first account, and see if we can find anything better.
2379 Account newAccount = allAccounts[0];
2380 if (currentFromList != null) {
2381 // Case 1: Current account exists but has changed
2382 if (!currentFromList.equals(mAccount)) {
2383 newAccount = currentFromList;
2384 accountChanged = true;
Andy Huang0d647352012-03-21 21:48:16 -07002385 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002386 // Case 1b: else, current account is unchanged: nothing to do.
Paul Westbrook23b74b92012-02-29 11:36:12 -08002387 } else {
Vikram Aggarwal60069912012-07-24 14:26:09 -07002388 // Case 2: Current account is not in allAccounts, the account needs to change.
2389 accountChanged = true;
2390 if (mAccount == null) {
2391 // Case 3: Check for last viewed account, and check if it exists in the list.
2392 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2393 if (lastAccountUri != null) {
2394 for (final Account account : allAccounts) {
2395 if (lastAccountUri.equals(account.uri.toString())) {
2396 newAccount = account;
2397 break;
2398 }
Andy Huang0d647352012-03-21 21:48:16 -07002399 }
2400 }
2401 }
Paul Westbrook23b74b92012-02-29 11:36:12 -08002402 }
Vikram Aggarwal60069912012-07-24 14:26:09 -07002403 if (accountChanged) {
Vikram Aggarwal5fd8afd2013-03-13 15:28:47 -07002404 changeAccount(newAccount);
Vikram Aggarwal60069912012-07-24 14:26:09 -07002405 }
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002406
Vikram Aggarwal60069912012-07-24 14:26:09 -07002407 // Whether we have updated the current account or not, we need to update the list of
2408 // accounts in the ActionBar.
Rohan Shahb905f0e2013-04-26 09:17:37 -07002409 mAllAccounts = allAccounts;
Vikram Aggarwal07dbaa62013-03-12 15:21:04 -07002410 mAllAccountObservers.notifyChanged();
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002411 return (allAccounts.length > 0);
Vikram Aggarwal7dedb952012-02-16 16:10:23 -08002412 }
2413
Paul Westbrook6ead20d2012-03-19 14:48:14 -07002414 private void disableNotifications() {
2415 mNewEmailReceiver.activate(mContext, this);
2416 }
2417
2418 private void enableNotifications() {
2419 mNewEmailReceiver.deactivate();
2420 }
2421
2422 private void disableNotificationsOnAccountChange(Account account) {
2423 // If the new mail suppression receiver is activated for a different account, we want to
2424 // activate it for the new account.
2425 if (mNewEmailReceiver.activated() &&
2426 !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2427 // Deactivate the current receiver, otherwise multiple receivers may be registered.
2428 mNewEmailReceiver.deactivate();
2429 mNewEmailReceiver.activate(mContext, this);
2430 }
2431 }
2432
Vikram Aggarwala9b93f32012-02-23 14:51:58 -08002433 /**
Vikram Aggarwalc7694222012-04-23 13:37:01 -07002434 * Destructive actions on Conversations. This class should only be created by controllers, and
2435 * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2436 * Only the controllers should know what kind of destructive actions are being created.
2437 */
Mindy Pereirade3e74a2012-07-24 09:43:10 -07002438 public class ConversationAction implements DestructiveAction {
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002439 /**
2440 * The action to be performed. This is specified as the resource ID of the menu item
2441 * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2442 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002443 private final int mAction;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002444 /** The action will act upon these conversations */
Paul Westbrook77eee622012-07-10 13:41:57 -07002445 private final Collection<Conversation> mTarget;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002446 /** Whether this destructive action has already been performed */
2447 private boolean mCompleted;
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002448 /** Whether this is an action on the currently selected set. */
2449 private final boolean mIsSelectedSet;
Vikram Aggarwale8a85322012-04-24 09:01:38 -07002450
Mindy Pereirafbe40192012-03-20 10:40:45 -07002451 /**
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002452 * Create a listener object.
2453 * @param action action is one of four constants: R.id.y_button (archive),
Mindy Pereirafbe40192012-03-20 10:40:45 -07002454 * R.id.delete , R.id.mute, and R.id.report_spam.
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002455 * @param target Conversation that we want to apply the action to.
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002456 * @param isBatch whether the conversations are in the currently selected batch set.
2457 */
2458 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
Mindy Pereirafbe40192012-03-20 10:40:45 -07002459 mAction = action;
Paul Westbrook77eee622012-07-10 13:41:57 -07002460 mTarget = ImmutableList.copyOf(target);
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002461 mIsSelectedSet = isBatch;
Mindy Pereirafbe40192012-03-20 10:40:45 -07002462 }
2463
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002464 /**
2465 * The action common to child classes. This performs the action specified in the constructor
2466 * on the conversations given here.
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07002467 */
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002468 @Override
2469 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002470 if (isPerformed()) {
2471 return;
2472 }
Mindy Pereira3cb28b52012-05-24 15:26:39 -07002473 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002474
2475 // Are we destroying the currently shown conversation? Show the next one.
2476 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
Vikram Aggarwal8742a612012-08-13 10:22:50 -07002477 LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2478 + "\nmTarget=%s\nCurrent=%s",
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002479 Conversation.toString(mTarget), mCurrentConversation);
2480 }
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002481
Paul Westbrooke1221d22012-08-19 11:09:07 -07002482 if (mConversationListCursor == null) {
2483 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2484 + "\nmTarget=%s\nCurrent=%s",
2485 Conversation.toString(mTarget), mCurrentConversation);
2486 return;
2487 }
2488
Mindy Pereirafbe40192012-03-20 10:40:45 -07002489 switch (mAction) {
Mindy Pereira0692baf2012-03-23 17:34:31 -07002490 case R.id.archive:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002491 LogUtils.d(LOG_TAG, "Archiving");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002492 mConversationListCursor.archive(mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002493 break;
2494 case R.id.delete:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002495 LogUtils.d(LOG_TAG, "Deleting");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002496 mConversationListCursor.delete(mTarget);
Mindy Pereira695d6962012-06-18 13:02:10 -07002497 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
Marc Blank386243f2012-05-25 10:40:59 -07002498 undoEnabled = false;
2499 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002500 break;
2501 case R.id.mute:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002502 LogUtils.d(LOG_TAG, "Muting");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002503 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002504 for (Conversation c : mTarget) {
2505 c.localDeleteOnUpdate = true;
2506 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002507 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002508 mConversationListCursor.mute(mTarget);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002509 break;
2510 case R.id.report_spam:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002511 LogUtils.d(LOG_TAG, "Reporting spam");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002512 mConversationListCursor.reportSpam(mTarget);
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002513 break;
Paul Westbrook77eee622012-07-10 13:41:57 -07002514 case R.id.mark_not_spam:
2515 LogUtils.d(LOG_TAG, "Marking not spam");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002516 mConversationListCursor.reportNotSpam(mTarget);
Paul Westbrook77eee622012-07-10 13:41:57 -07002517 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07002518 case R.id.report_phishing:
2519 LogUtils.d(LOG_TAG, "Reporting phishing");
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002520 mConversationListCursor.reportPhishing(mTarget);
Paul Westbrook76b20622012-07-12 11:45:43 -07002521 break;
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002522 case R.id.remove_star:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002523 LogUtils.d(LOG_TAG, "Removing star");
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002524 // Star removal is destructive in the Starred folder.
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002525 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED,
2526 false);
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002527 break;
2528 case R.id.mark_not_important:
Vikram Aggarwal7dd054e2012-05-21 14:43:10 -07002529 LogUtils.d(LOG_TAG, "Marking not-important");
Mindy Pereira445be212012-08-15 08:50:10 -07002530 // Marking not important is destructive in a mailbox
2531 // containing only important messages
Mindy Pereira0d03ef82012-08-15 09:05:48 -07002532 if (mFolder != null && mFolder.isImportantOnly()) {
Mindy Pereira445be212012-08-15 08:50:10 -07002533 for (Conversation conv : mTarget) {
2534 conv.localDeleteOnUpdate = true;
2535 }
2536 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002537 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY,
2538 UIProvider.ConversationPriority.LOW);
Mindy Pereirafbe40192012-03-20 10:40:45 -07002539 break;
Paul Westbrookef362542012-08-27 14:53:32 -07002540 case R.id.discard_drafts:
2541 LogUtils.d(LOG_TAG, "Discarding draft messages");
2542 // Discarding draft messages is destructive in a "draft" mailbox
2543 if (mFolder != null && mFolder.isDraft()) {
2544 for (Conversation conv : mTarget) {
2545 conv.localDeleteOnUpdate = true;
2546 }
2547 }
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002548 mConversationListCursor.discardDrafts(mTarget);
Paul Westbrookef362542012-08-27 14:53:32 -07002549 // We don't support undoing discarding drafts
2550 undoEnabled = false;
2551 break;
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002552 }
2553 if (undoEnabled) {
mindypead50392012-08-23 11:03:53 -07002554 mHandler.postDelayed(new Runnable() {
2555 @Override
2556 public void run() {
2557 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002558 ToastBarOperation.UNDO, mIsSelectedSet, mFolder));
mindypead50392012-08-23 11:03:53 -07002559 }
2560 }, mShowUndoBarDelay);
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002561 }
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07002562 refreshConversationList();
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002563 if (mIsSelectedSet) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07002564 mSelectedSet.clear();
2565 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002566 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002567
2568 /**
2569 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07002570 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07002571 */
2572 private synchronized boolean isPerformed() {
2573 if (mCompleted) {
2574 return true;
2575 }
2576 mCompleted = true;
2577 return false;
2578 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07002579 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002580
Vikram Aggarwald503df42012-05-11 10:13:35 -07002581 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2582 // conversations to.
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002583 @Override
Mindy Pereira8db7e402012-07-13 10:32:47 -07002584 public final void assignFolder(Collection<FolderOperation> folderOps,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002585 Collection<Conversation> target, boolean batch, boolean showUndo,
2586 final boolean isMoveTo) {
Mindy Pereira8db7e402012-07-13 10:32:47 -07002587 // Actions are destructive only when the current folder can be assigned
2588 // to (which is the same as being able to un-assign a conversation from the folder) and
2589 // when the list of folders contains the current folder.
2590 final boolean isDestructive = mFolder
2591 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2592 && FolderOperation.isDestructive(folderOps, mFolder);
Vikram Aggarwald503df42012-05-11 10:13:35 -07002593 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2594 if (isDestructive) {
2595 for (final Conversation c : target) {
2596 c.localDeleteOnUpdate = true;
Mindy Pereira6778f462012-03-23 18:01:55 -07002597 }
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002598 }
mindypc84759c2012-08-29 09:51:53 -07002599 final DestructiveAction folderChange;
Vikram Aggarwald503df42012-05-11 10:13:35 -07002600 // Update the UI elements depending no their visibility and availability
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002601 // TODO(viki): Consolidate this into a single method requestDelete.
Vikram Aggarwald503df42012-05-11 10:13:35 -07002602 if (isDestructive) {
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002603 /*
2604 * If this is a MOVE operation, we want the action folder to be the destination folder.
2605 * Otherwise, we want it to be the current folder.
2606 *
2607 * A set of folder operations is a move if there are exactly two operations: an add and
2608 * a remove.
2609 */
2610 final Folder actionFolder;
2611 if (folderOps.size() != 2) {
2612 actionFolder = mFolder;
2613 } else {
2614 Folder addedFolder = null;
2615 boolean hasRemove = false;
2616 for (final FolderOperation folderOperation : folderOps) {
2617 if (folderOperation.mAdd) {
2618 addedFolder = folderOperation.mFolder;
2619 } else {
2620 hasRemove = true;
2621 }
2622 }
2623
2624 if (hasRemove && addedFolder != null) {
2625 actionFolder = addedFolder;
2626 } else {
2627 actionFolder = mFolder;
2628 }
2629 }
2630
mindypc84759c2012-08-29 09:51:53 -07002631 folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002632 batch, showUndo, isMoveTo, actionFolder);
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002633 delete(0, target, folderChange, batch);
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002634 } else {
mindypc84759c2012-08-29 09:51:53 -07002635 folderChange = getFolderChange(target, folderOps, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002636 batch, showUndo, false /* isMoveTo */, mFolder);
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002637 requestUpdate(folderChange);
Mindy Pereirae5f4dc02012-03-21 16:08:53 -07002638 }
2639 }
2640
Mindy Pereira967ede62012-03-22 09:29:09 -07002641 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002642 public final void onRefreshRequired() {
mindyp5390fca2012-08-22 12:12:25 -07002643 if (isAnimating() || isDragging()) {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002644 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2645 return;
2646 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002647 // Refresh the query in the background
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002648 if (mConversationListCursor.isRefreshRequired()) {
2649 mConversationListCursor.refresh();
2650 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002651 }
2652
mindyp5390fca2012-08-22 12:12:25 -07002653 @Override
2654 public void startDragMode() {
2655 mIsDragHappening = true;
2656 }
2657
2658 @Override
2659 public void stopDragMode() {
2660 mIsDragHappening = false;
2661 if (mConversationListCursor.isRefreshReady()) {
2662 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2663 onRefreshReady();
2664 }
2665
2666 if (mConversationListCursor.isRefreshRequired()) {
2667 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2668 mConversationListCursor.refresh();
2669 }
2670 }
2671
2672 private boolean isDragging() {
2673 return mIsDragHappening;
2674 }
2675
mindyp6f54e1b2012-10-09 09:54:08 -07002676 @Override
2677 public boolean isAnimating() {
Mindy Pereira69e88dd2012-08-10 09:30:18 -07002678 boolean isAnimating = false;
2679 ConversationListFragment convListFragment = getConversationListFragment();
2680 if (convListFragment != null) {
2681 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2682 if (adapter != null) {
2683 isAnimating = adapter.isAnimating();
2684 }
2685 }
2686 return isAnimating;
2687 }
2688
Marc Blankbf128eb2012-04-18 15:58:45 -07002689 /**
2690 * Called when the {@link ConversationCursor} is changed or has new data in it.
2691 * <p>
2692 * {@inheritDoc}
2693 */
2694 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002695 public final void onRefreshReady() {
mindyp5c1d8352012-11-05 10:12:44 -08002696 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2697 mFolder != null ? mFolder.id : "-1");
Andy Huangb2ef9c12012-12-18 12:58:41 -06002698
2699 if (mDestroyed) {
2700 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2701 return;
2702 }
2703
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002704 if (!isAnimating()) {
Marc Blankbf128eb2012-04-18 15:58:45 -07002705 // Swap cursors
2706 mConversationListCursor.sync();
Marc Blankbf128eb2012-04-18 15:58:45 -07002707 }
Paul Westbrook937c94f2012-08-16 13:01:18 -07002708 mTracker.onCursorUpdated();
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08002709 perhapsShowFirstSearchResult();
Marc Blankbf128eb2012-04-18 15:58:45 -07002710 }
2711
2712 @Override
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002713 public final void onDataSetChanged() {
Paul Westbrook9f119c72012-04-24 16:10:59 -07002714 updateConversationListFragment();
Andy Huang632721e2012-04-11 16:57:26 -07002715 mConversationListObservable.notifyChanged();
Paul Westbrooka13b3742012-09-07 16:35:06 -07002716 mSelectedSet.validateAgainstCursor(mConversationListCursor);
Marc Blankbf128eb2012-04-18 15:58:45 -07002717 }
2718
Vikram Aggarwal3d7ca9d2012-05-11 14:40:36 -07002719 /**
2720 * If the Conversation List Fragment is visible, updates the fragment.
2721 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08002722 private void updateConversationListFragment() {
Marc Blankbf128eb2012-04-18 15:58:45 -07002723 final ConversationListFragment convList = getConversationListFragment();
2724 if (convList != null) {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07002725 refreshConversationList();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08002726 if (isFragmentVisible(convList)) {
Vikram Aggarwal69b5c302012-09-05 11:11:13 -07002727 informCursorVisiblity(true);
Paul Westbrook9f119c72012-04-24 16:10:59 -07002728 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002729 }
2730 }
2731
2732 /**
2733 * This class handles throttled refresh of the conversation list
2734 */
2735 static class RefreshTimerTask extends TimerTask {
2736 final Handler mHandler;
2737 final AbstractActivityController mController;
2738
2739 RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2740 mHandler = handler;
2741 mController = controller;
2742 }
2743
2744 @Override
2745 public void run() {
2746 mHandler.post(new Runnable() {
2747 @Override
2748 public void run() {
2749 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2750 mController.onRefreshRequired();
2751 }});
2752 }
2753 }
2754
2755 /**
2756 * Cancel the refresh task, if it's running
2757 */
2758 private void cancelRefreshTask () {
2759 if (mConversationListRefreshTask != null) {
2760 mConversationListRefreshTask.cancel();
2761 mConversationListRefreshTask = null;
2762 }
2763 }
2764
2765 @Override
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002766 public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
Paul Westbrook026139c2012-09-19 22:35:37 -07002767 if (mConversationListCursor == null) {
2768 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2769 return;
2770 }
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002771 if (mConversationListCursor.isRefreshReady()) {
mindyp52544862012-08-20 12:05:36 -07002772 LogUtils.d(LOG_TAG, "Stopped animating: try sync");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002773 onRefreshReady();
Marc Blankbf128eb2012-04-18 15:58:45 -07002774 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002775
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002776 if (mConversationListCursor.isRefreshRequired()) {
mindyp52544862012-08-20 12:05:36 -07002777 LogUtils.d(LOG_TAG, "Stopped animating: refresh");
Paul Westbrookcff1aea2012-08-10 11:51:00 -07002778 mConversationListCursor.refresh();
2779 }
mindyp6f54e1b2012-10-09 09:54:08 -07002780 if (mRecentsDataUpdated) {
2781 mRecentsDataUpdated = false;
2782 mRecentFolderObservers.notifyChanged();
2783 }
Marc Blankbf128eb2012-04-18 15:58:45 -07002784 }
2785
2786 @Override
Mindy Pereira967ede62012-03-22 09:29:09 -07002787 public void onSetEmpty() {
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08002788 // There are no selected conversations. Ensure that the listener and its associated actions
2789 // are blanked out.
2790 setListener(null, -1);
Mindy Pereira967ede62012-03-22 09:29:09 -07002791 }
2792
2793 @Override
2794 public void onSetPopulated(ConversationSelectionSet set) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08002795 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
Vikram Aggarwal5b9ed2b2013-01-28 18:08:37 -08002796 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
Vikram Aggarwal7704d792013-01-11 15:48:24 -08002797 enableCabMode();
Vikram Aggarwal6902dcf2012-04-11 08:57:42 -07002798 }
Mindy Pereira967ede62012-03-22 09:29:09 -07002799 }
2800
Mindy Pereira967ede62012-03-22 09:29:09 -07002801 @Override
2802 public void onSetChanged(ConversationSelectionSet set) {
2803 // Do nothing. We don't care about changes to the set.
2804 }
2805
2806 @Override
2807 public ConversationSelectionSet getSelectedSet() {
2808 return mSelectedSet;
2809 }
2810
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002811 /**
2812 * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2813 */
2814 protected void disableCabMode() {
Mindy Pereira8937bf12012-07-23 14:05:02 -07002815 // Commit any previous destructive actions when entering/ exiting CAB mode.
mindypc6adce32012-08-22 18:46:42 -07002816 commitDestructiveActions(true);
Vikram Aggarwale128fc22012-04-04 12:33:34 -07002817 if (mCabActionMenu != null) {
2818 mCabActionMenu.deactivate();
2819 }
2820 }
2821
2822 /**
2823 * Re-enable the CAB menu if required. The selection set is not changed.
2824 */
2825 protected void enableCabMode() {
2826 if (mCabActionMenu != null) {
2827 mCabActionMenu.activate();
2828 }
2829 }
2830
Vikram Aggarwal4eb52712012-06-19 16:24:50 -07002831 /**
2832 * Unselect conversations and exit CAB mode.
2833 */
2834 protected final void exitCabMode() {
2835 mSelectedSet.clear();
2836 }
2837
Mindy Pereira967ede62012-03-22 09:29:09 -07002838 @Override
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002839 public void startSearch() {
Vikram Aggarwal35f19d72012-04-24 13:24:48 -07002840 if (mAccount == null) {
2841 // We cannot search if there is no account. Drop the request to the floor.
2842 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2843 return;
2844 }
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002845 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
Andy Huang313ac132013-03-04 23:40:58 -08002846 || mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
2847 mActionBarView.expandSearch();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002848 } else {
2849 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
Mindy Pereiraa46c2992012-03-27 14:12:39 -07002850 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
Mindy Pereirafd0c2972012-03-27 13:50:39 -07002851 }
2852 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002853
Vikram Aggarwal0dda5732012-04-06 11:20:16 -07002854 @Override
2855 public void exitSearchMode() {
2856 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2857 mActivity.finish();
2858 }
2859 }
2860
Mindy Pereiraacf60392012-04-06 09:11:00 -07002861 /**
2862 * Supports dragging conversations to a folder.
2863 */
2864 @Override
2865 public boolean supportsDrag(DragEvent event, Folder folder) {
2866 return (folder != null
2867 && event != null
2868 && event.getClipDescription() != null
2869 && folder.supportsCapability
2870 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2871 && folder.supportsCapability
2872 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2873 && !mFolder.uri.equals(folder.uri));
2874 }
2875
2876 /**
Mindy Pereira6c2663d2012-07-20 15:37:29 -07002877 * Handles dropping conversations to a folder.
Mindy Pereiraacf60392012-04-06 09:11:00 -07002878 */
2879 @Override
2880 public void handleDrop(DragEvent event, final Folder folder) {
Mindy Pereiraacf60392012-04-06 09:11:00 -07002881 if (!supportsDrag(event, folder)) {
2882 return;
2883 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07002884 if (folder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08002885 // Moving a conversation to the starred folder adds the star and
2886 // removes the current label
2887 handleDropInStarred(folder);
2888 return;
2889 }
Scott Kennedy8c1058e2013-03-20 13:40:20 -07002890 if (mFolder.isType(UIProvider.FolderType.STARRED)) {
mindypae7e6a02012-11-29 13:28:10 -08002891 handleDragFromStarred(folder);
2892 return;
2893 }
mindypa8492632012-09-24 09:27:54 -07002894 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
mindypae7e6a02012-11-29 13:28:10 -08002895 final Collection<Conversation> conversations = mSelectedSet.values();
mindypa8492632012-09-24 09:27:54 -07002896 // Add the drop target folder.
2897 dragDropOperations.add(new FolderOperation(folder, true));
2898 // Remove the current folder unless the user is viewing "all".
2899 // That operation should just add the new folder.
2900 boolean isDestructive = !mFolder.isViewAll()
2901 && mFolder.supportsCapability
2902 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2903 if (isDestructive) {
2904 dragDropOperations.add(new FolderOperation(mFolder, false));
2905 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07002906 // Drag and drop is destructive: we remove conversations from the
2907 // current folder.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002908 final DestructiveAction action =
2909 getFolderChange(conversations, dragDropOperations, isDestructive,
2910 true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder);
mindypa8492632012-09-24 09:27:54 -07002911 if (isDestructive) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07002912 delete(0, conversations, action, true);
mindypa8492632012-09-24 09:27:54 -07002913 } else {
2914 action.performAction();
2915 }
Mindy Pereiraacf60392012-04-06 09:11:00 -07002916 }
Mindy Pereira0963ef82012-04-10 11:43:01 -07002917
mindypae7e6a02012-11-29 13:28:10 -08002918 private void handleDragFromStarred(Folder folder) {
2919 final Collection<Conversation> conversations = mSelectedSet.values();
2920 // The conversation list deletes and performs the action if it exists.
2921 final ConversationListFragment convListFragment = getConversationListFragment();
2922 // There should always be a convlistfragment, or the user could not have
2923 // dragged/ dropped conversations.
2924 if (convListFragment != null) {
2925 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2926 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
mindypcb0b30e2012-11-30 10:16:35 -08002927 ArrayList<Uri> folderUris;
2928 ArrayList<Boolean> adds;
mindypae7e6a02012-11-29 13:28:10 -08002929 for (Conversation target : conversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002930 folderUris = new ArrayList<Uri>();
2931 adds = new ArrayList<Boolean>();
2932 folderUris.add(folder.uri);
2933 adds.add(Boolean.TRUE);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002934 final HashMap<Uri, Folder> targetFolders =
2935 Folder.hashMapForFolders(target.getRawFolders());
mindypae7e6a02012-11-29 13:28:10 -08002936 targetFolders.put(folder.uri, folder);
Paul Westbrook26746eb2012-12-06 14:44:01 -08002937 ops.add(mConversationListCursor.getConversationFolderOperation(target,
2938 folderUris, adds, targetFolders.values()));
mindypae7e6a02012-11-29 13:28:10 -08002939 }
2940 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07002941 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08002942 }
2943 refreshConversationList();
2944 mSelectedSet.clear();
mindypae7e6a02012-11-29 13:28:10 -08002945 }
2946 }
2947
2948 private void handleDropInStarred(Folder folder) {
2949 final Collection<Conversation> conversations = mSelectedSet.values();
2950 // The conversation list deletes and performs the action if it exists.
2951 final ConversationListFragment convListFragment = getConversationListFragment();
2952 // There should always be a convlistfragment, or the user could not have
2953 // dragged/ dropped conversations.
2954 if (convListFragment != null) {
2955 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
Vikram Aggarwal669947b2013-01-10 17:05:56 -08002956 convListFragment.requestDelete(R.id.change_folder, conversations,
mindyp5cc0ab22012-12-11 08:47:35 -08002957 new DroppedInStarredAction(conversations, mFolder, folder));
mindypae7e6a02012-11-29 13:28:10 -08002958 }
2959 }
2960
2961 // When dragging conversations to the starred folder, remove from the
2962 // original folder and add a star
2963 private class DroppedInStarredAction implements DestructiveAction {
2964 private Collection<Conversation> mConversations;
2965 private Folder mInitialFolder;
mindyp5cc0ab22012-12-11 08:47:35 -08002966 private Folder mStarred;
mindypae7e6a02012-11-29 13:28:10 -08002967
mindyp5cc0ab22012-12-11 08:47:35 -08002968 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
2969 Folder starredFolder) {
mindypae7e6a02012-11-29 13:28:10 -08002970 mConversations = conversations;
mindyp5cc0ab22012-12-11 08:47:35 -08002971 mInitialFolder = initialFolder;
2972 mStarred = starredFolder;
mindypae7e6a02012-11-29 13:28:10 -08002973 }
2974
2975 @Override
2976 public void performAction() {
2977 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07002978 R.id.change_folder, ToastBarOperation.UNDO, true /* batch */, mInitialFolder);
mindypae7e6a02012-11-29 13:28:10 -08002979 onUndoAvailable(undoOp);
2980 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2981 ContentValues values = new ContentValues();
mindypcb0b30e2012-11-30 10:16:35 -08002982 ArrayList<Uri> folderUris;
2983 ArrayList<Boolean> adds;
mindyp5cc0ab22012-12-11 08:47:35 -08002984 ConversationOperation operation;
mindypae7e6a02012-11-29 13:28:10 -08002985 for (Conversation target : mConversations) {
mindypcb0b30e2012-11-30 10:16:35 -08002986 folderUris = new ArrayList<Uri>();
2987 adds = new ArrayList<Boolean>();
mindyp5cc0ab22012-12-11 08:47:35 -08002988 folderUris.add(mStarred.uri);
2989 adds.add(Boolean.TRUE);
mindypcb0b30e2012-11-30 10:16:35 -08002990 folderUris.add(mInitialFolder.uri);
2991 adds.add(Boolean.FALSE);
mindyp5cc0ab22012-12-11 08:47:35 -08002992 final HashMap<Uri, Folder> targetFolders =
2993 Folder.hashMapForFolders(target.getRawFolders());
2994 targetFolders.put(mStarred.uri, mStarred);
mindypae7e6a02012-11-29 13:28:10 -08002995 targetFolders.remove(mInitialFolder.uri);
mindyp5cc0ab22012-12-11 08:47:35 -08002996 values.put(ConversationColumns.STARRED, true);
2997 operation = mConversationListCursor.getConversationFolderOperation(target,
2998 folderUris, adds, targetFolders.values(), values);
2999 ops.add(operation);
mindypae7e6a02012-11-29 13:28:10 -08003000 }
3001 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003002 mConversationListCursor.updateBulkValues(ops);
mindypae7e6a02012-11-29 13:28:10 -08003003 }
3004 refreshConversationList();
3005 mSelectedSet.clear();
3006 }
3007 }
3008
Mindy Pereira0963ef82012-04-10 11:43:01 -07003009 @Override
Mindy Pereira0963ef82012-04-10 11:43:01 -07003010 public void onTouchEvent(MotionEvent event) {
3011 if (event.getAction() == MotionEvent.ACTION_DOWN) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003012 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
Mark Weid243d452012-10-31 16:24:08 -07003013 hideOrRepositionToastBar(true);
Mindy Pereira0963ef82012-04-10 11:43:01 -07003014 }
3015 }
3016 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003017
Mark Weid243d452012-10-31 16:24:08 -07003018 protected abstract void hideOrRepositionToastBar(boolean animated);
3019
Andy Huang632721e2012-04-11 16:57:26 -07003020 @Override
3021 public void onConversationSeen(Conversation conv) {
3022 mPagerController.onConversationSeen(conv);
3023 }
3024
Andy Huang9d3fd922012-09-26 22:23:58 -07003025 @Override
3026 public boolean isInitialConversationLoading() {
3027 return mPagerController.isInitialConversationLoading();
3028 }
3029
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003030 /**
3031 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
3032 * insufficient because that doesn't check if the window is currently in focus or not.
3033 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003034 private boolean isFragmentVisible(Fragment in) {
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003035 return in != null && in.isVisible() && mActivity.hasWindowFocus();
3036 }
3037
Andy Huangb1c34dc2012-04-17 16:36:19 -07003038 private class ConversationListLoaderCallbacks implements
3039 LoaderManager.LoaderCallbacks<ConversationCursor> {
3040
3041 @Override
3042 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003043 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY);
3044 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY);
3045 if (account == null || folder == null) {
3046 return null;
3047 }
3048 return new ConversationCursorLoader((Activity) mActivity, account,
3049 folder.conversationListUri, folder.name);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003050 }
3051
3052 @Override
3053 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
Andy Huang632721e2012-04-11 16:57:26 -07003054 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
3055 data, loader);
Andy Huang99be1922013-05-01 14:55:41 -07003056 if (isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
Vikram Aggarwala1b59dc2013-04-30 15:45:49 -07003057 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
3058 return;
3059 }
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003060 // Clear our all pending destructive actions before swapping the conversation cursor
3061 destroyPending(null);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003062 mConversationListCursor = data;
Paul Westbrookbf232c32012-04-18 03:17:41 -07003063 mConversationListCursor.addListener(AbstractActivityController.this);
Paul Westbrook937c94f2012-08-16 13:01:18 -07003064 mTracker.onCursorUpdated();
Andy Huange3df1ad2012-04-24 17:15:23 -07003065 mConversationListObservable.notifyChanged();
Yu Ping Hu7c909c72013-01-18 11:58:01 -08003066 // Handle actions that were deferred until after the conversation list was loaded.
3067 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
3068 callback.onLoadFinished();
3069 }
3070 mConversationListLoadFinishedCallbacks.clear();
Paul Westbrook9f119c72012-04-24 16:10:59 -07003071
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003072 final ConversationListFragment convList = getConversationListFragment();
Vikram Aggarwal6422b8d2013-01-14 10:47:41 -08003073 if (isFragmentVisible(convList)) {
Vikram Aggarwal17f373e2012-09-17 15:49:32 -07003074 // The conversation list is already listening to list changes and gets notified
3075 // in the mConversationListObservable.notifyChanged() line above. We only need to
3076 // check and inform the cursor of the change in visibility here.
3077 informCursorVisiblity(true);
Andy Huangb1c34dc2012-04-17 16:36:19 -07003078 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003079 perhapsShowFirstSearchResult();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003080 }
3081
3082 @Override
3083 public void onLoaderReset(Loader<ConversationCursor> loader) {
Paul Westbrook9a70e912012-08-17 15:53:20 -07003084 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
3085 mConversationListCursor, loader);
3086
3087 if (mConversationListCursor != null) {
3088 // Unregister the listener
3089 mConversationListCursor.removeListener(AbstractActivityController.this);
3090 mConversationListCursor = null;
3091
3092 // Inform anyone who is interested about the change
3093 mTracker.onCursorUpdated();
3094 mConversationListObservable.notifyChanged();
Andy Huangb1c34dc2012-04-17 16:36:19 -07003095 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003096 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07003097 }
Andy Huangb1c34dc2012-04-17 16:36:19 -07003098
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003099 /**
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003100 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects.
3101 */
3102 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
3103 @Override
3104 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
3105 final String[] everything = UIProvider.FOLDERS_PROJECTION;
3106 switch (id) {
3107 case LOADER_FOLDER_CURSOR:
3108 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created");
3109 final ObjectCursorLoader<Folder> loader = new
3110 ObjectCursorLoader<Folder>(
3111 mContext, mFolder.uri, everything, Folder.FACTORY);
3112 loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
3113 return loader;
3114 case LOADER_RECENT_FOLDERS:
3115 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created");
Yu Ping Huf2632b92013-03-15 13:56:27 -07003116 if (mAccount != null && mAccount.recentFolderListUri != null
3117 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) {
Vikram Aggarwal177097f2013-03-08 11:19:53 -08003118 return new ObjectCursorLoader<Folder>(mContext,
3119 mAccount.recentFolderListUri, everything, Folder.FACTORY);
3120 }
3121 break;
3122 case LOADER_ACCOUNT_INBOX:
3123 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created");
3124 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
3125 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
3126 mAccount.folderListUri : defaultInbox;
3127 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
3128 if (inboxUri != null) {
3129 return new ObjectCursorLoader<Folder>(mContext, inboxUri,
3130 everything, Folder.FACTORY);
3131 }
3132 break;
3133 case LOADER_SEARCH:
3134 LogUtils.d(LOG_TAG, "LOADER_SEARCH created");
3135 return Folder.forSearchResults(mAccount,
3136 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
3137 mActivity.getActivityContext());
3138 case LOADER_FIRST_FOLDER:
3139 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created");
3140 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI);
3141 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION);
3142 if (mConversationToShow != null && mConversationToShow.position < 0){
3143 mConversationToShow.position = 0;
3144 }
3145 return new ObjectCursorLoader<Folder>(mContext, folderUri,
3146 everything, Folder.FACTORY);
3147 default:
3148 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id);
3149 return null;
3150 }
3151 return null;
3152 }
3153
3154 @Override
3155 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
3156 if (data == null) {
3157 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3158 }
3159 switch (loader.getId()) {
3160 case LOADER_FOLDER_CURSOR:
3161 if (data != null && data.moveToFirst()) {
3162 final Folder folder = data.getModel();
3163 setHasFolderChanged(folder);
3164 mFolder = folder;
3165 mFolderObservable.notifyChanged();
3166 } else {
3167 LogUtils.d(LOG_TAG, "Unable to get the folder %s",
3168 mFolder != null ? mAccount.name : "");
3169 }
3170 break;
3171 case LOADER_RECENT_FOLDERS:
3172 // Few recent folders and we are running on a phone? Populate the default
3173 // recents. The number of default recent folders is at least 2: every provider
3174 // has at least two folders, and the recent folder count never decreases.
3175 // Having a single recent folder is an erroneous case, and we can gracefully
3176 // recover by populating default recents. The default recents will not stomp on
3177 // the existing value: it will be shown in addition to the default folders:
3178 // the max number of recent folders is more than 1+num(defaultRecents).
3179 if (data != null && data.getCount() <= 1 && !mIsTablet) {
3180 final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
3181 @Override
3182 protected Void doInBackground(Uri... uri) {
3183 // Asking for an update on the URI and ignore the result.
3184 final ContentResolver resolver = mContext.getContentResolver();
3185 resolver.update(uri[0], null, null, null);
3186 return null;
3187 }
3188 }
3189 final Uri uri = mAccount.defaultRecentFolderListUri;
3190 LogUtils.v(LOG_TAG, "Default recents at %s", uri);
3191 new PopulateDefault().execute(uri);
3192 break;
3193 }
3194 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
3195 mRecentFolderList.loadFromUiProvider(data);
3196 if (isAnimating()) {
3197 mRecentsDataUpdated = true;
3198 } else {
3199 mRecentFolderObservers.notifyChanged();
3200 }
3201 break;
3202 case LOADER_ACCOUNT_INBOX:
3203 if (data != null && !data.isClosed() && data.moveToFirst()) {
3204 final Folder inbox = data.getModel();
3205 onFolderChanged(inbox);
3206 // Just want to get the inbox, don't care about updates to it
3207 // as this will be tracked by the folder change listener.
3208 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
3209 } else {
3210 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
3211 mAccount != null ? mAccount.name : "");
3212 }
3213 break;
3214 case LOADER_SEARCH:
3215 if (data != null && data.getCount() > 0) {
3216 data.moveToFirst();
3217 final Folder search = data.getModel();
3218 updateFolder(search);
3219 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
3220 mActivity.getIntent()
3221 .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
3222 showConversationList(mConvListContext);
3223 mActivity.invalidateOptionsMenu();
3224 mHaveSearchResults = search.totalCount > 0;
3225 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
3226 } else {
3227 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader");
3228 }
3229 break;
3230 case LOADER_FIRST_FOLDER:
3231 if (data == null || data.getCount() <=0 || !data.moveToFirst()) {
3232 return;
3233 }
3234 final Folder folder = data.getModel();
3235 boolean handled = false;
3236 if (folder != null) {
3237 onFolderChanged(folder);
3238 handled = true;
3239 }
3240 if (mConversationToShow != null) {
3241 // Open the conversation.
3242 showConversation(mConversationToShow);
3243 handled = true;
3244 }
3245 if (!handled) {
3246 // We have an account, but nothing else: load the default inbox.
3247 loadAccountInbox();
3248 }
3249 mConversationToShow = null;
3250 // And don't run this anymore.
3251 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER);
3252 break;
3253 }
3254 }
3255
3256 @Override
3257 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
3258 }
3259 }
3260
3261 /**
3262 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects.
3263 */
3264 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> {
3265 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION;
3266 final CursorCreator<Account> mFactory = Account.FACTORY;
3267
3268 @Override
3269 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) {
3270 switch (id) {
3271 case LOADER_ACCOUNT_CURSOR:
3272 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created");
3273 return new ObjectCursorLoader<Account>(mContext,
3274 MailAppProvider.getAccountsUri(), mProjection, mFactory);
3275 case LOADER_ACCOUNT_UPDATE_CURSOR:
3276 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created");
3277 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection,
3278 mFactory);
3279 default:
3280 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id);
3281 break;
3282 }
3283 return null;
3284 }
3285
3286 @Override
3287 public void onLoadFinished(Loader<ObjectCursor<Account>> loader,
3288 ObjectCursor<Account> data) {
3289 if (data == null) {
3290 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
3291 }
3292 switch (loader.getId()) {
3293 case LOADER_ACCOUNT_CURSOR:
3294 if (data == null) {
3295 // Nothing useful to do if we have no valid data.
3296 break;
3297 }
3298 if (data.getCount() == 0) {
3299 // If an empty cursor is returned, the MailAppProvider is indicating that
3300 // no accounts have been specified. We want to navigate to the
3301 // "add account" activity that will handle the intent returned by the
3302 // MailAppProvider
3303
3304 // If the MailAppProvider believes that all accounts have been loaded,
3305 // and the account list is still empty, we want to prompt the user to add
3306 // an account.
3307 final Bundle extras = data.getExtras();
3308 final boolean accountsLoaded =
3309 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
3310
3311 if (accountsLoaded) {
3312 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent
3313 (mContext);
3314 if (noAccountIntent != null) {
3315 mActivity.startActivityForResult(noAccountIntent,
3316 ADD_ACCOUNT_REQUEST_CODE);
3317 }
3318 }
3319 } else {
3320 final boolean accountListUpdated = accountsUpdated(data);
3321 if (!isLoaderInitialized || accountListUpdated) {
3322 isLoaderInitialized = updateAccounts(data);
3323 }
3324 }
3325 break;
3326 case LOADER_ACCOUNT_UPDATE_CURSOR:
3327 // We have received an update for current account.
3328
3329 // Make sure that this is an update for the current account
3330 if (data != null && data.moveToFirst()) {
3331 final Account updatedAccount = data.getModel();
3332
3333 if (updatedAccount.uri.equals(mAccount.uri)) {
3334 // Keep a reference to the previous settings object
3335 final Settings previousSettings = mAccount.settings;
3336
3337 // Update the controller's reference to the current account
3338 mAccount = updatedAccount;
3339 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
3340 + "mAccount = %s", mAccount.uri);
3341
3342 // Only notify about a settings change if something differs
3343 if (!Objects.equal(mAccount.settings, previousSettings)) {
3344 mAccountObservers.notifyChanged();
3345 }
3346 perhapsEnterWaitMode();
3347 } else {
3348 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:"
3349 + " %s", updatedAccount.uri, mAccount.uri);
3350 // We need to restart the loader, so the correct account information
3351 // will be returned.
3352 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY);
3353 }
3354 }
3355 break;
3356 }
3357 }
3358
3359 @Override
3360 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) {
3361 }
3362 }
3363
3364 /**
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003365 * Updates controller state based on search results and shows first conversation if required.
3366 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003367 private void perhapsShowFirstSearchResult() {
mindypd27009a2012-11-27 11:54:18 -08003368 if (mCurrentConversation == null) {
3369 // Shown for search results in two-pane mode only.
3370 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
3371 && mConversationListCursor.getCount() > 0;
3372 if (!shouldShowFirstConversation()) {
3373 return;
3374 }
3375 mConversationListCursor.moveToPosition(0);
3376 final Conversation conv = new Conversation(mConversationListCursor);
3377 conv.position = 0;
3378 onConversationSelected(conv, true /* checkSafeToModifyFragments */);
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003379 }
Vikram Aggarwalf0ef2302012-11-07 14:53:34 -08003380 }
3381
3382 /**
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003383 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
3384 * next destructive action..
3385 * @param nextAction the next destructive action to be performed. This can be null.
3386 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003387 private void destroyPending(DestructiveAction nextAction) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003388 // If there is a pending action, perform that first.
3389 if (mPendingDestruction != null) {
3390 mPendingDestruction.performAction();
3391 }
3392 mPendingDestruction = nextAction;
3393 }
3394
3395 /**
3396 * Register a destructive action with the controller. This performs the previous destructive
Vikram Aggarwalacaa3c02012-04-24 12:45:27 -07003397 * 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 -07003398 * embellish this method any more.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003399 * @param action the action to register.
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003400 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003401 private void registerDestructiveAction(DestructiveAction action) {
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003402 // TODO(viki): This is not a good idea. The best solution is for clients to request a
3403 // destructive action from the controller and for the controller to own the action. This is
3404 // a half-way solution while refactoring DestructiveAction.
3405 destroyPending(action);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003406 }
3407
Vikram Aggarwal531488e2012-05-29 16:36:52 -07003408 @Override
3409 public final DestructiveAction getBatchAction(int action) {
Vikram Aggarwalc4113952012-05-11 14:14:56 -07003410 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
Vikram Aggarwale8a85322012-04-24 09:01:38 -07003411 registerDestructiveAction(da);
3412 return da;
3413 }
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003414
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003415 @Override
3416 public final DestructiveAction getDeferredBatchAction(int action) {
mindypf0656a12012-10-01 08:30:57 -07003417 return getDeferredAction(action, mSelectedSet.values(), true);
3418 }
3419
3420 /**
3421 * Get a destructive action for a menu action. This is a temporary method,
3422 * to control the profusion of {@link DestructiveAction} classes that are
3423 * created. Please do not copy this paradigm.
3424 * @param action the resource ID of the menu action: R.id.delete, for
3425 * example
3426 * @param target the conversations to act upon.
3427 * @return a {@link DestructiveAction} that performs the specified action.
3428 */
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003429 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
mindypf0656a12012-10-01 08:30:57 -07003430 boolean batch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003431 return new ConversationAction(action, target, batch);
Mindy Pereirade3e74a2012-07-24 09:43:10 -07003432 }
3433
Vikram Aggarwalbc67bb12012-04-30 14:05:35 -07003434 /**
3435 * Class to change the folders that are assigned to a set of conversations. This is destructive
3436 * because the user can remove the current folder from the conversation, in which case it has
3437 * to be animated away from the current folder.
3438 */
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003439 private class FolderDestruction implements DestructiveAction {
Paul Westbrook77eee622012-07-10 13:41:57 -07003440 private final Collection<Conversation> mTarget;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003441 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003442 private final boolean mIsDestructive;
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003443 /** Whether this destructive action has already been performed */
3444 private boolean mCompleted;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003445 private boolean mIsSelectedSet;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003446 private boolean mShowUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003447 private int mAction;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003448 private final Folder mActionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003449
3450 /**
3451 * Create a new folder destruction object to act on the given conversations.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003452 * @param target conversations to act upon.
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003453 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003454 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003455 private FolderDestruction(final Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003456 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003457 boolean showUndo, int action, final Folder actionFolder) {
Paul Westbrook77eee622012-07-10 13:41:57 -07003458 mTarget = ImmutableList.copyOf(target);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003459 mFolderOps.addAll(folders);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003460 mIsDestructive = isDestructive;
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003461 mIsSelectedSet = isBatch;
Mindy Pereira06642fa2012-07-12 16:23:27 -07003462 mShowUndo = showUndo;
Mindy Pereira01f30502012-08-14 10:30:51 -07003463 mAction = action;
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003464 mActionFolder = actionFolder;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003465 }
3466
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003467 @Override
3468 public void performAction() {
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003469 if (isPerformed()) {
3470 return;
3471 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07003472 if (mIsDestructive && mShowUndo) {
mindypcb0b30e2012-11-30 10:16:35 -08003473 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003474 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder);
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003475 onUndoAvailable(undoOp);
3476 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003477 // For each conversation, for each operation, add/ remove the
3478 // appropriate folders.
mindypcb0b30e2012-11-30 10:16:35 -08003479 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3480 ArrayList<Uri> folderUris;
3481 ArrayList<Boolean> adds;
Mindy Pereira8db7e402012-07-13 10:32:47 -07003482 for (Conversation target : mTarget) {
mindypcb0b30e2012-11-30 10:16:35 -08003483 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3484 .getRawFolders());
3485 folderUris = new ArrayList<Uri>();
mindypcb0b30e2012-11-30 10:16:35 -08003486 adds = new ArrayList<Boolean>();
Mindy Pereira01f30502012-08-14 10:30:51 -07003487 if (mIsDestructive) {
3488 target.localDeleteOnUpdate = true;
3489 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003490 for (FolderOperation op : mFolderOps) {
mindypcb0b30e2012-11-30 10:16:35 -08003491 folderUris.add(op.mFolder.uri);
3492 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003493 if (op.mAdd) {
3494 targetFolders.put(op.mFolder.uri, op.mFolder);
3495 } else {
3496 targetFolders.remove(op.mFolder.uri);
3497 }
3498 }
Paul Westbrook26746eb2012-12-06 14:44:01 -08003499 ops.add(mConversationListCursor.getConversationFolderOperation(target,
3500 folderUris, adds, targetFolders.values()));
mindyp389f0b22012-08-29 11:12:54 -07003501 }
3502 if (mConversationListCursor != null) {
Scott Kennedy9e2d4072013-03-21 21:46:01 -07003503 mConversationListCursor.updateBulkValues(ops);
Mindy Pereira8db7e402012-07-13 10:32:47 -07003504 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003505 refreshConversationList();
Mindy Pereiraf3a45562012-05-24 16:30:19 -07003506 if (mIsSelectedSet) {
3507 mSelectedSet.clear();
3508 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003509 }
Mindy Pereira8db7e402012-07-13 10:32:47 -07003510
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003511 /**
3512 * Returns true if this action has been performed, false otherwise.
Andy Huang839ada22012-07-20 15:48:40 -07003513 *
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003514 */
3515 private synchronized boolean isPerformed() {
3516 if (mCompleted) {
3517 return true;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003518 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003519 mCompleted = true;
3520 return false;
Vikram Aggarwal41e6e712012-04-24 11:22:57 -07003521 }
3522 }
Vikram Aggarwal7f602f72012-04-30 16:04:06 -07003523
mindypc84759c2012-08-29 09:51:53 -07003524 public final DestructiveAction getFolderChange(Collection<Conversation> target,
3525 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003526 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
mindypc84759c2012-08-29 09:51:53 -07003527 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003528 isBatch, showUndo, isMoveTo, actionFolder);
mindypc84759c2012-08-29 09:51:53 -07003529 registerDestructiveAction(da);
3530 return da;
3531 }
3532
3533 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
Mindy Pereira8db7e402012-07-13 10:32:47 -07003534 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003535 boolean showUndo, final boolean isMoveTo, final Folder actionFolder) {
3536 return new FolderDestruction(target, folders, isDestructive, isBatch, showUndo,
3537 isMoveTo ? R.id.move_folder : R.id.change_folder, actionFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003538 }
3539
3540 @Override
3541 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3542 Folder toRemove, boolean isDestructive, boolean isBatch,
3543 boolean showUndo) {
3544 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3545 folderOps.add(new FolderOperation(toRemove, false));
3546 return new FolderDestruction(target, folderOps, isDestructive, isBatch,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003547 showUndo, R.id.remove_folder, mFolder);
Mindy Pereira01f30502012-08-14 10:30:51 -07003548 }
3549
Vikram Aggarwal4f4782b2012-05-30 08:39:09 -07003550 @Override
3551 public final void refreshConversationList() {
Vikram Aggarwal75daee52012-04-30 11:13:09 -07003552 final ConversationListFragment convList = getConversationListFragment();
3553 if (convList == null) {
3554 return;
3555 }
3556 convList.requestListRefresh();
3557 }
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003558
3559 protected final ActionClickedListener getUndoClickedListener(
3560 final AnimatedAdapter listAdapter) {
3561 return new ActionClickedListener() {
3562 @Override
3563 public void onActionClicked() {
3564 if (mAccount.undoUri != null) {
3565 // NOTE: We might want undo to return the messages affected, in which case
3566 // the resulting cursor might be interesting...
3567 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3568 // commands to undo
3569 if (mConversationListCursor != null) {
3570 mConversationListCursor.undo(
3571 mActivity.getActivityContext(), mAccount.undoUri);
3572 }
3573 if (listAdapter != null) {
3574 listAdapter.setUndo(true);
3575 }
3576 }
3577 }
3578 };
3579 }
3580
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003581 /**
3582 * Shows an error toast in the bottom when a folder was not fetched successfully.
3583 * @param folder the folder which could not be fetched.
3584 * @param replaceVisibleToast if true, this should replace any currently visible toast.
3585 */
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003586 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003587 mToastBar.setConversationMode(false);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003588
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003589 final ActionClickedListener listener;
3590 final int actionTextResourceId;
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003591 final int lastSyncResult = folder.lastSyncResult;
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003592 switch (lastSyncResult & 0x0f) {
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003593 case UIProvider.LastSyncResult.CONNECTION_ERROR:
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003594 // The sync request that caused this failure.
3595 final int syncRequest = lastSyncResult >> 4;
3596 // Show: User explicitly pressed the refresh button and there is no connection
3597 // Show: The first time the user enters the app and there is no connection
3598 // TODO(viki): Implement this.
3599 // Reference: http://b/7202801
3600 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3601 // Don't show: Already in the app; user switches to a synced label
3602 // Don't show: In a live label and a background sync fails
3603 final boolean avoidToast = !showToast && (folder.syncWindow > 0
3604 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3605 if (avoidToast) {
3606 return;
3607 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003608 listener = getRetryClickedListener(folder);
3609 actionTextResourceId = R.string.retry;
3610 break;
3611 case UIProvider.LastSyncResult.AUTH_ERROR:
3612 listener = getSignInClickedListener();
3613 actionTextResourceId = R.string.signin;
3614 break;
3615 case UIProvider.LastSyncResult.SECURITY_ERROR:
3616 return; // Currently we do nothing for security errors.
3617 case UIProvider.LastSyncResult.STORAGE_ERROR:
3618 listener = getStorageErrorClickedListener();
3619 actionTextResourceId = R.string.info;
3620 break;
3621 case UIProvider.LastSyncResult.INTERNAL_ERROR:
3622 listener = getInternalErrorClickedListener();
3623 actionTextResourceId = R.string.report;
3624 break;
3625 default:
3626 return;
3627 }
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003628 mToastBar.show(listener,
Andrew Sapperstein5d420962012-07-12 16:43:10 -07003629 R.drawable.ic_alert_white,
Vikram Aggarwal41b9e8f2012-09-25 10:15:04 -07003630 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003631 false, /* showActionIcon */
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003632 actionTextResourceId,
Andrew Sapperstein9d7519d2012-07-16 14:03:53 -07003633 replaceVisibleToast,
Scott Kennedy6a3d5ce2013-03-15 17:33:14 -07003634 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder));
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003635 }
3636
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003637 private ActionClickedListener getRetryClickedListener(final Folder folder) {
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003638 return new ActionClickedListener() {
3639 @Override
3640 public void onActionClicked() {
3641 final Uri uri = folder.refreshUri;
3642
3643 if (uri != null) {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003644 startAsyncRefreshTask(uri);
Andrew Sappersteinc2c9dc12012-07-02 18:17:32 -07003645 }
3646 }
3647 };
3648 }
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003649
3650 private ActionClickedListener getSignInClickedListener() {
3651 return new ActionClickedListener() {
3652 @Override
3653 public void onActionClicked() {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003654 promptUserForAuthentication(mAccount);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003655 }
3656 };
3657 }
3658
3659 private ActionClickedListener getStorageErrorClickedListener() {
3660 return new ActionClickedListener() {
3661 @Override
3662 public void onActionClicked() {
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003663 showStorageErrorDialog();
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003664 }
3665 };
3666 }
3667
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003668 private void showStorageErrorDialog() {
3669 DialogFragment fragment = (DialogFragment)
3670 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3671 if (fragment == null) {
3672 fragment = SyncErrorDialogFragment.newInstance();
3673 }
3674 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3675 }
3676
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003677 private ActionClickedListener getInternalErrorClickedListener() {
3678 return new ActionClickedListener() {
3679 @Override
3680 public void onActionClicked() {
Paul Westbrook83e6b572013-02-05 16:22:42 -08003681 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Andrew Sapperstein00179f12012-08-09 15:15:40 -07003682 }
3683 };
3684 }
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003685
3686 @Override
3687 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3688 Uri uri = null;
3689 switch (errorStatus) {
3690 case UIProvider.LastSyncResult.CONNECTION_ERROR:
3691 if (folder != null && folder.refreshUri != null) {
3692 uri = folder.refreshUri;
3693 }
3694 break;
3695 case UIProvider.LastSyncResult.AUTH_ERROR:
Paul Westbrook122f7c22012-08-20 17:50:31 -07003696 promptUserForAuthentication(mAccount);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003697 return;
3698 case UIProvider.LastSyncResult.SECURITY_ERROR:
3699 return; // Currently we do nothing for security errors.
3700 case UIProvider.LastSyncResult.STORAGE_ERROR:
3701 showStorageErrorDialog();
3702 return;
3703 case UIProvider.LastSyncResult.INTERNAL_ERROR:
Paul Westbrook83e6b572013-02-05 16:22:42 -08003704 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
Paul Westbrook4969e0c2012-08-20 14:38:39 -07003705 return;
3706 default:
3707 return;
3708 }
3709
3710 if (uri != null) {
3711 startAsyncRefreshTask(uri);
3712 }
3713 }
3714
3715 @Override
3716 public void onFooterViewLoadMoreClick(Folder folder) {
3717 if (folder != null && folder.loadMoreUri != null) {
3718 startAsyncRefreshTask(folder.loadMoreUri);
3719 }
3720 }
3721
3722 private void startAsyncRefreshTask(Uri uri) {
3723 if (mFolderSyncTask != null) {
3724 mFolderSyncTask.cancel(true);
3725 }
3726 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3727 mFolderSyncTask.execute();
3728 }
Paul Westbrook122f7c22012-08-20 17:50:31 -07003729
3730 private void promptUserForAuthentication(Account account) {
Paul Westbrook429399b2012-08-24 11:19:17 -07003731 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
Paul Westbrook122f7c22012-08-20 17:50:31 -07003732 final Intent authenticationIntent =
3733 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3734 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3735 }
3736 }
mindypca87de42012-09-28 15:02:39 -07003737
3738 @Override
3739 public void onAccessibilityStateChanged() {
3740 // Clear the cache of objects.
3741 ConversationItemViewModel.onAccessibilityUpdated();
3742 // Re-render the list if it exists.
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003743 final ConversationListFragment frag = getConversationListFragment();
mindypca87de42012-09-28 15:02:39 -07003744 if (frag != null) {
3745 AnimatedAdapter adapter = frag.getAnimatedAdapter();
3746 if (adapter != null) {
3747 adapter.notifyDataSetInvalidated();
3748 }
3749 }
3750 }
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003751
3752 @Override
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07003753 public void makeDialogListener (final int action, final boolean isBatch) {
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003754 final Collection<Conversation> target;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003755 if (isBatch) {
3756 target = mSelectedSet.values();
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003757 } else {
3758 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
3759 target = Conversation.listOf(mCurrentConversation);
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003760 }
3761 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003762 mDialogAction = action;
Vikram Aggarwalb8c31712013-01-03 17:03:19 -08003763 mDialogFromSelectedSet = isBatch;
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003764 mDialogListener = new AlertDialog.OnClickListener() {
3765 @Override
3766 public void onClick(DialogInterface dialog, int which) {
Vikram Aggarwal84fe9942013-04-17 11:20:58 -07003767 delete(action, target, destructiveAction, isBatch);
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003768 // Afterwards, let's remove references to the listener and the action.
3769 setListener(null, -1);
3770 }
3771 };
3772 }
3773
3774 @Override
3775 public AlertDialog.OnClickListener getListener() {
3776 return mDialogListener;
3777 }
3778
3779 /**
3780 * Sets the listener for the positive action on a confirmation dialog. Since only a single
3781 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to
3782 * unset the listener; in which case action should be set to -1.
Vikram Aggarwal715f83d2013-03-05 17:04:56 -08003783 * @param listener the listener that will perform the task for this dialog's positive action.
3784 * @param action the action that created this dialog.
Vikram Aggarwal6cadbfc2012-12-27 09:17:05 -08003785 */
3786 private void setListener(AlertDialog.OnClickListener listener, final int action){
3787 mDialogListener = listener;
3788 mDialogAction = action;
3789 }
Vikram Aggarwal69a6cdf2013-01-08 16:05:17 -08003790
3791 @Override
3792 public VeiledAddressMatcher getVeiledAddressMatcher() {
3793 return mVeiledMatcher;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003794 }
3795
3796 @Override
3797 public void setDetachedMode() {
3798 // Tell the conversation list not to select anything.
3799 final ConversationListFragment frag = getConversationListFragment();
3800 if (frag != null) {
3801 frag.setChoiceNone();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08003802 } else if (mIsTablet) {
3803 // How did we ever land here? Detached mode, and no CLF on tablet???
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003804 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
3805 }
3806 mDetachedConvUri = mCurrentConversation.uri;
3807 }
3808
3809 private void clearDetachedMode() {
3810 // Tell the conversation list to go back to its usual selection behavior.
3811 final ConversationListFragment frag = getConversationListFragment();
3812 if (frag != null) {
3813 frag.revertChoiceMode();
Vikram Aggarwal81230ee2013-02-26 15:26:44 -08003814 } else if (mIsTablet) {
3815 // How did we ever land here? Detached mode, and no CLF on tablet???
3816 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
Vikram Aggarwala91d00b2013-01-18 12:00:37 -08003817 }
3818 mDetachedConvUri = null;
3819 }
Andy Huang12b3ee42013-04-24 22:49:43 -07003820
3821 private class MailDrawerListener implements DrawerLayout.DrawerListener {
3822 @Override
3823 public void onDrawerOpened(View drawerView) {
3824 mDrawerToggle.onDrawerOpened(drawerView);
3825 }
3826
3827 @Override
3828 public void onDrawerClosed(View drawerView) {
3829 mDrawerToggle.onDrawerClosed(drawerView);
3830 if (mHasNewAccountOrFolder) {
3831 refreshDrawer();
3832 }
3833 }
3834
3835 /**
3836 * As part of the overriden function, it will animate the alpha of the conversation list
3837 * view along with the drawer sliding when we're in the process of switching accounts or
3838 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}.
3839 */
3840 @Override
3841 public void onDrawerSlide(View drawerView, float slideOffset) {
3842 mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
3843 if (mHasNewAccountOrFolder && mListViewForAnimating != null) {
3844 mListViewForAnimating.setAlpha(slideOffset);
3845 }
3846 }
3847
3848 /**
3849 * This condition here should only be called when the drawer is stuck in a weird state
3850 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh
3851 * and, more importantly, unlock the drawer when this is the case.
3852 */
3853 @Override
3854 public void onDrawerStateChanged(int newState) {
3855 mDrawerToggle.onDrawerStateChanged(newState);
3856 if (mHasNewAccountOrFolder && newState == DrawerLayout.STATE_IDLE) {
3857 refreshDrawer();
3858 }
3859 }
3860
3861 /**
3862 * If we've reached a stable drawer state, unlock the drawer for usage, clear the
3863 * conversation list, and finish end actions. Also, make
3864 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing.
3865 */
3866 public void refreshDrawer() {
3867 mHasNewAccountOrFolder = false;
3868 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
3869 ConversationListFragment conversationList = getConversationListFragment();
3870 if (conversationList != null) {
3871 conversationList.clear();
3872 }
3873 mDrawerObservers.notifyChanged();
3874 }
3875 }
3876
Scott Kennedy8a72b852013-05-02 14:18:50 -07003877 @Override
3878 public boolean isDrawerPullEnabled() {
3879 return getShouldAllowDrawerPull(mViewMode.getMode());
3880 }
Vikram Aggarwal4a5c5302012-01-12 15:07:13 -08003881}