Java tutorial
/******************************************************************************* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ /* ========================================================================== */ /* Modifications on Features list / Changes Request / Problems Report */ /* -------------------------------------------------------------------------- */ /* date | author | Key | comment */ /* --------------|---------------- |--------------------|------------------- */ /* 11/28/2014 | wei.huang | FR846709 | ] Email will */ /* | | | force close and */ /* | | | back to homescreen*/ /* --------------|------------------|--------------------|--------------------*/ /* 01/12/2014 | Zhenhua.Fan | PR-854923 |Move to Dialog */ /* | | |should not show in */ /* | | |Combine View */ /* ---------- |------------------|--------------------|----------------- */ /******************************************************************************/ /* =========================================================================================================== *HISTORY * *Tag Date Author Description *============== ============ =============== ============================================================== *CONFLICT-20001 2014/10/24 wenggangjin Modify the package conflict *BUGFIX-844194 2014/11/26 zhaotianyong [Android5.0][Email][Crash] Email crashs when first launching it *BUGFIX-845345 2014/12/19 wenggangjin [Android5.0][Email][UE] Should show selection info at top of screen *BUGFIX-879393 2014/12/20 xiaolin.li [Android5.0][Email][Monkey][Crash] com.tct.email crash caused by java.lang.NullPointerException *BUGFIX-887553 2014/12/30 xiaolin.li [Email]Quick horizontal sliding flash back in the mail details interface *BUGFIX-881447 2014/12/31 wenggangjin [Email]Can't click "retry" icon *BUGFIX-879468 2014/1/5 junwei-xu [Email]Add unread widget to the desktop after the restart the phone click into the inbox *BUGFIX-881437 2015/1/22 junwei-xu [Email]Click "Drafts" can't pop up "Move to inbox..." *BUGFIX-913979 2015-02-05 wenggangjin [REG][Exchange]Exchange search very slowly and can not search anything *BUGFIX-927510 2015-02-09 wenggangjin [Android5.0][Email][Crash] Email app crash when re-launching it after configured an account *BUGFIX-948137 2015-03-18 zhonghua.tuo [Monitor][Email]All Email account disappear sometimes *BUGFIX-942796 2015-03-24 zheng.zou [Email] Improve Email search function. *BUGFIX-957916 2015/4/2 junwei-xu [Android5.0][Email]No response when we touch the folder name in some folders *BUGFIX-968391 2015/4/5 gangjin.weng [Android5.0][Email][REG]Can't delete words in server search by sougou input method *BUGFIX-991085 2015/03/30 jin.dong [Email]After the web side to change the password, MS without prompting *BUGFIX-1022808 2015/6/23 chao zhang [Email]After change password,TCL exchange can not work,always in "Getting your messages" page *BUGFIX-1027389 2015/7/7 yanhua.chen [Email]It's no response when tap report after prompt internal error Edit Notification *BUGFIX-1355979 2016/01/15 chao-zhang [Android M][Email][Force close]Mutiple press UNDO Email force close =========================================================================================================== */ package com.tct.mail.ui; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.MissingFormatArgumentException; import java.util.Set; import java.util.TimerTask; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentManager; import android.app.LoaderManager; import android.app.SearchManager; import android.content.*; import android.content.DialogInterface.OnClickListener; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.database.DataSetObservable; import android.database.DataSetObserver; import android.database.Observable; import android.graphics.Outline; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.os.SystemClock; import android.provider.BaseColumns; import android.provider.SearchRecentSuggestions; import android.support.annotation.NonNull; import android.support.v4.content.PermissionChecker; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.DragEvent; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.CompoundButton; import android.widget.HorizontalScrollView; import android.widget.ImageButton; import android.widget.ListView; import android.widget.Toast; import com.tct.email.EmailApplication; import com.tct.email.R; import com.tct.emailcommon.provider.EmailContent; import com.tct.emailcommon.provider.Mailbox; import com.tct.emailcommon.service.SearchParams; import com.tct.fw.google.common.base.Objects; import com.tct.fw.google.common.collect.ImmutableList; import com.tct.fw.google.common.collect.Lists; import com.tct.fw.google.common.collect.Sets; import com.tct.mail.ConversationListContext; import com.tct.mail.MailLogService; import com.tct.mail.analytics.Analytics; import com.tct.mail.analytics.AnalyticsTimer; import com.tct.mail.analytics.AnalyticsUtils; import com.tct.mail.browse.ConfirmDialogFragment; import com.tct.mail.browse.ConversationCursor; import com.tct.mail.browse.ConversationCursor.ConversationOperation; import com.tct.mail.browse.ConversationItemViewModel; import com.tct.mail.browse.ConversationMessage; import com.tct.mail.browse.ConversationPagerController; import com.tct.mail.browse.SelectedConversationsActionMenu; import com.tct.mail.browse.SyncErrorDialogFragment; import com.tct.mail.browse.UndoCallback; import com.tct.mail.compose.ComposeActivity; import com.tct.mail.content.CursorCreator; import com.tct.mail.content.ObjectCursor; import com.tct.mail.content.ObjectCursorLoader; import com.tct.mail.preferences.AccountPreferences; import com.tct.mail.providers.Account; import com.tct.mail.providers.Conversation; import com.tct.mail.providers.ConversationInfo; import com.tct.mail.providers.Folder; import com.tct.mail.providers.FolderWatcher; import com.tct.mail.providers.MailAppProvider; import com.tct.mail.providers.Settings; import com.tct.mail.providers.SuggestionsProvider; import com.tct.mail.providers.UIProvider; import com.tct.mail.providers.UIProvider.AccountCapabilities; import com.tct.mail.providers.UIProvider.AccountCursorExtraKeys; import com.tct.mail.providers.UIProvider.AutoAdvance; import com.tct.mail.providers.UIProvider.ConversationColumns; import com.tct.mail.providers.UIProvider.ConversationOperations; import com.tct.mail.providers.UIProvider.FolderCapabilities; import com.tct.mail.providers.UIProvider.FolderType; import com.tct.mail.ui.ActionableToastBar.ActionClickedListener; import com.tct.mail.utils.*; import com.tct.permission.PermissionUtil; //[BUGFIX]-ADD by SCDTABLET.shujing.jin@tcl.com,08/05/2016,2635083 import com.tct.emailcommon.utility.Utility; //TS: MOD by wenggangjin for CONFLICT_20001 START //import com.google.common.base.Objects; //import com.google.common.collect.ImmutableList; //import com.google.common.collect.Lists; //import com.google.common.collect.Sets; //TS: MOD by wenggangjin for CONFLICT_20001 END //[FEATURE]-Add-BEGIN by TSNJ,Zhenhua.Fan,06/11/2014,FR-622609 /* ================================================================================= *HISTORY * *Tag Date Author Description *============== ============ =============== ==================================== *BUGFIX-883925 2015/1/7 jian.xu Click on the leftmost mail, return icon anomaly *BUGFIX-902637 2015-01-20 wenggangjin [Email]The menu display wrong after some oprations in mark list screen *BUGFIX-941849 2015-03-10 qing.liang [Android5.0][Email][Force close]Email force close when tapping 'Undo' after rotating screen. *BUGFIX-947440 2015/03/12 ke.ma [Email] Need to add Email FAB button shadow effect. *BUGFIX-944708 2015/03/18 zheng.zou [Email]Can not stop search email when no network *BUGFIX-976622 2015/4/24 yanhua.chen [Monitor][FC][Email]Email flash back when open email from widget *BUGFIX-976970 2015/5/08 zheng.zou [Email]Portrait and landscape screen to switch each other occur black screen *BUGFIX_996919 2015/06/04 zheng.zou [Email](new) draft auto saving & discard ui change *BUGFIX_1013807 2015/06/09 jin.dong [SW][Email][Monitor] Return to login interface after login account *BUGFIX_1019473 2015/07/01 jin.dong [Android5.0][Email] Not highlight search word when search on server. *BUGFIX_552138 2015/09/01 zheng.zou [Email](new) draft auto saving & discard ui change *BUGFIX-526255 2015-09-01 zheng.zou CR:swipe to delete or star unstar email *FEATURE-559891 2015/09/10 tao.gan [Email] Auto hiding aciont bar in mail content UI *FEATURE-559893 2015/09/11 tao.gan [Email]Auto hiding action bar in mail box list *BUGFIX_1065353 2015/09/08 kaifeng.lu [Performance][Email]Email is too slow and always refresh emails after rotating screen when loading email content via data flow. *BUGFIX_721230 2015/10/22 zheng.zou [Android L][Email]The compose icon overlap the UNDO notification *TASK-869664 2015/11/25 zheng.zou [Email]Android M Permission Upgrade *BUGFIX_1101083 2015/11/10 kaifeng.lu [Android L][Email][Monitor] Cannot exit Inbox by pressing 'Back' key after stress test *BUGFIX_1126514 2015/12/23 chao-zhang [Monkey][Force Close][Email]Email crashes during monkey test ================================================================================= */ //[FEATURE]-Add-END by TSNJ,Zhenhua.Fan /** * This is an abstract implementation of the Activity Controller. This class * knows how to respond to menu items, state changes, layout changes, etc. It * weaves together the views and listeners, dispatching actions to the * respective underlying classes. * <p> * Even though this class is abstract, it should provide default implementations * for most, if not all the methods in the ActivityController interface. This * makes the task of the subclasses easier: OnePaneActivityController and * TwoPaneActivityController can be concise when the common functionality is in * AbstractActivityController. * </p> * <p> * In the Gmail codebase, this was called BaseActivityController * </p> */ public abstract class AbstractActivityController implements ActivityController, EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener, View.OnClickListener { // Keys for serialization of various information in Bundles. /** TCT: Tag for save global search */ private static final String SAVED_GLOBAL_SEARCH = "saved-global-search"; /** TCT: Tag for {@link ConversationListContext#localSearch} */ private static final String SAVED_LOCAL_SEARCH = "saved-local-search"; /** Tag for {@link #mAccount} */ private static final String SAVED_ACCOUNT = "saved-account"; /** Tag for {@link #mFolder} */ private static final String SAVED_FOLDER = "saved-folder"; /** Tag for {@link #mCurrentConversation} */ private static final String SAVED_CONVERSATION = "saved-conversation"; /** Tag for {@link #mSelectedSet} */ private static final String SAVED_SELECTED_SET = "saved-selected-set"; /** Tag for {@link ActionableToastBar#getOperation()} */ private static final String SAVED_TOAST_BAR_OP = "saved-toast-844469bar-op"; private static final String SAVED_UNDO_CONVERSATION = "saved-undo-conversation"; private static final String SAVED_UNDO_ACTION = "saved-undo-action"; /** Tag for {@link #mFolderListFolder} */ private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder"; /** Tag for {@link ConversationListContext#searchQuery} */ private static final String SAVED_QUERY = "saved-query"; /** Tag for {@link #mDialogAction} */ private static final String SAVED_ACTION = "saved-action"; /** Tag for {@link #mDialogFromSelectedSet} */ private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected"; /** Tag for {@link #mDetachedConvUri} */ private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri"; /** Key to store {@link #mInbox}. */ private static final String SAVED_INBOX_KEY = "m-inbox"; /** Key to store {@link #mConversationListScrollPositions} */ private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS = "saved-conversation-list-scroll-positions"; private static final String SAVED_DRAFT_MSG_ID = "saved-discard-msg-id"; /** Tag used when loading a wait fragment */ protected static final String TAG_WAIT = "wait-fragment"; /** Tag used when loading a conversation list fragment. */ public static final String TAG_CONVERSATION_LIST = "tag-conversation-list"; /** Tag used when loading a custom fragment. */ protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment"; /** Key to store an account in a bundle */ private final String BUNDLE_ACCOUNT_KEY = "account"; /** Key to store a folder in a bundle */ private final String BUNDLE_FOLDER_KEY = "folder"; //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S /** key to store check status for star toggle */ private final String BUNDLE_CHECK_STATUS_KEY = "star-check-status"; /** check status for star toggle in actionbar */ private boolean mCheckStatus = false; //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E /** * Key to set a flag for the ConversationCursorLoader to ignore any * initial load limit that may be set by the Account. Instead, * perform a full load instead of the full-stage load. */ private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY = "ignore-initial-conversation-limit"; private final String BUNDLE_CONVERSATION_ORDER_KEY = "conversation-order"; //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD protected Account mAccount; protected Folder mFolder; protected Folder mInbox; /** True when {@link #mFolder} is first shown to the user. */ private boolean mFolderChanged = false; protected ActionBarController mActionBarController; protected final MailActivity mActivity; protected final Context mContext; private final FragmentManager mFragmentManager; protected final RecentFolderList mRecentFolderList; protected ConversationListContext mConvListContext; protected Conversation mCurrentConversation; /** * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode. */ private Uri mDetachedConvUri; /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */ private final Bundle mConversationListScrollPositions = new Bundle(); /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ private SuppressNotificationReceiver mNewEmailReceiver = null; /** Handler for all our local runnables. */ protected Handler mHandler = new Handler(); /** * The current mode of the application. All changes in mode are initiated by * the activity controller. View mode changes are propagated to classes that * attach themselves as listeners of view mode changes. */ protected final ViewMode mViewMode; protected ContentResolver mResolver; protected boolean mHaveAccountList = false; private AsyncRefreshTask mAsyncRefreshTask; private boolean mDestroyed; /** True if running on tablet */ private final boolean mIsTablet; /** * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment * transactions? (including back stack manipulation) * <p> * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart * and onResume. */ private boolean mSafeToModifyFragments = true; private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); protected ConversationCursor mConversationListCursor; private final DataSetObservable mConversationListObservable = new MailObservable("List"); /** Runnable that checks the logging level to enable/disable the logging service. */ private Runnable mLogServiceChecker = null; /** List of all accounts currently known to the controller. This is never null. */ private Account[] mAllAccounts = new Account[0]; private FolderWatcher mFolderWatcher; private boolean mIgnoreInitialConversationLimit; /** * Interface for actions that are deferred until after a load completes. This is for handling * user actions which affect cursors (e.g. marking messages read or unread) that happen before * that cursor is loaded. */ private interface LoadFinishedCallback { void onLoadFinished(); } /** The deferred actions to execute when mConversationListCursor load completes. */ private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks = new ArrayList<LoadFinishedCallback>(); private RefreshTimerTask mConversationListRefreshTask; /** Listeners that are interested in changes to the current account. */ private final DataSetObservable mAccountObservers = new MailObservable("Account"); /** Listeners that are interested in changes to the recent folders. */ private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder"); /** Listeners that are interested in changes to the list of all accounts. */ private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts"); /** Listeners that are interested in changes to the current folder. */ private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder"); /** Listeners that are interested in changes to the Folder or Account selection */ private final DataSetObservable mFolderOrAccountObservers = new MailObservable("FolderOrAccount"); /** * Selected conversations, if any. */ private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); private final int mFolderItemUpdateDelayMs; /** Keeps track of selected and unselected conversations */ final protected ConversationPositionTracker mTracker; /** * Action menu associated with the selected set. */ SelectedConversationsActionMenu mCabActionMenu; /** The compose button floating over the conversation/search lists */ protected View mFloatingComposeButton; protected ActionableToastBar mToastBar; protected ConversationPagerController mPagerController; // This is split out from the general loader dispatcher because its loader doesn't return a // basic Cursor /** Handles loader callbacks to create a convesation cursor. */ private final ConversationListLoaderCallbacks mListCursorCallbacks = new ConversationListLoaderCallbacks(); /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */ private final FolderLoads mFolderCallbacks = new FolderLoads(); /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */ private final AccountLoads mAccountCallbacks = new AccountLoads(); /** * Matched addresses that must be shielded from users because they are temporary. Even though * this is instantiated from settings, this matcher is valid for all accounts, and is expected * to live past the life of an account. */ private final VeiledAddressMatcher mVeiledMatcher; protected static final String LOG_TAG = LogTag.getLogTag(); // Loader constants: Accounts /** * The list of accounts. This loader is started early in the application life-cycle since * the list of accounts is central to all other data the application needs: unread counts for * folders, critical UI settings like show/hide checkboxes, ... * The loader is started when the application is created: both in * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never * destroyed since the cursor is needed through the life of the application. When the list of * accounts changes, we notify {@link #mAllAccountObservers}. */ private static final int LOADER_ACCOUNT_CURSOR = 0; /** * The current account. This loader is started when we have an account. The mail application * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount}, * we start a loader to observe for changes on the current account. * The loader is always restarted when an account is set in {@link #setAccount(Account)}. * When the current account object changes, we notify {@link #mAccountObservers}. * A possible performance improvement would be to listen purely on * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list, * and would avoid two updates when a single setting on the current account changes. */ private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 1; // Loader constants: Conversations /** The conversation cursor over the current conversation list. This loader provides * a cursor over conversation entries from a folder to display a conversation * list. * This loader is started when the user switches folders (in {@link #updateFolder(Folder)}, * or when the controller is told that a folder/account change is imminent * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of * the current folder. When the user switches folders, the old loader is destroyed and a new * one is created. * * When the conversation list changes, we notify {@link #mConversationListObservable}. */ private static final int LOADER_CONVERSATION_LIST = 10; // Loader constants: misc /** * The loader that determines whether the Warm welcome tour should be displayed for the user. */ public static final int LOADER_WELCOME_TOUR = 20; /** * The load which loads accounts for the welcome tour. */ public static final int LOADER_WELCOME_TOUR_ACCOUNTS = 21; // Loader constants: Folders /** The current folder. This loader watches for updates to the current folder in a manner * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder * might be due to server-side changes (unread count), or local changes (sync window or sync * status change). * The change of current folder calls {@link #updateFolder(Folder)}. * This is responsible for restarting a loader using the URI of the provided folder. When the * loader returns, the current folder is updated and consumers, if any, are notified. * When the current folder changes, we notify {@link #mFolderObservable} */ private static final int LOADER_FOLDER_CURSOR = 30; /** * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent * folders are tied to the current account being viewed. When the account is changed, * we restart this loader to retrieve the recent accounts. Recents are pre-populated for * phones historically, when they were displayed in the spinner. On the tablet, * they showed in the {@link FolderListFragment} and were not-populated. The code to * pre-populate the recents is somewhat convoluted: when the loader returns a short list of * recent folders, it issues an update on the Recent Folder URI. The underlying provider then * does the appropriate thing to populate recent folders, and notify of a change on the cursor. * Recent folders are needed for the life of the current account. * When the recent folders change, we notify {@link #mRecentFolderObservers}. */ private static final int LOADER_RECENT_FOLDERS = 31; /** * The primary inbox for the current account. The mechanism to load the default inbox for the * current account is (sadly) different from loading other folders. The method * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR} * over the current folder. * When we have a valid cursor, we destroy this loader, This convoluted flow is historical. */ private static final int LOADER_ACCOUNT_INBOX = 32; /** * The fake folder of search results for a term. When we search for a term, * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity, * we start a loader which returns conversations that match the user-provided query. * We destroy the loader when we obtain a valid cursor since subsequent searches will create * a new activity. */ private static final int LOADER_SEARCH = 33; /** * The initial folder at app start. When the application is launched from an intent that * specifies the initial folder (notifications/widgets/shortcuts), * then we extract the folder URI from the intent, but we cannot trust the folder object. Since * shortcuts and widgets persist past application update, they might have incorrect * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri}, * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed. * An additional complication arises if we have to view a specific conversation within this * folder. This is the case when launching the app from a single conversation notification * or tapping on a specific conversation in the widget. In these cases, the conversation is * saved in {@link #mConversationToShow} and is retrieved when the loader returns. */ public static final int LOADER_FIRST_FOLDER = 34; /** * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only * other class that uses this activity's LoaderManager. If another class needs activity-level * loaders, consider consolidating the loaders in a central location: a UI-less fragment * perhaps. */ public static final int LAST_LOADER_ID = 35; /** * T: local search loader. */ private static final int LOADER_LOCALSEARCH_CONVERSATION_LIST = 110; /** /** * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or * fragments, and within an activity, loader IDs need to be unique. Currently, * SectionedInboxTeaserView is the only class that uses the * {@link ConversationListFragment}'s LoaderManager. */ public static final int LAST_FRAGMENT_LOADER_ID = 1000; /** Code returned after an account has been added. */ private static final int ADD_ACCOUNT_REQUEST_CODE = 1; /** Code returned when the user has to enter the new password on an existing account. */ private static final int REAUTHENTICATE_REQUEST_CODE = 2; /** Code returned when the previous activity needs to navigate to a different folder * or account */ private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3; public static final String EXTRA_FOLDER = "extra-folder"; public static final String EXTRA_ACCOUNT = "extra-account"; private static final int DRAFT_SAVE_PRIORITY = 10; //TS: zheng.zou 2015-07-14 EMAIL FEATURE_996919 ADD /** The pending destructive action to be carried out before swapping the conversation cursor.*/ private DestructiveAction mPendingDestruction; protected AsyncRefreshTask mFolderSyncTask; private Folder mFolderListFolder; private boolean mIsDragHappening; private final int mShowUndoBarDelay; private boolean mRecentsDataUpdated; /** A wait fragment we added, if any. */ private WaitFragment mWaitFragment; /** True if we have results from a search query */ private boolean mHaveSearchResults = false; /** If a confirmation dialog is being show, the listener for the positive action. */ private OnClickListener mDialogListener; /** * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This * is used to create a new {@link #mDialogListener} on orientation changes. */ private int mDialogAction = -1; /** * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set * and false if it acts on the currently selected conversation */ private boolean mDialogFromSelectedSet; //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_S private int mUndoAction = -1; private Conversation mUndoConversation = null; //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_E /** Which conversation to show, if started from widget/notification. */ private Conversation mConversationToShow = null; /** * A temporary reference to the pending destructive action that was deferred due to an * auto-advance transition in progress. * <p> * In detail: when auto-advance triggers a mode change, we must wait until the transition * completes before executing the destructive action to ensure a smooth mode change transition. * This member variable houses the pending destructive action work to be run upon completion. */ private Runnable mAutoAdvanceOp = null; protected DrawerLayout mDrawerContainer; protected View mDrawerPullout; protected ActionBarDrawerToggle mDrawerToggle; protected ListView mListViewForAnimating; protected boolean mHasNewAccountOrFolder; private boolean mConversationListLoadFinishedIgnored; private final MailDrawerListener mDrawerListener = new MailDrawerListener(); private boolean mHideMenuItems; private final DrawIdler mDrawIdler = new DrawIdler(); // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 ADD_S //when first launch email,this will start twice,this field to avoid it private boolean mFirstLoadAccount = true; // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 ADD_E public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment"; /// TCT: If this is a global search private boolean mGlobalSearch; private DraftSaveBroadcastReceiver mDraftReceiver = new DraftSaveBroadcastReceiver(); //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD private long mSavedDraftMsgId = -1; // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD //TS: tao.gan 2015-09-10 EMAIL FEATURE-559891 ADD_S //animator when show view private AnimatorSet backAnimatorSet; //animator when hide view private AnimatorSet hideAnimatorSet; //indicate that whether toolbar and fab button is hidden private boolean mToolbarHidden = false; private HorizontalScrollView mSearchHeader; //TS: tao.gan 2015-09-10 EMAIL FEATURE-559891 ADD_E //TS: Gantao 2015-12-16 EMAIL BUGFIX_1171140 ADD_S //Count of mails in mail list private int mConversationCount; //TS: Gantao 2015-12-16 EMAIL BUGFIX_1171140 ADD_E private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() { @Override public void onChanged() { super.onChanged(); if (mConversationListCursor != null) { mConversationListCursor.handleNotificationActions(); } } }; private final HomeButtonListener mHomeButtonListener = new HomeButtonListener(); public AbstractActivityController(MailActivity activity, ViewMode viewMode) { mActivity = activity; mFragmentManager = mActivity.getFragmentManager(); mViewMode = viewMode; mContext = activity.getApplicationContext(); mRecentFolderList = new RecentFolderList(mContext); mTracker = new ConversationPositionTracker(this); // Allow the fragment to observe changes to its own selection set. No other object is // aware of the selected set. mSelectedSet.addObserver(this); final Resources r = mContext.getResources(); mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms); mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms); mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources()); mIsTablet = Utils.useTabletUI(r); mConversationListLoadFinishedIgnored = false; } @Override public Account getCurrentAccount() { return mAccount; } @Override public ConversationListContext getCurrentListContext() { return mConvListContext; } @Override public final ConversationCursor getConversationListCursor() { return mConversationListCursor; } /** * Check if the fragment is attached to an activity and has a root view. * @param in fragment to be checked * @return true if the fragment is valid, false otherwise */ private static boolean isValidFragment(Fragment in) { return !(in == null || in.getActivity() == null || in.getView() == null); } /** * Get the conversation list fragment for this activity. If the conversation list fragment is * not attached, this method returns null. * * Caution! This method returns the {@link ConversationListFragment} after the fragment has been * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the * fragment. There is a non-trivial amount of time after the fragment is instantiated and before * this call returns a non-null value, depending on the {@link FragmentManager}. If you * need the fragment immediately after adding it, consider making the fragment an observer of * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)} */ public ConversationListFragment getConversationListFragment() { final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST); if (isValidFragment(fragment)) { return (ConversationListFragment) fragment; } return null; } /** * Returns the folder list fragment attached with this activity. If no such fragment is attached * this method returns null. * * Caution! This method returns the {@link FolderListFragment} after the fragment has been * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the * fragment. There is a non-trivial amount of time after the fragment is instantiated and before * this call returns a non-null value, depending on the {@link FragmentManager}. If you * need the fragment immediately after adding it, consider making the fragment an observer of * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)} */ protected FolderListFragment getFolderListFragment() { final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag); final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag); if (isValidFragment(fragment)) { return (FolderListFragment) fragment; } return null; } /** * Initialize the action bar. This is not visible to OnePaneController and * TwoPaneController so they cannot override this behavior. */ protected void initializeActionBar() { final ActionBar actionBar = mActivity.getSupportActionBar(); if (actionBar == null) { return; } Intent intent = mActivity.getIntent(); /// TCT: Action is search but no extra account means a global search final boolean isSearch = intent != null && Intent.ACTION_SEARCH.equals(intent.getAction()) && intent.hasExtra(Utils.EXTRA_ACCOUNT); mActionBarController = isSearch ? new SearchActionBarController(mContext) : new ActionBarController(mContext); mActionBarController.initialize(mActivity, this, actionBar); // init the action bar to allow the 'up' affordance. // any configurations that disallow 'up' should do that later. mActionBarController.setBackButton(); } /** * Attach the action bar to the activity. */ private void attachActionBar() { final ActionBar actionBar = mActivity.getSupportActionBar(); if (actionBar != null) { // Show a title final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME; actionBar.setDisplayOptions(mask, mask); mActionBarController.setViewModeController(mViewMode); } } /** * Returns whether the conversation list fragment is visible or not. * Different layouts will have their own notion on the visibility of * fragments, so this method needs to be overriden. * */ protected abstract boolean isConversationListVisible(); /** * If required, starts wait mode for the current account. */ final void perhapsEnterWaitMode() { // If the account is not initialized, then show the wait fragment, since nothing can be // shown. if (mAccount.isAccountInitializationRequired()) { LogUtils.d(LOG_TAG, "AAC.perhapsEnterWaitMode-->showWaitForInitialization,The account neet initialized"); showWaitForInitialization(); return; } final boolean inWaitingMode = inWaitMode(); final boolean isSyncRequired = mAccount.isAccountSyncRequired(); //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_S // attempts to get the inbox, actually, it would be null until folder sync completed. final Folder inbox = mFolderWatcher != null ? mFolderWatcher.getDefaultInbox(mAccount) : null; LogUtils.d(LOG_TAG, "AAC.perhapsEnterWaitMode inWaitingMode [%s], isSyncRequired [%s], inbox [%s]", inWaitingMode, isSyncRequired, inbox); //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_E if (isSyncRequired) { if (inWaitingMode) { // Update the WaitFragment's account object updateWaitMode(); } else { // Transition to waiting mode showWaitForInitialization(); //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_S LogUtils.d(LOG_TAG, "AAC.perhapsEnterWaitMode-->showWaitForInitialization now!!!"); //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_E } } else if (inWaitingMode && inbox != null) {//TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 MOD_S // Dismiss waiting mode hideWaitForInitialization(); } } @Override public void switchToDefaultInboxOrChangeAccount(Account account) { LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account); if (mViewMode.isSearchMode()) { // We are in an activity on top of the main navigation activity. // We need to return to it with a result code that indicates it should navigate to // a different folder. final Intent intent = new Intent(); intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account); mActivity.setResult(Activity.RESULT_OK, intent); mActivity.finish(); return; } final boolean firstLoad = mAccount == null; final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri); // If the active account has been clicked in the drawer, go to default inbox if (switchToDefaultInbox) { loadAccountInbox(); return; } changeAccount(account); } public void changeAccount(Account account) { LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account); // Is the account or account settings different from the existing account? final boolean firstLoad = mAccount == null; final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri); // If nothing has changed, return early without wasting any more time. if (!accountChanged && !account.settingsDiffer(mAccount)) { return; } // We also don't want to do anything if the new account is null if (account == null) { LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called."); return; } final String emailAddress = account.getEmailAddress(); mHandler.post(new Runnable() { @Override public void run() { MailActivity.setNfcMessage(emailAddress); } }); if (accountChanged) { commitDestructiveActions(false); } Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE, AnalyticsUtils.getAccountTypeForAccount(emailAddress)); // Change the account here setAccount(account); // And carry out associated actions. cancelRefreshTask(); if (accountChanged) { loadAccountInbox(); } // Check if we need to force setting up an account before proceeding. if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) { // Launch the intent! final Intent intent = new Intent(Intent.ACTION_EDIT); intent.setPackage(mContext.getPackageName()); intent.setData(mAccount.settings.setupIntentUri); mActivity.startActivity(intent); } } /** * Adds a listener interested in change in the current account. If a class is storing a * reference to the current account, it should listen on changes, so it can receive updates to * settings. Must happen in the UI thread. */ @Override public void registerAccountObserver(DataSetObserver obs) { mAccountObservers.registerObserver(obs); } /** * Removes a listener from receiving current account changes. * Must happen in the UI thread. */ @Override public void unregisterAccountObserver(DataSetObserver obs) { mAccountObservers.unregisterObserver(obs); } @Override public void registerAllAccountObserver(DataSetObserver observer) { mAllAccountObservers.registerObserver(observer); } @Override public void unregisterAllAccountObserver(DataSetObserver observer) { mAllAccountObservers.unregisterObserver(observer); } @Override public Account[] getAllAccounts() { return mAllAccounts; } @Override public Account getAccount() { return mAccount; } @Override public void registerFolderOrAccountChangedObserver(final DataSetObserver observer) { mFolderOrAccountObservers.registerObserver(observer); } @Override public void unregisterFolderOrAccountChangedObserver(final DataSetObserver observer) { mFolderOrAccountObservers.unregisterObserver(observer); } /** * If the drawer is open, the function locks the drawer to the closed, thereby sliding in * the drawer to the left edge, disabling events, and refreshing it once it's either closed * or put in an idle state. */ @Override public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount, Folder nextFolder) { if (!isDrawerEnabled()) { if (hasNewFolderOrAccount) { mFolderOrAccountObservers.notifyChanged(); } return; } // If there are no new folders or accounts to switch to, just close the drawer if (!hasNewFolderOrAccount) { mDrawerContainer.closeDrawers(); return; } // Otherwise, start preloading the conversation list for the new folder. if (nextFolder != null) { //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S // reset star toggle status. mActionBarController.setCheckStatus(false); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E preloadConvList(nextAccount, nextFolder); } // Remember if the conversation list view is animating final ConversationListFragment conversationList = getConversationListFragment(); if (conversationList != null) { mListViewForAnimating = conversationList.getListView(); } else { // There is no conversation list to animate, so just set it to null mListViewForAnimating = null; } if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) { // Lets the drawer listener update the drawer contents and notify the FolderListFragment mHasNewAccountOrFolder = true; mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } else { // Drawer is already closed, notify observers that is the case. if (hasNewFolderOrAccount) { mFolderOrAccountObservers.notifyChanged(); } } } /** * Load the conversation list early for the given folder. This happens when some UI element * (usually the drawer) instructs the controller that an account change or folder change is * imminent. While the UI element is animating, the controller can preload the conversation * list for the default inbox of the account provided here or to the folder provided here. * * @param nextAccount The account which the app will switch to shortly, possibly null. * @param nextFolder The folder which the app will switch to shortly, possibly null. */ protected void preloadConvList(Account nextAccount, Folder nextFolder) { // Fire off the conversation list loader for this account already with a fake // listener. final Bundle args = new Bundle(2); if (nextAccount != null) { args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount); } else { args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); } if (nextFolder != null) { args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder); } else { LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder"); } mFolder = null; final LoaderManager lm = mActivity.getLoaderManager(); lm.destroyLoader(LOADER_CONVERSATION_LIST); lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); } //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S /** * Load conversation list when click the star toggle in actionbar. */ private void loadConvListByStarToggle(Account nextAccount, Folder nextFolder) { final Bundle args = new Bundle(3); //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 MOD if (nextAccount != null) { args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount); } else { args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); } if (nextFolder != null) { args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder); } else { LogUtils.e(LOG_TAG, new Error(), "AAC.loadConvListByStarToggle(): Got an empty folder"); } args.putInt(BUNDLE_CONVERSATION_ORDER_KEY, SortHelper.getCurrentSort()); //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD final LoaderManager lm = mActivity.getLoaderManager(); lm.destroyLoader(LOADER_CONVERSATION_LIST); lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { //save the check status for star toggle and reload conversation list. if (mCheckStatus != isChecked) { mCheckStatus = isChecked; loadConvListByStarToggle(mAccount, mFolder); } } @Override public boolean getCurrentStarToggleStatus() { return mCheckStatus; } //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E /** * Initiates the async request to create a fake search folder, which returns conversations that * match the query term provided by the user. Returns immediately. * @param intent Intent that the app was started with. This intent contains the search query. */ private void fetchSearchFolder(Intent intent) { final Bundle args = new Bundle(1); args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent.getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); /// TCT: add search field for remote search. @{ args.putString(SearchParams.BUNDLE_QUERY_FIELD, intent.getStringExtra(SearchParams.BUNDLE_QUERY_FIELD)); /// @} mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks); } @Override public void onFolderChanged(Folder folder, final boolean force) { if (isDrawerEnabled()) { /** If the folder doesn't exist, or its parent URI is empty, * this is not a child folder */ final boolean isTopLevel = Folder.isRoot(folder); final int mode = mViewMode.getMode(); mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel)); mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); mDrawerContainer.closeDrawers(); } if (mFolder == null || !mFolder.equals(folder)) { // We are actually changing the folder, so exit cab mode exitCabMode(); } final String query; //TS: zheng.zou 2015-03-24 EMAIL BUGFIX-942796 MOD_S //keep search word after go back from view conversation // if (folder != null && folder.isType(FolderType.SEARCH)) { // query = mConvListContext.searchQuery; // } else { // query = null; // } if (mConvListContext != null) { query = mConvListContext.searchQuery; } else { query = null; } //TS: zheng.zou 2015-03-24 EMAIL BUGFIX-942796 MOD_E changeFolder(folder, query, force); /** TCT: For global search, enter local search mode and search the text @{ */ if (mGlobalSearch) { LogUtils.logFeature(LogTag.SEARCH_TAG, "[Global Search]Enter and execute local search"); mActionBarController.expandSearch(mActivity.getIntent().getStringExtra(SearchManager.QUERY), null); } /** @{ */ } /** * Sets the folder state without changing view mode and without creating a list fragment, if * possible. * @param folder the folder whose list of conversations are to be shown * @param query the query string for a list of conversations matching a search */ private void setListContext(Folder folder, String query) { updateFolder(folder); final boolean localSearching = mConvListContext != null && mConvListContext.isLocalSearch(); final String localSearchField = mConvListContext != null ? mConvListContext.getSearchField() : null; if (query != null) { mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query); } else { mConvListContext = ConversationListContext.forFolder(mAccount, mFolder); } /// TCT: restore last local search params after re-instance list context.@{ mConvListContext.setLocalSearch(localSearching); mConvListContext.setSearchField(localSearchField); /// @} cancelRefreshTask(); } /** * Changes the folder to the value provided here. This causes the view mode to change. * @param folder the folder to change to * @param query if non-null, this represents the search string that the folder represents. * @param force <code>true</code> to force a folder change, <code>false</code> to disallow * changing to the current folder */ private void changeFolder(Folder folder, String query, final boolean force) { if (!Objects.equal(mFolder, folder)) { commitDestructiveActions(false); } if (folder != null && (!folder.equals(mFolder) || force) || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) { if (folder != null && !folder.equals(mFolder)) { SortHelper.resetCurrentOrder(); } setListContext(folder, query); showConversationList(mConvListContext); // Touch the current folder: it is different, and it has been accessed. // Prevent rare NPE b/18017065 if (mFolder != null) { mRecentFolderList.touchFolder(mFolder, mAccount); } //TS: jin.dong 2015-12-29 EMAIL FEATURE_1125784 ADD_S if (folder != null && folder.isInbox()) { showManualRefreshInRoamingIfNeed(); } //TS: jin.dong 2015-12-29 EMAIL FEATURE_1125784 ADD_E } resetActionBarIcon(); } @Override public void onFolderSelected(Folder folder) { onFolderChanged(folder, false /* force */); } /** * Adds a listener interested in change in the recent folders. If a class is storing a * reference to the recent folders, it should listen on changes, so it can receive updates. * Must happen in the UI thread. */ @Override public void registerRecentFolderObserver(DataSetObserver obs) { mRecentFolderObservers.registerObserver(obs); } /** * Removes a listener from receiving recent folder changes. * Must happen in the UI thread. */ @Override public void unregisterRecentFolderObserver(DataSetObserver obs) { mRecentFolderObservers.unregisterObserver(obs); } @Override public RecentFolderList getRecentFolders() { return mRecentFolderList; } @Override public void loadAccountInbox() { boolean handled = false; if (mFolderWatcher != null) { final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount); if (inbox != null) { onFolderChanged(inbox, false /* force */); handled = true; } } if (!handled) { LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount); restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY); //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_S LogUtils.d(LOG_TAG, "Restart a LOADER_ACCOUNT_INBOX"); // if the inbox has not be prepared when we change to new account, we must revert everything /// that depends on a mailbox. such as loader, actionbar @{ final LoaderManager lm = mActivity.getLoaderManager(); lm.destroyLoader(LOADER_FOLDER_CURSOR); // Also destroy the loader LOADER_CONVERSATION_LIST. lm.destroyLoader(LOADER_CONVERSATION_LIST); mInbox = null; mFolder = null; mActionBarController.setFolder(mFolder); perhapsEnterWaitMode(); return; //TS: chaozhang 2015-6-26 EMAIL BUGFIX_1022808 ADD_E } final int mode = mViewMode.getMode(); if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { mViewMode.enterConversationListMode(); } } @Override public void setFolderWatcher(FolderWatcher watcher) { mFolderWatcher = watcher; } /** * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to * mFolder. * @param newFolder the new folder we are switching to. */ private void setHasFolderChanged(final Folder newFolder) { // We should never try to assign a null folder. But in the rare event that we do, we should // only set the bit when we have a valid folder, and null is not valid. if (newFolder == null) { return; } // If the previous folder was null, or if the two folders represent different data, then we // consider that the folder has changed. if (mFolder == null || !newFolder.equals(mFolder)) { mFolderChanged = true; } } /** * Sets the current folder if it is different from the object provided here. This method does * NOT notify the folder observers that a change has happened. Observers are notified when we * get an updated folder from the loaders, which will happen as a consequence of this method * (since this method starts/restarts the loaders). * @param folder The folder to assign */ private void updateFolder(Folder folder) { if (folder == null || !folder.isInitialized()) { LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder); return; } if (folder.equals(mFolder)) { LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder); return; } final boolean wasNull = mFolder == null; LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name); final LoaderManager lm = mActivity.getLoaderManager(); // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to // ensure that the folder is different from the previous folder before marking the // folder changed. setHasFolderChanged(folder); mFolder = folder; // We do not need to notify folder observers yet. Instead we start the loaders and // when the load finishes, we will get an updated folder. Then, we notify the // folderObservers in onLoadFinished. mActionBarController.setFolder(mFolder); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S mActivity.invalidateOptionsMenu(); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E // Only when we switch from one folder to another do we want to restart the // folder and conversation list loaders (to trigger onCreateLoader). // The first time this runs when the activity is [re-]initialized, we want to re-use the // previous loader's instance and data upon configuration change (e.g. rotation). // If there was not already an instance of the loader, init it. if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) { lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks); } else { lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks); } if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) { // If there was an existing folder AND we have changed // folders, we want to restart the loader to get the information // for the newly selected folder lm.destroyLoader(LOADER_CONVERSATION_LIST); } /// TCT: Fix the empty view will always flash out here. no need show empty view when folde changing, // folders, we want to restart the loader to get the information /// cause loadConversationListData would always destroy loader,and the cursor always be empty when do this. @{ // for the newly selected folder final ConversationListFragment conversationList = getConversationListFragment(); // TS: zheng.zou 2015-05-8 EMAIL BUGFIX-976970 DEL_S // lm.destroyLoader(LOADER_CONVERSATION_LIST); // TS: zheng.zou 2015-05-8 EMAIL BUGFIX-976970 DEL_E if (conversationList != null) { conversationList.getListView().setEmptyView(null); } loadConversationListData(true); } /** * TCT: Move data load code to an independent function, if we just want refresh loader data. * call this to refresh cursor data(local search or update folder) */ private void loadConversationListData(boolean folderUpdated) { if (mFolder == null || !mFolder.isInitialized()) { LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", mFolder); return; } final LoaderManager lm = mActivity.getLoaderManager(); if (mConvListContext != null && mConvListContext.isLocalSearchExecuted()) { final Bundle args = new Bundle(4); args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); args.putString(SearchParams.BUNDLE_QUERY_FIELD, mConvListContext.getSearchField()); args.putString(SearchParams.BUNDLE_QUERY_TERM, mConvListContext.getSearchQuery()); LogUtils.logFeature(LogTag.SEARCH_TAG, "loadConversationListData for local search query [%s], field [%s]", mConvListContext.getSearchQuery(), mConvListContext.getSearchField()); ///TCT: Before we start the local search loader, need cancel the normal conversation list // loader to avoid no need load and wrong result display by the load finish delay.@{ if (lm.getLoader(LOADER_CONVERSATION_LIST) != null) { lm.destroyLoader(LOADER_CONVERSATION_LIST); } // @} lm.restartLoader(LOADER_LOCALSEARCH_CONVERSATION_LIST, args, mListCursorCallbacks); } else { ///TCT: Before we start the normal search loader, need cancel the local search conversation list // loader to avoid no need load and wrong result display by the load finish delay.@{ if (lm.getLoader(LOADER_LOCALSEARCH_CONVERSATION_LIST) != null) { lm.destroyLoader(LOADER_LOCALSEARCH_CONVERSATION_LIST); } // @} // TS: kaifeng.lu 2015-09-8 EMAIL BUGFIX-1065353 DEL_S // final ConversationCursorLoader ccl = (ConversationCursorLoader) ((Object) lm // .getLoader(LOADER_CONVERSATION_LIST)); // if (ccl != null && !ccl.getUri().equals(mFolder.conversationListUri) && folderUpdated) { // If there was an existing folder AND we have changed // folders, we want to restart the loader to get the information // for the newly selected folder // lm.destroyLoader(LOADER_CONVERSATION_LIST); // } // TS: kaifeng.lu 2015-09-8 EMAIL BUGFIX-1065353 DEL_E final Bundle args = new Bundle(2); args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, mIgnoreInitialConversationLimit); mIgnoreInitialConversationLimit = false; lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); } } @Override public Folder getFolder() { return mFolder; } @Override public Folder getHierarchyFolder() { return mFolderListFolder; } @Override public void setHierarchyFolder(Folder folder) { mFolderListFolder = folder; } /** * The mail activity calls other activities for two specific reasons: * <ul> * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li> * <li>To update the password on a current account. The result {@link * #REAUTHENTICATE_REQUEST_CODE} is received.</li> * </ul> * @param requestCode * @param resultCode * @param data */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ADD_ACCOUNT_REQUEST_CODE: // We were waiting for the user to create an account // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 ADD_S mFirstLoadAccount = true; // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 ADD_E if (resultCode == Activity.RESULT_OK) { // restart the loader to get the updated list of accounts mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY, mAccountCallbacks); } else { // The user failed to create an account, just exit the app mActivity.finish(); } break; case REAUTHENTICATE_REQUEST_CODE: if (resultCode == Activity.RESULT_OK) { // The user successfully authenticated, attempt to refresh the list final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null; if (refreshUri != null) { startAsyncRefreshTask(refreshUri); } } break; case CHANGE_NAVIGATION_REQUEST_CODE: if (resultCode == Activity.RESULT_OK && data != null) { // We have have received a result that indicates we need to navigate to a // different folder or account. This happens if someone navigates using the // drawer on the search results activity. final Folder folder = data.getParcelableExtra(EXTRA_FOLDER); final Account account = data.getParcelableExtra(EXTRA_ACCOUNT); if (folder != null) { onFolderSelected(folder); mViewMode.enterConversationListMode(); } else if (account != null) { switchToDefaultInboxOrChangeAccount(account); mViewMode.enterConversationListMode(); } } break; } } //TS: zheng.zou 2015-11-25 EMAIL TASK_996919 ADD_S @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionUtil.REQ_CODE_PERMISSION_SAVE_ATTACHMENT || requestCode == PermissionUtil.REQ_CODE_PERMISSION_REDOWNLOAD_ATTACHMENT || requestCode == PermissionUtil.REQ_CODE_PERMISSION_VIEW_ATTACHMENT) { //TS: zheng.zou 2016-1-22 EMAIL BUGFIX-1431088 MOD for (String permission : permissions) { if (PermissionChecker.checkSelfPermission(mContext, permission) != PackageManager.PERMISSION_GRANTED) { showNeedPermissionToast(R.string.permission_needed_to_save_attachment); } } } else if (requestCode == PermissionUtil.REQ_CODE_PERMISSION_SEE_CALENDAR) { for (String permission : permissions) { if (PermissionChecker.checkSelfPermission(mContext, permission) != PackageManager.PERMISSION_GRANTED) { showNeedPermissionToast(R.string.permission_needed_to_see_calendar); } } } } //TS: zheng.zou 2015-11-25 EMAIL TASK_996919 ADD_E /** * Inform the conversation cursor that there has been a visibility change. * @param visible true if the conversation list is visible, false otherwise. */ protected synchronized void informCursorVisiblity(boolean visible) { if (mConversationListCursor != null) { Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged); // We have informed the cursor. Subsequent visibility changes should not tell it that // the folder has changed. mFolderChanged = false; } } @Override public void onConversationListVisibilityChanged(boolean visible) { informCursorVisiblity(visible); commitAutoAdvanceOperation(); // Notify special views final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible); } } /** * Called when a conversation is visible. Child classes must call the super class implementation * before performing local computation. */ @Override public void onConversationVisibilityChanged(boolean visible) { commitAutoAdvanceOperation(); } /** * Commits any pending destructive action that was earlier deferred by an auto-advance * mode-change transition. */ private void commitAutoAdvanceOperation() { if (mAutoAdvanceOp != null) { mAutoAdvanceOp.run(); mAutoAdvanceOp = null; } } /** * Initialize development time logging. This can potentially log a lot of PII, and we don't want * to turn it on for shipped versions. */ private void initializeDevLoggingService() { if (!MailLogService.DEBUG_ENABLED) { return; } // Check every 5 minutes. final int WAIT_TIME = 5 * 60 * 1000; // Start a runnable that periodically checks the log level and starts/stops the service. mLogServiceChecker = new Runnable() { /** True if currently logging. */ private boolean mCurrentlyLogging = false; /** * If the logging level has been changed since the previous run, start or stop the * service. */ private void startOrStopService() { // If the log level is already high, start the service. final Intent i = new Intent(mContext, MailLogService.class); final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough(); if (mCurrentlyLogging == loggingEnabled) { // No change since previous run, just return; return; } if (loggingEnabled) { LogUtils.e(LOG_TAG, "Starting MailLogService"); mContext.startService(i); } else { LogUtils.e(LOG_TAG, "Stopping MailLogService"); mContext.stopService(i); } mCurrentlyLogging = loggingEnabled; } @Override public void run() { startOrStopService(); mHandler.postDelayed(this, WAIT_TIME); } }; // Start the runnable right away. mHandler.post(mLogServiceChecker); } /** * The application can be started from the following entry points: * <ul> * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of * as Starting the app?.</li> * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li> * <li>Widget: Shows the contents of a synced label, and allows: * <ul> * <li>Viewing the list (tapping on the title)</li> * <li>Composing a new message (tapping on the new message icon in the title. This * launches the {@link ComposeActivity}. * </li> * <li>Viewing a single message (tapping on a list element)</li> * </ul> * * </li> * <li>Tapping on a notification: * <ul> * <li>Shows message list if more than one message</li> * <li>Shows the conversation if the notification is for a single message</li> * </ul> * </li> * <li>...and most importantly, the activity life cycle can tear down the application and * restart it: * <ul> * <li>Rotate the application: it is destroyed and recreated.</li> * <li>Navigate away, and return from recent applications.</li> * </ul> * </li> * <li>Add a new account: fires off an intent to add an account, * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li> * <li>Re-authenticate your account: again returns in onActivityResult().</li> * <li>Composing can happen from many entry points: third party applications fire off an * intent to compose email, and launch directly into the {@link ComposeActivity} * .</li> * </ul> * {@inheritDoc} */ @SuppressLint("NewApi") @Override public boolean onCreate(Bundle savedState) { initializeActionBar(); initializeDevLoggingService(); // Allow shortcut keys to function for the ActionBar and menus. mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); mResolver = mActivity.getContentResolver(); mNewEmailReceiver = new SuppressNotificationReceiver(); mRecentFolderList.initialize(mActivity); mVeiledMatcher.initialize(this); mFloatingComposeButton = mActivity.findViewById(R.id.compose_button); //TS: ke.ma 2015-03-12 EMAIL BUGFIX-947440 ADD_S mFloatingComposeButton.setElevation(8); mFloatingComposeButton.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { // TODO Auto-generated method stub outline.setOval(0, 0, view.getWidth(), view.getWidth()); } }); //TS: ke.ma 2015-03-12 EMAIL BUGFIX-947440 ADD_E mFloatingComposeButton.setOnClickListener(this); if (isDrawerEnabled()) { mDrawerToggle = new ActionBarDrawerToggle(mActivity, mDrawerContainer, // false, // R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close); mDrawerContainer.setDrawerListener(mDrawerListener); mDrawerContainer.setDrawerShadow(mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START); mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled()); } else { final ActionBar ab = mActivity.getSupportActionBar(); ab.setHomeAsUpIndicator(R.drawable.ic_drawer); ab.setHomeActionContentDescription(R.string.drawer_open); ab.setDisplayHomeAsUpEnabled(true); } // All the individual UI components listen for ViewMode changes. This // simplifies the amount of logic in the AbstractActivityController, but increases the // possibility of timing-related bugs. mViewMode.addListener(this); mPagerController = new ConversationPagerController(mActivity, this); mToastBar = findActionableToastBar(mActivity); attachActionBar(); mDrawIdler.setRootView(mActivity.getWindow().getDecorView()); final Intent intent = mActivity.getIntent(); // Immediately handle a clean launch with intent, and any state restoration // that does not rely on restored fragments or loader data // any state restoration that relies on those can be done later in // onRestoreInstanceState, once fragments are up and loader data is re-delivered if (savedState != null) { //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S // restore check status for star toggle mCheckStatus = savedState.getBoolean(BUNDLE_CHECK_STATUS_KEY, false); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E /// TCT: restore global search tag. if (savedState.containsKey(SAVED_GLOBAL_SEARCH)) { mGlobalSearch = savedState.getBoolean(SAVED_GLOBAL_SEARCH); LogUtils.logFeature(LogTag.SEARCH_TAG, "onCreate restore mGlobalSearch [%s] ", mGlobalSearch); } if (savedState.containsKey(SAVED_ACCOUNT)) { setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT)); } if (savedState.containsKey(SAVED_FOLDER)) { final Folder folder = savedState.getParcelable(SAVED_FOLDER); /** * TCT: Restore the local search or global search instance: * 1. Restore the ConversationListContext from Bundle. * 2. Restore query if in global search mode. * 3. Update Local Search UI (ActionBarView) * @{ */ final Bundle bundle = savedState.getParcelable(SAVED_LOCAL_SEARCH); if (bundle != null) { final ConversationListContext convListContext = ConversationListContext.forBundle(bundle); mConvListContext = convListContext; LogUtils.logFeature(LogTag.SEARCH_TAG, "onCreate restore ConverationListContext from saved instance [%s] ", mConvListContext); } String query = mConvListContext != null ? mConvListContext.getSearchQuery() : null; if (TextUtils.isEmpty(query) && mGlobalSearch) { query = intent.getStringExtra(SearchManager.QUERY); mConvListContext.setLocalSearch(true); mConvListContext.setSearchQueryText(query); LogUtils.logFeature(LogTag.SEARCH_TAG, "onCreate restore global search query [%s]", mConvListContext); } setListContext(folder, query); if (mConvListContext.isLocalSearch()) { LogUtils.logFeature(LogTag.SEARCH_TAG, "[Local Search] Enter and execute local search [%s]", query); mActionBarController.expandSearch(query, mConvListContext.getSearchField()); } /** @} */ } if (savedState.containsKey(SAVED_ACTION)) { mDialogAction = savedState.getInt(SAVED_ACTION); } mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false); mViewMode.handleRestore(savedState); } else if (intent != null) { handleIntent(intent); } // Create the accounts loader; this loads the account switch spinner. mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY, mAccountCallbacks); return true; } /** * @param activity the activity that has been inflated * @return the Actionable Toast Bar defined within the activity */ protected ActionableToastBar findActionableToastBar(MailActivity activity) { return (ActionableToastBar) activity.findViewById(R.id.toast_bar); } @Override public void onPostCreate(Bundle savedState) { if (!isDrawerEnabled()) { return; } // Sync the toggle state after onRestoreInstanceState has occurred. mDrawerToggle.syncState(); mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout); } @Override public void onConfigurationChanged(Configuration newConfig) { if (isDrawerEnabled()) { mDrawerToggle.onConfigurationChanged(newConfig); } } /** * This controller listens for clicks on items in the floating action bar. * * @param view the item that was clicked in the floating action bar */ @Override public void onClick(View view) { final int viewId = view.getId(); if (viewId == R.id.compose_button) { ComposeActivity.compose(mActivity.getActivityContext(), getAccount()); } else if (viewId == android.R.id.home) { // TODO: b/16627877 onUpPressed(); } } /** * If drawer is open/visible (even partially), close it. */ protected void closeDrawerIfOpen() { if (!isDrawerEnabled()) { return; } if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) { mDrawerContainer.closeDrawers(); } } @Override public void onStart() { mSafeToModifyFragments = true; NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver); if (mViewMode.getMode() != ViewMode.UNKNOWN) { Analytics.getInstance().sendView("MainActivity" + mViewMode.toString()); } IntentFilter filter = new IntentFilter(ComposeActivity.DRAFT_SAVED_ACTION); filter.setPriority(DRAFT_SAVE_PRIORITY); mActivity.registerReceiver(mDraftReceiver, filter); //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD } @Override public void onRestart() { final DialogFragment fragment = (DialogFragment) mFragmentManager .findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); if (fragment != null) { fragment.dismiss(); } // When the user places the app in the background by pressing "home", // dismiss the toast bar. However, since there is no way to determine if // home was pressed, just dismiss any existing toast bar when restarting // the app. if (mToastBar != null) { mToastBar.hide(false, false /* actionClicked */); } } @Override public Dialog onCreateDialog(int id, Bundle bundle) { return null; } @Override public final boolean onCreateOptionsMenu(Menu menu) { if (mViewMode.isAdMode()) { return false; } final MenuInflater inflater = mActivity.getMenuInflater(); inflater.inflate(mActionBarController.getOptionsMenuId(), menu); mActionBarController.onCreateOptionsMenu(menu); return true; } @Override public final boolean onKeyDown(int keyCode, KeyEvent event) { //[FRETURE]-ADD-BEGIN by TSNJ.wei huang 11/24/2014 FR848855 if (keyCode == event.KEYCODE_BACK) { if (mCabActionMenu != null) { if (mCabActionMenu.isActivated()) { if (mCabActionMenu.isActionModeNull()) { return false; } else { mCabActionMenu.onSetEmpty(); } return true; } else { return false; } } } //[FEATURE]-ADD-END by TSNJ.wei huang // TS: gangjin.weng 2015-4-5 EMAIL BUGFIX-968391 ADD_S if (keyCode == KeyEvent.KEYCODE_DEL && mCabActionMenu == null && event != null && event.getScanCode() == 0 && event.getMetaState() == 0 && event.getFlags() == (KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)) { return true; } // TS: gangjin.weng 2015-4-5 EMAIL BUGFIX-968391 ADD_E return false; } public abstract boolean doesActionChangeConversationListVisibility(int action); /** * Helper function that determines if we should associate an undo callback with * the current menu action item * @param actionId the id of the action * @return the appropriate callback handler, or null if not applicable */ private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance(int actionId, final Conversation conv) { // We associated the undoCallback if the user is going to perform an action on the current // conversation, causing the current conversation to be removed from view and replacing it // with another (via Auto Advance). The undoCallback will bring the removed conversation // back into the view if the action is undone. final Collection<Conversation> convCol = Conversation.listOf(conv); final boolean isApplicableForReshow = mAccount != null && mAccount.settings != null && mTracker != null && // ensure that we will show another conversation due to Auto Advance mTracker.getNextConversation(mAccount.settings.getAutoAdvanceSetting(), convCol) != null && // ensure that we are performing the action from conversation view isCurrentConversationInView(convCol) && // check for the appropriate destructive actions doesActionRemoveCurrentConversationFromView(actionId); return (isApplicableForReshow) ? new UndoCallback() { @Override public void performUndoCallback() { showConversation(conv); } } : null; } /** * Check if the provided action will remove the active conversation from view * @param actionId the applied action * @return true if it will remove the conversation from view, false otherwise */ private boolean doesActionRemoveCurrentConversationFromView(int actionId) { return actionId == R.id.archive || actionId == R.id.delete || actionId == R.id.discard_outbox || actionId == R.id.remove_folder || actionId == R.id.report_spam || actionId == R.id.report_phishing || actionId == R.id.move_to; } @Override public boolean onOptionsItemSelected(MenuItem item) { /* * The action bar home/up action should open or close the drawer. * mDrawerToggle will take care of this. */ if (isDrawerEnabled() && mDrawerToggle.onOptionsItemSelected(item)) { Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle", null, 0); return true; } Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0); final int id = item.getItemId(); LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id); boolean handled = true; /** This is NOT a batch action. */ final boolean isBatch = false; final Collection<Conversation> target = Conversation.listOf(mCurrentConversation); final Settings settings = (mAccount == null) ? null : mAccount.settings; // The user is choosing a new action; commit whatever they had been // doing before. Don't animate if we are launching a new screen. commitDestructiveActions(!doesActionChangeConversationListVisibility(id)); final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance(id, mCurrentConversation); //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_S mUndoAction = id; mUndoConversation = mCurrentConversation; //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_E if (id == R.id.archive) { final boolean showDialog = (settings != null && settings.confirmArchive); confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback); } else if (id == R.id.remove_folder) { delete(R.id.remove_folder, target, getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback), isBatch); } else if (id == R.id.delete) { final boolean showDialog = (settings != null && settings.confirmDelete); confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback); } else if (id == R.id.discard_drafts) { // drafts are lost forever, so always confirm confirmAndDelete(id, target, true /* showDialog */, R.plurals.confirm_discard_drafts_conversation, undoCallback); } else if (id == R.id.discard_outbox) { // discard in outbox means we discard the failed message and save them in drafts delete(id, target, getDeferredAction(id, target, isBatch, undoCallback), isBatch); } else if (id == R.id.mark_important) { updateConversation(Conversation.listOf(mCurrentConversation), ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH); } else if (id == R.id.mark_not_important) { if (mFolder != null && mFolder.isImportantOnly()) { delete(R.id.mark_not_important, target, getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback), isBatch); } else { updateConversation(Conversation.listOf(mCurrentConversation), ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); } } else if (id == R.id.mute) { delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback), isBatch); } else if (id == R.id.report_spam) { delete(R.id.report_spam, target, getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch); } else if (id == R.id.mark_not_spam) { // Currently, since spam messages are only shown in list with // other spam messages, // marking a message not as spam is a destructive action delete(R.id.mark_not_spam, target, getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch); } else if (id == R.id.report_phishing) { delete(R.id.report_phishing, target, getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch); } else if (id == android.R.id.home) { onUpPressed(); } else if (id == R.id.compose) { ComposeActivity.compose(mActivity.getActivityContext(), mAccount); } else if (id == R.id.local_search) {//[FEATURE]-Add-BEGIN by TSCD.zhonghua.tuo,05/28/2014,FR 670064 ActionBarController.SERVICE_SEARCH_MODE = false; mActivity.invalidateOptionsMenu(); } else if (id == R.id.service_search) { ActionBarController.SERVICE_SEARCH_MODE = true; executeSearch(mActionBarController.getSearchWidgetText());//[FEATURE]-Add-END by TSCD.zhonghua.tuo } else if (id == R.id.refresh) { requestFolderRefresh(); } else if (id == R.id.settings) { Utils.showSettings(mActivity.getActivityContext(), mAccount); } else if (id == R.id.help_info_menu_item) { mActivity.showHelp(mAccount, mViewMode.getMode()); } else if (id == R.id.move_to || id == R.id.change_folders) { //TS: junwei-xu 2015-4-2 EMAIL BUGFIX_957916 ADD_S //[BUGFIX]-Add-BEGIN by TSNJ Zhenhua.Fan,01/12/2014,PR 854923 //if(mFolder != null && (mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION) || mFolder.isDraft())){//TS: junwei-xu 2015-1-22 EMAIL BUGFIX_881437 MOD final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount, Conversation.listOf(mCurrentConversation), isBatch, mFolder, id == R.id.move_to); if (!(mAccount.getAccountId().equalsIgnoreCase("Account Id"))) { if (dialog != null) { //TS: xinlei.sheng 2016-3-3 EMAIL BUGFIX_1723314 MOD_S try { dialog.show(mActivity.getFragmentManager(), null); } catch (IllegalStateException e) { LogUtils.e(LOG_TAG, "onOptionsItemSelected: IllegalStateException when show FolderSelectionDialog"); } //TS: xinlei.sheng 2016-3-3 EMAIL BUGFIX_1723314 MOD_E } } // } //TS: junwei-xu 2015-4-2 EMAIL BUGFIX_957916 ADD_E //[BUGFIX]-Add-END by TSNJ Zhenhua.Fan } else if (id == R.id.move_to_inbox) { new AsyncTask<Void, Void, Folder>() { @Override protected Folder doInBackground(final Void... params) { // Get the "move to" inbox return Utils.getFolder(mContext, mAccount.settings.moveToInbox, true /* allowHidden */); } @Override protected void onPostExecute(final Folder moveToInbox) { final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1); // Add inbox ops.add(new FolderOperation(moveToInbox, true)); assignFolder(ops, Conversation.listOf(mCurrentConversation), true, true /* showUndo */, false /* isMoveTo */); } }.execute((Void[]) null); } else if (id == R.id.empty_trash) { showEmptyDialog(); } else if (id == R.id.empty_spam) { showEmptyDialog(); } else if (id == R.id.sort_mail) { //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD_S showSortDialog(); } //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD_E else { handled = false; } return handled; } /** * Opens an {@link EmptyFolderDialogFragment} for the current folder. */ private void showEmptyDialog() { if (mFolder != null) { final EmptyFolderDialogFragment fragment = EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type); fragment.setListener(this); fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG); } } private void showSortDialog() { DialogFragment fragment = SortChooseDialog.newInstance(SortHelper.getCurrentSort()); fragment.show(mActivity.getFragmentManager(), SortChooseDialog.TAG); } @Override public void onFolderEmptied() { emptyFolder(); } /** * Performs the work of emptying the currently visible folder. */ private void emptyFolder() { if (mConversationListCursor != null) { mConversationListCursor.emptyFolder(); } } private void attachEmptyFolderDialogFragmentListener() { final EmptyFolderDialogFragment fragment = (EmptyFolderDialogFragment) mActivity.getFragmentManager() .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG); if (fragment != null) { fragment.setListener(this); } } /** * Toggles the drawer pullout. If it was open (Fully extended), the * drawer will be closed. Otherwise, the drawer will be opened. This should * only be called when used with a toggle item. Other cases should be handled * explicitly with just closeDrawers() or openDrawer(View drawerView); */ protected void toggleDrawerState() { if (!isDrawerEnabled()) { return; } if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) { mDrawerContainer.closeDrawers(); } else { mDrawerContainer.openDrawer(mDrawerPullout); } } @Override public final boolean onUpPressed() { return handleUpPress(); } @Override public final boolean onBackPressed() { if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) { LogUtils.i(LOG_TAG, " --- mDrawerContainer.isDrawerVisible = " + mDrawerContainer.isDrawerVisible(mDrawerPullout)); // TS: kaifeng.lu 2015-11-10 EMAIL LOG-1101083 ADD mDrawerContainer.closeDrawers(); return true; } return handleBackPress(); } protected abstract boolean handleBackPress(); protected abstract boolean handleUpPress(); @Override public void updateConversation(Collection<Conversation> target, ContentValues values) { //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_S if (mConversationListCursor == null) { LogUtils.d(LOG_TAG, "updateConversation: mConversationListCursor = null"); return; } //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_E mConversationListCursor.updateValues(target, values); refreshConversationList(); } @Override public void updateConversation(Collection<Conversation> target, String columnName, boolean value) { //TS: xiaolin.li 2014-12-20 EMAIL BUGFIX-879393 ADD_S if (mConversationListCursor == null) { LogUtils.d(LOG_TAG, "updateConversation: mConversationListCursor = null"); return; } //TS: xiaolin.li 2014-12-20 EMAIL BUGFIX-879393 ADD_E mConversationListCursor.updateBoolean(target, columnName, value); refreshConversationList(); } @Override public void updateConversation(Collection<Conversation> target, String columnName, int value) { //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_S if (mConversationListCursor == null) { LogUtils.d(LOG_TAG, "updateConversation: mConversationListCursor = null"); return; } //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_E mConversationListCursor.updateInt(target, columnName, value); refreshConversationList(); } @Override public void updateConversation(Collection<Conversation> target, String columnName, String value) { //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_S if (mConversationListCursor == null) { LogUtils.d(LOG_TAG, "updateConversation: mConversationListCursor = null"); return; } //TS: xiaolin.li 2014-12-30 EMAIL BUGFIX-887553 ADD_E mConversationListCursor.updateString(target, columnName, value); refreshConversationList(); } @Override public void markConversationMessagesUnread(final Conversation conv, final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) { LogUtils.i(LOG_TAG, "markConversationMessagesUnread begin convId=" + conv.id + " read=" + conv.read); // TS: zheng.zou 2015-1-18 EMAIL BUGFIX_1130225 ADD // The only caller of this method is the conversation view, from where marking unread should // *always* take you back to list mode. showConversation(null); // locally mark conversation unread (the provider is supposed to propagate message unread // to conversation unread) conv.read = false; if (mConversationListCursor == null) { LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id); mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() { @Override public void onLoadFinished() { doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo); } }); } else { LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id); doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo); } } private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris, byte[] originalConversationInfo) { // Only do a granular 'mark unread' if a subset of messages are unread final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size(); final int numMessages = conv.getNumMessages(); final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0 && unreadCount < numMessages); LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)" + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b", conv, numMessages, unreadCount, subsetIsUnread); if (!subsetIsUnread) { // Conversations are neither marked read, nor viewed, and we don't want to show // the next conversation. LogUtils.d(LOG_TAG, ". . doing full mark unread"); markConversationsRead(Collections.singletonList(conv), false, false, false); } else { if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo); LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s", info); } mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0); // Locally update conversation's conversationInfo to revert to original version if (originalConversationInfo != null) { mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.CONVERSATION_INFO, originalConversationInfo); } // applyBatch with each CPO as an UPDATE op on each affected message uri final ArrayList<ContentProviderOperation> ops = Lists.newArrayList(); String authority = null; for (Uri messageUri : unreadMessageUris) { if (authority == null) { authority = messageUri.getAuthority(); } ops.add(ContentProviderOperation.newUpdate(messageUri).withValue(UIProvider.MessageColumns.READ, 0) .build()); LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri); } LogUtils.d(LOG_TAG, ". . operations = %s", ops); new ContentProviderTask() { @Override protected void onPostExecute(Result result) { if (result.exception != null) { LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR."); } else { LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s", Arrays.toString(result.results)); } } }.run(mResolver, authority, ops); } } @Override public void markConversationsRead(final Collection<Conversation> targets, final boolean read, final boolean viewed) { //TS: Gantao 2016-01-14 EMAIL BUGFIX-1424288 ADD_S try { LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray()); } catch (MissingFormatArgumentException e) { LogUtils.e(LOG_TAG, "Miss format argument while output the log of markConversationsRead"); } //TS: Gantao 2016-01-14 EMAIL BUGFIX-1424288 ADD_E if (mConversationListCursor == null) { if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring", targets.toArray()); } mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() { @Override public void onLoadFinished() { markConversationsRead(targets, read, viewed, true); } }); } else { // We want to show the next conversation if we are marking unread. markConversationsRead(targets, read, viewed, true); } } private void markConversationsRead(final Collection<Conversation> targets, final boolean read, final boolean markViewed, final boolean showNext) { LogUtils.d(LOG_TAG, "performing markConversationsRead"); // Auto-advance if requested and the current conversation is being marked unread if (showNext && !read) { final Runnable operation = new Runnable() { @Override public void run() { markConversationsRead(targets, read, markViewed, showNext); } }; if (!showNextConversation(targets, operation)) { // This method will be called again if the user selects an autoadvance option return; } } final int size = targets.size(); final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size); for (final Conversation target : targets) { final ContentValues value = new ContentValues(4); value.put(ConversationColumns.READ, read); // We never want to mark unseen here, but we do want to mark it seen if (read || markViewed) { value.put(ConversationColumns.SEEN, Boolean.TRUE); } // The mark read/unread/viewed operations do not show an undo bar value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true); if (markViewed) { value.put(ConversationColumns.VIEWED, true); } final ConversationInfo info = target.conversationInfo; final boolean changed = info.markRead(read); if (changed) { value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob()); } opList.add(mConversationListCursor.getOperationForConversation(target, ConversationOperation.UPDATE, value)); // Update the local conversation objects so they immediately change state. target.read = read; if (markViewed) { target.markViewed(); } } mConversationListCursor.updateBulkValues(opList); } /** * Auto-advance to a different conversation if the currently visible conversation in * conversation mode is affected (deleted, marked unread, etc.). * * <p>Does nothing if outside of conversation mode.</p> * * @param target the set of conversations being deleted/marked unread */ @Override public void showNextConversation(final Collection<Conversation> target) { showNextConversation(target, null); } /** * Helper function to determine if the provided set of conversations is in view * @param target set of conversations that we are interested in * @return true if they are in view, false otherwise */ private boolean isCurrentConversationInView(final Collection<Conversation> target) { final int viewMode = mViewMode.getMode(); return (viewMode == ViewMode.CONVERSATION || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION) && Conversation.contains(target, mCurrentConversation); } /** * Auto-advance to a different conversation if the currently visible conversation in * conversation mode is affected (deleted, marked unread, etc.). * * <p>Does nothing if outside of conversation mode.</p> * <p> * Clients may pass an operation to execute on the target that this method will run after * auto-advance is complete. The operation, if provided, may run immediately, or it may run * later, or not at all. Reasons it may run later include: * <ul> * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li> * <li>auto-advance in this configuration requires a mode change, and we need to wait for the * mode change transition to finish</li> * </ul> * <p>If the current conversation is not in the target collection, this method will do nothing, * and will not execute the operation. * * @param target the set of conversations being deleted/marked unread * @param operation (optional) the operation to execute after advancing * @return <code>false</code> if this method handled or will execute the operation, * <code>true</code> otherwise. */ private boolean showNextConversation(final Collection<Conversation> target, final Runnable operation) { if (isCurrentConversationInView(target)) { final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting(); // If we don't have one set, but we're here, just take the default final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ? AutoAdvance.DEFAULT : autoAdvanceSetting; final Conversation next = mTracker.getNextConversation(autoAdvance, target); LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next); // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the // transition doesn't run (i.e. it "completes" immediately). mAutoAdvanceOp = operation; showConversation(next); return (mAutoAdvanceOp == null); } return true; } @Override public void starMessage(ConversationMessage msg, boolean starred) { if (msg.starred == starred) { return; } msg.starred = starred; // locally propagate the change to the owning conversation // (figure the provider will properly propagate the change when it commits it) // // when unstarring, only propagate the change if this was the only message starred final boolean conversationStarred = starred || msg.isConversationStarred(); final Conversation conv = msg.getConversation(); if (conversationStarred != conv.starred) { conv.starred = conversationStarred; mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.STARRED, conversationStarred); } final ContentValues values = new ContentValues(1); values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0); new ContentProviderTask.UpdateTask() { @Override protected void onPostExecute(Result result) { // TODO: handle errors? } }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */); } @Override public void requestFolderRefresh() { if (mFolder == null) { return; } final ConversationListFragment convList = getConversationListFragment(); if (convList == null) { // This could happen if this account is in initial sync (user // is seeing the "your mail will appear shortly" message) return; } convList.showSyncStatusBar(); if (mAsyncRefreshTask != null) { mAsyncRefreshTask.cancel(true); } mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri); mAsyncRefreshTask.execute(); } /** * Confirm (based on user's settings) and delete a conversation from the conversation list and * from the database. * @param actionId the ID of the menu item that caused the delete: R.id.delete, R.id.archive... * @param target the conversations to act upon * @param showDialog true if a confirmation dialog is to be shown, false otherwise. * @param confirmResource the resource ID of the string that is shown in the confirmation dialog */ private void confirmAndDelete(int actionId, final Collection<Conversation> target, boolean showDialog, int confirmResource, UndoCallback undoCallback) { final boolean isBatch = false; if (showDialog) { makeDialogListener(actionId, isBatch, undoCallback); final CharSequence message = Utils.formatPlural(mContext, confirmResource, target.size()); final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message); c.displayDialog(mActivity.getFragmentManager()); } else { delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch); } } @Override public void delete(final int actionId, final Collection<Conversation> target, final DestructiveAction action, final boolean isBatch) { // Order of events is critical! The Conversation View Fragment must be // notified of the next conversation with showConversation(next) *before* the // conversation list // fragment has a chance to delete the conversation, animating it away. // Update the conversation fragment if the current conversation is // deleted. final Runnable operation = new Runnable() { @Override public void run() { delete(actionId, target, action, isBatch); } }; if (!showNextConversation(target, operation)) { // This method will be called again if the user selects an autoadvance option return; } // If the conversation is in the selected set, remove it from the set. // Batch selections are cleared in the end of the action, so not done for batch actions. if (!isBatch) { for (final Conversation conv : target) { if (mSelectedSet.contains(conv)) { mSelectedSet.toggle(conv); } } } // The conversation list deletes and performs the action if it exists. final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null) { LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); convListFragment.requestDelete(actionId, target, action); return; } // No visible UI element handled it on our behalf. Perform the action // ourself. LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves"); action.performAction(); } /** * Requests that the action be performed and the UI state is updated to reflect the new change. * @param action the action to be performed, specified as a menu id: R.id.archive, ... */ private void requestUpdate(final DestructiveAction action) { action.performAction(); refreshConversationList(); } @Override public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { // TODO(viki): Auto-generated method stub } @Override public boolean onPrepareOptionsMenu(Menu menu) { return mActionBarController.onPrepareOptionsMenu(menu); } @Override public void onPause() { mHaveAccountList = false; enableNotifications(); } @Override public void onResume() { // Register the receiver that will prevent the status receiver from // displaying its notification icon as long as we're running. // The SupressNotificationReceiver will block the broadcast if we're looking at the folder // that the notification was received for. disableNotifications(); mSafeToModifyFragments = true; attachEmptyFolderDialogFragmentListener(); // Invalidating the options menu so that when we make changes in settings, // the changes will always be updated in the action bar/options menu/ mActivity.invalidateOptionsMenu(); } @Override public void onSaveInstanceState(Bundle outState) { /// TCT: save global search tag, for it may be lost after rotated devices outState.putBoolean(SAVED_GLOBAL_SEARCH, mGlobalSearch); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S // save the check status for star toggle outState.putBoolean(BUNDLE_CHECK_STATUS_KEY, mCheckStatus); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E mViewMode.handleSaveInstanceState(outState); if (mAccount != null) { outState.putParcelable(SAVED_ACCOUNT, mAccount); } if (mFolder != null) { outState.putParcelable(SAVED_FOLDER, mFolder); } // TCT: If this is a search activity, let's store the search context if (mConvListContext != null) { // outState.putString(SAVED_QUERY, mConvListContext.searchQuery); outState.putParcelable(SAVED_LOCAL_SEARCH, mConvListContext.toBundle()); } if (mCurrentConversation != null && mViewMode.isConversationMode()) { outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); } if (!mSelectedSet.isEmpty()) { outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet); } if (mToastBar.getVisibility() == View.VISIBLE) { outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation()); //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_S if (mUndoConversation != null) { outState.putParcelable(SAVED_UNDO_CONVERSATION, mUndoConversation); } if (mUndoAction != -1) { outState.putInt(SAVED_UNDO_ACTION, mUndoAction); } //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_E // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_S if (mSavedDraftMsgId != -1) { outState.putLong(SAVED_DRAFT_MSG_ID, mSavedDraftMsgId); } // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_E } final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null) { convListFragment.getAnimatedAdapter().onSaveInstanceState(outState); } // If there is a dialog being shown, save the state so we can create a listener for it. if (mDialogAction != -1) { outState.putInt(SAVED_ACTION, mDialogAction); outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet); } if (mDetachedConvUri != null) { outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri); } outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder); mSafeToModifyFragments = false; outState.putParcelable(SAVED_INBOX_KEY, mInbox); outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS, mConversationListScrollPositions); } /** * @see #mSafeToModifyFragments */ protected boolean safeToModifyFragments() { return mSafeToModifyFragments; } /** * TCT: implement local search method @{ */ @Override public void enterLocalSearch(String searchfield) { if (mConvListContext == null || TextUtils.isEmpty(searchfield)) { LogUtils.logFeature(LogTag.SEARCH_TAG, "enterLocalSearch failed for search field or mConvListContext is null, searchfie ld [%s]", searchfield); return; } LogUtils.logFeature(LogTag.SEARCH_TAG, " >>>> enterLocalSearch searchfield [%s] ", searchfield); mConvListContext.setLocalSearch(true); mConvListContext.setSearchField(searchfield); if (isDrawerEnabled()) { mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } } @Override public void exitLocalSearch() { if (mConvListContext == null) { return; } LogUtils.logFeature(LogTag.SEARCH_TAG, " <<<< exitLocalSearch"); mConvListContext.setLocalSearch(false); mConvListContext.setSearchQueryText(null); mConvListContext.setSearchField(null); // refresh conversation list. loadConversationListData(false); if (isDrawerEnabled()) { mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); } } /** * Note: if {@link Query} is null, default refresh current mailbox. */ @Override public void executeLocalSearch(String query) { if (mConvListContext == null || !mConvListContext.isLocalSearch()) { LogUtils.logFeature(LogTag.SEARCH_TAG, "enterLocalSearch failed for mConvListContext is null or current is not local search, mConvListContext: [%s]", mConvListContext); return; } LogUtils.logFeature(LogTag.SEARCH_TAG, "executeLocalSearch query [%s]", query); mConvListContext.setSearchQueryText(query); // Clear global search tag, since it has worked. if (mGlobalSearch) { mGlobalSearch = false; } loadConversationListData(false); } /** @} */ /** * TCT: This will launch a new activity with a new controller to do remote search. */ @Override public void executeSearch(String query) { AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST); /// TCT: add search field for remote search. @{ String searchFiled = null; if (mConvListContext != null) { searchFiled = mConvListContext.getSearchField(); } if (TextUtils.isEmpty(searchFiled)) { LogUtils.d(LOG_TAG, "set search field as ALL, if user not set search field"); searchFiled = SearchParams.SEARCH_FIELD_ALL; } /// @} Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEARCH); intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); /// TCT: add search field for remote search. @{ intent.putExtra(SearchParams.BUNDLE_QUERY_FIELD, searchFiled); /// @} intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); intent.setComponent(mActivity.getComponentName()); /// TCT: It's better to stay at local search UI when back from remote search. // mActionBarController.collapseSearch(); // Call startActivityForResult here so we can tell if we have navigated to a different folder // or account from search results. mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE); } //[FEATURE]-Add-BEGIN by TSCD.zhonghua.tuo,05/28/2014,FR 670064 // @Override // public void executeLocalSearch(String query) { // if(getConversationListFragment() != null) { // getConversationListFragment().reFreshLocalSearch(query); // } // } //[FEATURE]-Add-END by TSCD.zhonghua.tuo /** * TCT: This will retry remote search right in this remote context(activity/controller). */ private void retryRemoteSearch() { LogUtils.d(LOG_TAG, "AAC. retry remote search."); Intent intent = mActivity.getIntent(); if (null != intent) { fetchSearchFolder(intent); } else { LogUtils.d(LOG_TAG, "AAC. retry remote search failed!"); } } @Override public void onStop() { NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver); mActivity.unregisterReceiver(mDraftReceiver); //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD } @Override public void onDestroy() { // stop listening to the cursor on e.g. configuration changes if (mConversationListCursor != null) { mConversationListCursor.removeListener(this); } mDrawIdler.setListener(null); mDrawIdler.setRootView(null); // unregister the ViewPager's observer on the conversation cursor mPagerController.onDestroy(); mActionBarController.onDestroy(); mRecentFolderList.destroy(); mDestroyed = true; mHandler.removeCallbacks(mLogServiceChecker); mLogServiceChecker = null; } /** * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button * or not. The individual controller is responsible for changing the icon based on the mode. */ protected abstract void resetActionBarIcon(); /** * {@inheritDoc} Subclasses must override this to listen to mode changes * from the ViewMode. Subclasses <b>must</b> call the parent's * onViewModeChanged since the parent will handle common state changes. */ @Override public void onViewModeChanged(int newMode) { // The floating action compose button is only visible in the conversation/search lists final int composeVisible = ViewMode.isListMode(newMode) ? View.VISIBLE : View.GONE; mFloatingComposeButton.setVisibility(composeVisible); //TODO for debug, will be deleted later LogUtils.i(LOG_TAG, "update float compose button visibility as " + composeVisible); // When we step away from the conversation mode, we don't have a current conversation // anymore. Let's blank it out so clients calling getCurrentConversation are not misled. if (!ViewMode.isConversationMode(newMode)) { setCurrentConversation(null); } // If the viewmode is not set, preserve existing icon. if (newMode != ViewMode.UNKNOWN) { resetActionBarIcon(); } if (isDrawerEnabled()) { /** If the folder doesn't exist, or its parent URI is empty, * this is not a child folder */ final boolean isTopLevel = Folder.isRoot(mFolder); mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(newMode, isTopLevel)); mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); closeDrawerIfOpen(); } } /** * Returns true if the drawer icon is shown * @param viewMode the current view mode * @param isTopLevel true if the current folder is not a child * @return whether the drawer indicator is shown */ private boolean getShouldShowDrawerIndicator(final int viewMode, final boolean isTopLevel) { // If search list/conv mode: disable indicator // Indicator is enabled either in conversation list or folder list mode. return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode) && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel); } public void disablePagerUpdates() { mPagerController.stopListening(); } public boolean isDestroyed() { return mDestroyed; } @Override public void commitDestructiveActions(boolean animate) { ConversationListFragment fragment = getConversationListFragment(); if (fragment != null) { fragment.commitDestructiveActions(animate); } } @Override public void onWindowFocusChanged(boolean hasFocus) { final ConversationListFragment convList = getConversationListFragment(); // hasFocus already ensures that the window is in focus, so we don't need to call // AAC.isFragmentVisible(convList) here. if (hasFocus && convList != null && convList.isVisible()) { // The conversation list is visible. informCursorVisiblity(true); } } /** * Set the account, and carry out all the account-related changes that rely on this. * @param account new account to set to. */ private void setAccount(Account account) { if (account == null) { LogUtils.w(LOG_TAG, new Error(), "AAC ignoring null (presumably invalid) account restoration"); return; } LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri); mAccount = account; // Only change AAC state here. Do *not* modify any other object's state. The object // should listen on account changes. restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY); mActivity.invalidateOptionsMenu(); disableNotificationsOnAccountChange(mAccount); restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY); // The Mail instance can be null during test runs. final MailAppProvider instance = MailAppProvider.getInstance(); if (instance != null) { instance.setLastViewedAccount(mAccount.uri.toString()); } if (account.settings == null) { LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings."); return; } mAccountObservers.notifyChanged(); perhapsEnterWaitMode(); } /** * Restore the state from the previous bundle. Subclasses should call this * method from the parent class, since it performs important UI * initialization. * * @param savedState previous state */ @Override public void onRestoreInstanceState(Bundle savedState) { mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI); if (savedState.containsKey(SAVED_CONVERSATION)) { // Open the conversation. final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION); if (conversation != null && conversation.position < 0) { // Set the position to 0 on this conversation, as we don't know where it is // in the list conversation.position = 0; } showConversation(conversation); } if (savedState.containsKey(SAVED_TOAST_BAR_OP)) { ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP); if (op != null) { //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_S if (mConversationListCursor != null && mConversationListCursor.getUndoCallback() != null) { mUndoConversation = savedState.getParcelable(SAVED_UNDO_CONVERSATION); mUndoAction = savedState.getInt(SAVED_UNDO_ACTION); final UndoCallback undoCallback = new UndoCallback() { @Override public void performUndoCallback() { showConversation(mUndoConversation); } }; mConversationListCursor.updateCallback(undoCallback); } //TS: qing.liang 2015-03-10 EMAIL BUGFIX_-941849 ADD_E if (op.getType() == ToastBarOperation.UNDO) { onUndoAvailable(op); } else if (op.getType() == ToastBarOperation.ERROR) { onError(mFolder, true); } else if (op.getType() == ToastBarOperation.DISCARD) { // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_S mSavedDraftMsgId = savedState.getLong(SAVED_DRAFT_MSG_ID); showDiscardDraftToast(mSavedDraftMsgId); } else if (op.getType() == ToastBarOperation.DISCARDED) { showDiscardedToast(); } // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_E } } mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER); final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null) { convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState); } /* * Restore the state of selected conversations. This needs to be done after the correct mode * is set and the action bar is fully initialized. If not, several key pieces of state * information will be missing, and the split views may not be initialized correctly. */ restoreSelectedConversations(savedState); // Order is important!!! // The dialog listener needs to happen *after* the selected set is restored. // If there has been an orientation change, and we need to recreate the listener for the // confirm dialog fragment (delete/archive/...), then do it here. if (mDialogAction != -1) { makeDialogListener(mDialogAction, mDialogFromSelectedSet, getUndoCallbackForDestructiveActionsWithAutoAdvance(mDialogAction, mCurrentConversation)); } mInbox = savedState.getParcelable(SAVED_INBOX_KEY); mConversationListScrollPositions.clear(); mConversationListScrollPositions.putAll(savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS)); } /** * Handle an intent to open the app. This method is called only when there is no saved state, * so we need to set state that wasn't set before. It is correct to change the viewmode here * since it has not been previously set. * * This method is called for a subset of the reasons mentioned in * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from * notifications, widgets, and shortcuts. * @param intent intent passed to the activity. */ private void handleIntent(Intent intent) { LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction()); if (Intent.ACTION_VIEW.equals(intent.getAction())) { if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT))); } if (mAccount == null) { return; } final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION); if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) { Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE, AnalyticsUtils.getAccountTypeForAccount(mAccount.getEmailAddress())); Analytics.getInstance().sendEvent("notification_click", isConversationMode ? "conversation" : "conversation_list", null, 0); } if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) { mViewMode.enterConversationMode(); } else { mViewMode.enterConversationListMode(); } // Put the folder and conversation, and ask the loader to create this folder. final Bundle args = new Bundle(); final Uri folderUri; if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) { folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI); } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) { final Folder folder = Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)); folderUri = folder.folderUri.fullUri; //TS: junwei-xu 2014-1-5 EMAIL BUGFIX_879468 ADD_S } else if (intent.getData() != null) { folderUri = intent.getData(); //TS: junwei-xu 2014-1-5 EMAIL BUGFIX_879468 ADD_E } else { final Bundle extras = intent.getExtras(); LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s", extras == null ? "null" : extras.toString()); folderUri = mAccount.settings.defaultInbox; } // Check if we should load all conversations instead of using // the default behavior which loads an initial subset. mIgnoreInitialConversationLimit = intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false); args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri); args.putParcelable(Utils.EXTRA_CONVERSATION, intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args); } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { mHaveSearchResults = false; //TS: zheng.zou 2015-03-18 EMAIL BUGFIX_744708 ADD_S mToastBar.hide(false, false); //TS: zheng.zou 2015-03-18 EMAIL BUGFIX_744708 ADD_E // Save this search query for future suggestions. final String query = intent.getStringExtra(SearchManager.QUERY); final String authority = mContext.getString(R.string.suggestions_authority); final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(mContext, authority, SuggestionsProvider.MODE); suggestions.saveRecentQuery(query, null); setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); fetchSearchFolder(intent); if (shouldEnterSearchConvMode()) { mViewMode.enterSearchResultsConversationMode(); } else { mViewMode.enterSearchResultsListMode(); } /** TCT: init the list context for remote search, except folder. @{ */ // use a UNINITIALIZED folder temporarily. need update when finish search load. Folder folder = Folder.newUnsafeInstance(); mConvListContext = ConversationListContext.forSearchQuery(mAccount, folder, query); mConvListContext .setSearchField(mActivity.getIntent().getStringExtra(SearchParams.BUNDLE_QUERY_FIELD)); /** @} */ } else { /** TCT: The action is search but no extra account means it's a global search. @{ */ mGlobalSearch = true; LogUtils.logFeature(LogTag.SEARCH_TAG, "Handle ACTION_SEARCH , is mGlobalSearch [%s]", mGlobalSearch); // reload conbined inbox folder if needed. if (mAccount != null && (mFolder == null || !mFolder.isInitialized())) { Bundle args = new Bundle(); LogUtils.logFeature(LogTag.SEARCH_TAG, " GlobalSearch but without Folder, reload inbox again."); args.putParcelable(Utils.EXTRA_FOLDER_URI, mAccount.settings.defaultInbox); restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args); } /** @} */ } } if (mAccount != null) { restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY); } } /** * Returns true if we should enter conversation mode with search. */ protected final boolean shouldEnterSearchConvMode() { return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext()); } /** * Copy any selected conversations stored in the saved bundle into our selection set, * triggering {@link ConversationSetObserver} callbacks as our selection set changes. * */ private void restoreSelectedConversations(Bundle savedState) { if (savedState == null) { mSelectedSet.clear(); return; } final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET); if (selectedSet == null || selectedSet.isEmpty()) { mSelectedSet.clear(); return; } // putAll will take care of calling our registered onSetPopulated method mSelectedSet.putAll(selectedSet); } /** * Show the conversation provided in the arguments. It is safe to pass a null conversation * object, which is a signal to back out of conversation view mode. * Child classes must call super.showConversation() <b>before</b> their own implementations. * @param conversation the conversation to be shown, or null if we want to back out to list * mode. * onLoadFinished(Loader, Cursor) on any callback. */ protected void showConversation(Conversation conversation) { showConversation(conversation, true /* markAsRead */); } protected void showConversation(Conversation conversation, boolean markAsRead) { if (conversation != null) { Utils.sConvLoadTimer.start(); } MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation); // Set the current conversation just in case it wasn't already set. setCurrentConversation(conversation); } /** * Children can override this method, but they must call super.showWaitForInitialization(). * {@inheritDoc} */ @Override public void showWaitForInitialization() { mViewMode.enterWaitingForInitializationMode(); mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */); } private void updateWaitMode() { final FragmentManager manager = mActivity.getFragmentManager(); final WaitFragment waitFragment = (WaitFragment) manager.findFragmentByTag(TAG_WAIT); if (waitFragment != null) { waitFragment.updateAccount(mAccount); } } /** * Remove the "Waiting for Initialization" fragment. Child classes are free to override this * method, though they must call the parent implementation <b>after</b> they do anything. */ protected void hideWaitForInitialization() { mWaitFragment = null; } /** * Use the instance variable and the wait fragment's tag to get the wait fragment. This is * far superior to using the value of mWaitFragment, which might be invalid or might refer * to a fragment after it has been destroyed. * @return a wait fragment that is already attached to the activity, if one exists */ protected final WaitFragment getWaitFragment() { final FragmentManager manager = mActivity.getFragmentManager(); final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT); if (waitFrag != null) { // The Fragment Manager knows better, so use its instance. mWaitFragment = waitFrag; } return mWaitFragment; } /** * Returns true if we are waiting for the account to sync, and cannot show any folders or * conversation for the current account yet. */ private boolean inWaitMode() { final WaitFragment waitFragment = getWaitFragment(); if (waitFragment != null) { final Account fragmentAccount = waitFragment.getAccount(); return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) && mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION; } return false; } /** * Children can override this method, but they must call super.showConversationList(). * {@inheritDoc} */ @Override public void showConversationList(ConversationListContext listContext) { } @Override public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { convListFragment.getAnimatedAdapter().onConversationSelected(); } // Only animate destructive actions if we are going to be showing the // conversation list when we show the next conversation. commitDestructiveActions(mIsTablet); //TS: yanhua.chen 2015-4-24 EMAIL BUGFIX_976622 MOD_S if (conversation != null) { showConversation(conversation); } //TS: yanhua.chen 2015-4-24 EMAIL BUGFIX_976622 MOD_E } @Override public final void onCabModeEntered() { final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { convListFragment.getAnimatedAdapter().onCabModeEntered(); } } @Override public final void onCabModeExited() { final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { convListFragment.getAnimatedAdapter().onCabModeExited(); } } @Override public Conversation getCurrentConversation() { return mCurrentConversation; } /** * Set the current conversation. This is the conversation on which all actions are performed. * Do not modify mCurrentConversation except through this method, which makes it easy to * perform common actions associated with changing the current conversation. * @param conversation new conversation to view. Passing null indicates that we are backing * out to conversation list mode. */ @Override public void setCurrentConversation(Conversation conversation) { // The controller should come out of detached mode if a new conversation is viewed, or if // we are going back to conversation list mode. if (mDetachedConvUri != null && (conversation == null || !mDetachedConvUri.equals(conversation.uri))) { clearDetachedMode(); } // Must happen *before* setting mCurrentConversation because this sets // conversation.position if a cursor is available. mTracker.initialize(conversation); mCurrentConversation = conversation; if (mCurrentConversation != null) { LogUtils.i(LOG_TAG, "in AAC.setCurrentConversation() conversation [ %s, %s ]", mCurrentConversation.id, mCurrentConversation.starred); mActionBarController.setCurrentConversation(mCurrentConversation); mActivity.invalidateOptionsMenu(); } } /** * {@link LoaderManager} currently has a bug in * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around * this bug by destroying any loaders that may have been created as null (essentially because * they are optional loads, and may not apply to a particular account). * <p> * A simple null check before restarting a loader will not work, because that would not * give the controller a chance to invalidate UI corresponding the prior loader result. * * @param id loader ID to safely restart * @param handler the LoaderCallback which will handle this loader ID. * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no * arguments need to be specified. */ private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) { final LoaderManager lm = mActivity.getLoaderManager(); lm.destroyLoader(id); lm.restartLoader(id, args, handler); } @Override public void registerConversationListObserver(DataSetObserver observer) { mConversationListObservable.registerObserver(observer); } @Override public void unregisterConversationListObserver(DataSetObserver observer) { try { mConversationListObservable.unregisterObserver(observer); } catch (IllegalStateException e) { // Log instead of crash LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that " + "hasn't been registered"); } } @Override public void registerFolderObserver(DataSetObserver observer) { mFolderObservable.registerObserver(observer); } @Override public void unregisterFolderObserver(DataSetObserver observer) { try { mFolderObservable.unregisterObserver(observer); } catch (IllegalStateException e) { // Log instead of crash LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that " + "hasn't been registered"); } } @Override public void registerConversationLoadedObserver(DataSetObserver observer) { mPagerController.registerConversationLoadedObserver(observer); } @Override public void unregisterConversationLoadedObserver(DataSetObserver observer) { try { mPagerController.unregisterConversationLoadedObserver(observer); } catch (IllegalStateException e) { // Log instead of crash LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer " + "that hasn't been registered"); } } /** * Returns true if the number of accounts is different, or if the current account has * changed. This method is meant to filter frequent changes to the list of * accounts, and only return true if the new list is substantially different from the existing * list. Returning true is safe here, it leads to more work in creating the * same account list again. * @param accountCursor the cursor which points to all the accounts. * @return true if the number of accounts is changed or current account missing from the list. */ private boolean accountsUpdated(ObjectCursor<Account> accountCursor) { // Check to see if the current account hasn't been set, or the account cursor is empty if (mAccount == null || !accountCursor.moveToFirst()) { return true; } // Check to see if the number of accounts are different, from the number we saw on the last // updated if (mCurrentAccountUris.size() != accountCursor.getCount()) { return true; } // Check to see if the account list is different or if the current account is not found in // the cursor. boolean foundCurrentAccount = false; do { final Account account = accountCursor.getModel(); if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) { if (mAccount.settingsDiffer(account)) { // Settings changed, and we don't need to look any further. return true; } foundCurrentAccount = true; } // Is there a new account that we do not know about? if (!mCurrentAccountUris.contains(account.uri)) { return true; } } while (accountCursor.moveToNext()); // As long as we found the current account, the list hasn't been updated return !foundCurrentAccount; } /** * Updates accounts for the app. If the current account is missing, the first * account in the list is set to the current account (we <em>have</em> to choose something). * * @param accounts cursor into the AccountCache * @return true if the update was successful, false otherwise */ private boolean updateAccounts(ObjectCursor<Account> accounts) { if (accounts == null || !accounts.moveToFirst()) { return false; } final Account[] allAccounts = Account.getAllAccounts(accounts); // A match for the current account's URI in the list of accounts. Account currentFromList = null; // Save the uris for the accounts and find the current account in the updated cursor. mCurrentAccountUris.clear(); for (final Account account : allAccounts) { LogUtils.d(LOG_TAG, "updateAccounts(%s)", account); mCurrentAccountUris.add(account.uri); if (mAccount != null && account.uri.equals(mAccount.uri)) { currentFromList = account; } } // 1. current account is already set and is in allAccounts: // 1a. It has changed -> load the updated account. // 2b. It is unchanged -> no-op // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) // 3. saved preference has an account -> pick that one // 4. otherwise just pick first boolean accountChanged = false; /// Assume case 4, initialize to first account, and see if we can find anything better. Account newAccount = allAccounts[0]; if (currentFromList != null) { // Case 1: Current account exists but has changed if (!currentFromList.equals(mAccount)) { newAccount = currentFromList; accountChanged = true; } // Case 1b: else, current account is unchanged: nothing to do. } else { // Case 2: Current account is not in allAccounts, the account needs to change. accountChanged = true; if (mAccount == null) { // Case 3: Check for last viewed account, and check if it exists in the list. final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount(); if (lastAccountUri != null) { for (final Account account : allAccounts) { if (lastAccountUri.equals(account.uri.toString())) { newAccount = account; break; } } } } } if (accountChanged) { /// tct: If this is global search, take combined account as the new account. // This lead to enter combined inbox. if (mGlobalSearch) { LogUtils.logFeature(LogTag.SEARCH_TAG, "[Global Search]Change account to combined account"); newAccount = allAccounts[allAccounts.length - 1]; } changeAccount(newAccount); } // Whether we have updated the current account or not, we need to update the list of // accounts in the ActionBar. mAllAccounts = allAccounts; mAllAccountObservers.notifyChanged(); return (allAccounts.length > 0); } private void disableNotifications() { mNewEmailReceiver.activate(mContext, this); } private void enableNotifications() { mNewEmailReceiver.deactivate(); } private void disableNotificationsOnAccountChange(Account account) { // If the new mail suppression receiver is activated for a different account, we want to // activate it for the new account. if (mNewEmailReceiver.activated() && !mNewEmailReceiver.notificationsDisabledForAccount(account)) { // Deactivate the current receiver, otherwise multiple receivers may be registered. mNewEmailReceiver.deactivate(); mNewEmailReceiver.activate(mContext, this); } } /** * Destructive actions on Conversations. This class should only be created by controllers, and * clients should only require {@link DestructiveAction}s, not specific implementations of the. * Only the controllers should know what kind of destructive actions are being created. */ public class ConversationAction implements DestructiveAction { /** * The action to be performed. This is specified as the resource ID of the menu item * corresponding to this action: R.id.delete, R.id.report_spam, etc. */ private final int mAction; /** The action will act upon these conversations */ private final Collection<Conversation> mTarget; /** Whether this destructive action has already been performed */ private boolean mCompleted; /** Whether this is an action on the currently selected set. */ private final boolean mIsSelectedSet; private UndoCallback mCallback; /** * Create a listener object. * @param action action is one of four constants: R.id.y_button (archive), * R.id.delete , R.id.mute, and R.id.report_spam. * @param target Conversation that we want to apply the action to. * @param isBatch whether the conversations are in the currently selected batch set. */ public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) { mAction = action; mTarget = ImmutableList.copyOf(target); mIsSelectedSet = isBatch; } @Override public void setUndoCallback(UndoCallback undoCallback) { mCallback = undoCallback; } /** * The action common to child classes. This performs the action specified in the constructor * on the conversations given here. */ @Override public void performAction() { if (isPerformed()) { return; } boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO); // Are we destroying the currently shown conversation? Show the next one. if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { LogUtils.d(LOG_TAG, "ConversationAction.performAction():" + "\nmTarget=%s\nCurrent=%s", Conversation.toString(mTarget), mCurrentConversation); } if (mConversationListCursor == null) { LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():" + "\nmTarget=%s\nCurrent=%s", Conversation.toString(mTarget), mCurrentConversation); return; } if (mAction == R.id.archive) { LogUtils.d(LOG_TAG, "Archiving"); mConversationListCursor.archive(mTarget, mCallback); } else if (mAction == R.id.delete) { LogUtils.d(LOG_TAG, "Deleting"); mConversationListCursor.delete(mTarget, mCallback); if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) { undoEnabled = false; } } else if (mAction == R.id.mute) { LogUtils.d(LOG_TAG, "Muting"); if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) { for (Conversation c : mTarget) { c.localDeleteOnUpdate = true; } } mConversationListCursor.mute(mTarget, mCallback); } else if (mAction == R.id.report_spam) { LogUtils.d(LOG_TAG, "Reporting spam"); mConversationListCursor.reportSpam(mTarget, mCallback); } else if (mAction == R.id.mark_not_spam) { LogUtils.d(LOG_TAG, "Marking not spam"); mConversationListCursor.reportNotSpam(mTarget, mCallback); } else if (mAction == R.id.report_phishing) { LogUtils.d(LOG_TAG, "Reporting phishing"); mConversationListCursor.reportPhishing(mTarget, mCallback); } else if (mAction == R.id.remove_star) { LogUtils.d(LOG_TAG, "Removing star"); // Star removal is destructive in the Starred folder. mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED, false); } else if (mAction == R.id.mark_not_important) { LogUtils.d(LOG_TAG, "Marking not-important"); // Marking not important is destructive in a mailbox // containing only important messages if (mFolder != null && mFolder.isImportantOnly()) { for (Conversation conv : mTarget) { conv.localDeleteOnUpdate = true; } } mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); } else if (mAction == R.id.discard_drafts) { LogUtils.d(LOG_TAG, "Discarding draft messages"); // Discarding draft messages is destructive in a "draft" mailbox if (mFolder != null && mFolder.isDraft()) { for (Conversation conv : mTarget) { conv.localDeleteOnUpdate = true; } } mConversationListCursor.discardDrafts(mTarget); // We don't support undoing discarding drafts undoEnabled = false; } else if (mAction == R.id.discard_outbox) { LogUtils.d(LOG_TAG, "Discarding failed messages in Outbox"); mConversationListCursor.moveFailedIntoDrafts(mTarget); undoEnabled = false; } if (undoEnabled) { mHandler.postDelayed(new Runnable() { @Override public void run() { onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction, ToastBarOperation.UNDO, mIsSelectedSet, mFolder)); } }, mShowUndoBarDelay); } refreshConversationList(); if (mIsSelectedSet) { mSelectedSet.clear(); } } /** * Returns true if this action has been performed, false otherwise. * */ private synchronized boolean isPerformed() { if (mCompleted) { return true; } mCompleted = true; return false; } } // Called from the FolderSelectionDialog after a user is done selecting folders to assign the // conversations to. @Override public final void assignFolder(Collection<FolderOperation> folderOps, Collection<Conversation> target, boolean batch, boolean showUndo, final boolean isMoveTo) { // Actions are destructive only when the current folder can be un-assigned from and // when the list of folders contains the current folder. final boolean isDestructive = mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION) && FolderOperation.isDestructive(folderOps, mFolder); LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive); if (isDestructive) { for (final Conversation c : target) { c.localDeleteOnUpdate = true; } } final DestructiveAction folderChange; final UndoCallback undoCallback = isMoveTo ? getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to, mCurrentConversation) : null; // Update the UI elements depending no their visibility and availability // TODO(viki): Consolidate this into a single method requestDelete. if (isDestructive) { /* * If this is a MOVE operation, we want the action folder to be the destination folder. * Otherwise, we want it to be the current folder. * * A set of folder operations is a move if there are exactly two operations: an add and * a remove. */ final Folder actionFolder; if (folderOps.size() != 2) { actionFolder = mFolder; } else { Folder addedFolder = null; boolean hasRemove = false; for (final FolderOperation folderOperation : folderOps) { if (folderOperation.mAdd) { addedFolder = folderOperation.mFolder; } else { hasRemove = true; } } if (hasRemove && addedFolder != null) { actionFolder = addedFolder; } else { actionFolder = mFolder; } } folderChange = getDeferredFolderChange(target, folderOps, isDestructive, batch, showUndo, isMoveTo, actionFolder, undoCallback); delete(0, target, folderChange, batch); } else { folderChange = getFolderChange(target, folderOps, isDestructive, batch, showUndo, false /* isMoveTo */, mFolder, undoCallback); requestUpdate(folderChange); } } @Override public final void onRefreshRequired() { if (isAnimating() || isDragging()) { final ConversationListFragment f = getConversationListFragment(); LogUtils.w(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done. cursor=%s adapter=%s", mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null); return; } //TS: chao-zhang 2015-12-23 EMAIL FEATURE_1126514 MOD_S // Refresh the query in the background if (mConversationListCursor != null && mConversationListCursor.isRefreshRequired()) { mConversationListCursor.refresh(); } //TS: chao-zhang 2015-12-23 EMAIL FEATURE_1126514 MOD_E } @Override public void startDragMode() { mIsDragHappening = true; } @Override public void stopDragMode() { mIsDragHappening = false; if (mConversationListCursor.isRefreshReady()) { LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: try sync"); onRefreshReady(); } if (mConversationListCursor.isRefreshRequired()) { LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: refresh"); mConversationListCursor.refresh(); } } private boolean isDragging() { return mIsDragHappening; } @Override public boolean isAnimating() { boolean isAnimating = false; ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null) { isAnimating = convListFragment.isAnimating(); } return isAnimating; } /** * Called when the {@link ConversationCursor} is changed or has new data in it. * <p> * {@inheritDoc} */ @Override public final void onRefreshReady() { LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s", mFolder != null ? mFolder.id : "-1"); if (mDestroyed) { LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC"); return; } if (!isAnimating()) { // Swap cursors mConversationListCursor.sync(); } else { // (CLF guaranteed to be non-null due to check in isAnimating) LogUtils.w(LOG_TAG, "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s", mConversationListCursor, getConversationListFragment().getAnimatedAdapter()); } mTracker.onCursorUpdated(); perhapsShowFirstSearchResult(); } @Override public final void onDataSetChanged() { updateConversationListFragment(); mConversationListObservable.notifyChanged(); mSelectedSet.validateAgainstCursor(mConversationListCursor); } /** * If the Conversation List Fragment is visible, updates the fragment. */ private void updateConversationListFragment() { final ConversationListFragment convList = getConversationListFragment(); if (convList != null) { refreshConversationList(); if (isFragmentVisible(convList)) { informCursorVisiblity(true); } } } /** * This class handles throttled refresh of the conversation list */ static class RefreshTimerTask extends TimerTask { final Handler mHandler; final AbstractActivityController mController; RefreshTimerTask(AbstractActivityController controller, Handler handler) { mHandler = handler; mController = controller; } @Override public void run() { mHandler.post(new Runnable() { @Override public void run() { LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired"); mController.onRefreshRequired(); } }); } } /** * Cancel the refresh task, if it's running */ private void cancelRefreshTask() { if (mConversationListRefreshTask != null) { mConversationListRefreshTask.cancel(); mConversationListRefreshTask = null; } } @Override public void onAnimationEnd(AnimatedAdapter animatedAdapter) { if (animatedAdapter != null) { LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor, animatedAdapter); } if (mConversationListCursor == null) { LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd"); return; } if (mConversationListCursor.isRefreshReady()) { LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync"); onRefreshReady(); } if (mConversationListCursor.isRefreshRequired()) { LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh"); mConversationListCursor.refresh(); } if (mRecentsDataUpdated) { mRecentsDataUpdated = false; mRecentFolderObservers.notifyChanged(); } } @Override public void onSetEmpty() { // There are no selected conversations. Ensure that the listener and its associated actions // are blanked out. setListener(null, -1); } @Override public void onSetPopulated(ConversationSelectionSet set) { mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder); //TS: Gantao 2015-10-16 EMAIL BUGFIX-ID ADD_S mCabActionMenu.setCallback(mActionBarController); //TS: Gantao 2015-10-16 EMAIL BUGFIX-ID ADD_E if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) { enableCabMode(); } } @Override public void onSetChanged(ConversationSelectionSet set) { // Do nothing. We don't care about changes to the set. } @Override public ConversationSelectionSet getSelectedSet() { return mSelectedSet; } /** * Disable the Contextual Action Bar (CAB). The selected set is not changed. */ protected void disableCabMode() { // Commit any previous destructive actions when entering/ exiting CAB mode. commitDestructiveActions(true); if (mCabActionMenu != null) { mCabActionMenu.deactivate(); } } /** * Re-enable the CAB menu if required. The selection set is not changed. */ protected void enableCabMode() { if (mCabActionMenu != null && !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) { mCabActionMenu.activate(); } } /** * Re-enable CAB mode only if we have an active selection */ protected void maybeEnableCabMode() { if (!mSelectedSet.isEmpty()) { if (mCabActionMenu != null) { mCabActionMenu.activate(); } } } /** * Unselect conversations and exit CAB mode. */ protected final void exitCabMode() { mSelectedSet.clear(); } @Override public void startSearch() { if (mAccount == null) { // We cannot search if there is no account. Drop the request to the floor. LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account"); return; } if (mAccount.supportsSearch()) { mActionBarController.expandSearch(); } else { //[BUGFIX]-MOD by SCDTABLET.shujing.jin@tcl.com,08/05/2016,2635083 Utility.showShortToast(mActivity.getActivityContext(), R.string.search_unsupported); //Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext() // .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show(); } } @Override public void exitSearchMode() { if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) { mActivity.finish(); } } /** * Supports dragging conversations to a folder. */ @Override public boolean supportsDrag(DragEvent event, Folder folder) { return (folder != null && event != null && event.getClipDescription() != null && folder.supportsCapability(UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) && !mFolder.equals(folder)); } /** * Handles dropping conversations to a folder. */ @Override public void handleDrop(DragEvent event, final Folder folder) { if (!supportsDrag(event, folder)) { return; } if (folder.isType(UIProvider.FolderType.STARRED)) { // Moving a conversation to the starred folder adds the star and // removes the current label handleDropInStarred(folder); return; } if (mFolder.isType(UIProvider.FolderType.STARRED)) { handleDragFromStarred(folder); return; } final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>(); final Collection<Conversation> conversations = mSelectedSet.values(); // Add the drop target folder. dragDropOperations.add(new FolderOperation(folder, true)); // Remove the current folder unless the user is viewing "all". // That operation should just add the new folder. boolean isDestructive = !mFolder.isViewAll() && mFolder.supportsCapability(UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES); if (isDestructive) { dragDropOperations.add(new FolderOperation(mFolder, false)); } // Drag and drop is destructive: we remove conversations from the // current folder. final DestructiveAction action = getFolderChange(conversations, dragDropOperations, isDestructive, true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder, null /* undoCallback */); if (isDestructive) { delete(0, conversations, action, true); } else { action.performAction(); } } private void handleDragFromStarred(Folder folder) { final Collection<Conversation> conversations = mSelectedSet.values(); // The conversation list deletes and performs the action if it exists. final ConversationListFragment convListFragment = getConversationListFragment(); // There should always be a convlistfragment, or the user could not have // dragged/ dropped conversations. if (convListFragment != null) { LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); ArrayList<Uri> folderUris; ArrayList<Boolean> adds; for (Conversation target : conversations) { folderUris = new ArrayList<Uri>(); adds = new ArrayList<Boolean>(); folderUris.add(folder.folderUri.fullUri); adds.add(Boolean.TRUE); final HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target.getRawFolders()); targetFolders.put(folder.folderUri.fullUri, folder); ops.add(mConversationListCursor.getConversationFolderOperation(target, folderUris, adds, targetFolders.values())); } if (mConversationListCursor != null) { mConversationListCursor.updateBulkValues(ops); } refreshConversationList(); mSelectedSet.clear(); } } private void handleDropInStarred(Folder folder) { final Collection<Conversation> conversations = mSelectedSet.values(); // The conversation list deletes and performs the action if it exists. final ConversationListFragment convListFragment = getConversationListFragment(); // There should always be a convlistfragment, or the user could not have // dragged/ dropped conversations. if (convListFragment != null) { LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); convListFragment.requestDelete(R.id.change_folders, conversations, new DroppedInStarredAction(conversations, mFolder, folder)); } } // When dragging conversations to the starred folder, remove from the // original folder and add a star private class DroppedInStarredAction implements DestructiveAction { private final Collection<Conversation> mConversations; private final Folder mInitialFolder; private final Folder mStarred; public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder, Folder starredFolder) { mConversations = conversations; mInitialFolder = initialFolder; mStarred = starredFolder; } @Override public void setUndoCallback(UndoCallback undoCallback) { return; // currently not applicable } @Override public void performAction() { ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(), R.id.change_folders, ToastBarOperation.UNDO, true /* batch */, mInitialFolder); onUndoAvailable(undoOp); ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); ContentValues values = new ContentValues(); ArrayList<Uri> folderUris; ArrayList<Boolean> adds; ConversationOperation operation; for (Conversation target : mConversations) { folderUris = new ArrayList<Uri>(); adds = new ArrayList<Boolean>(); folderUris.add(mStarred.folderUri.fullUri); adds.add(Boolean.TRUE); folderUris.add(mInitialFolder.folderUri.fullUri); adds.add(Boolean.FALSE); final HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target.getRawFolders()); targetFolders.put(mStarred.folderUri.fullUri, mStarred); targetFolders.remove(mInitialFolder.folderUri.fullUri); values.put(ConversationColumns.STARRED, true); operation = mConversationListCursor.getConversationFolderOperation(target, folderUris, adds, targetFolders.values(), values); ops.add(operation); } if (mConversationListCursor != null) { mConversationListCursor.updateBulkValues(ops); } refreshConversationList(); mSelectedSet.clear(); } } @Override public void onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (mToastBar != null && !mToastBar.isEventInToastBar(event)) { // if the toast bar is still animating, ignore this attempt to hide it if (mToastBar.isAnimating()) { return; } // if the toast bar has not been seen long enough, ignore this attempt to hide it if (mToastBar.cannotBeHidden()) { return; } // hide the toast bar mToastBar.hide(true /* animated */, false /* actionClicked */); } } } @Override public void onConversationSeen() { mPagerController.onConversationSeen(); } @Override public boolean isInitialConversationLoading() { return mPagerController.isInitialConversationLoading(); } /** * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is * insufficient because that doesn't check if the window is currently in focus or not. */ private boolean isFragmentVisible(Fragment in) { return in != null && in.isVisible() && mActivity.hasWindowFocus(); } /** * This class handles callbacks that create a {@link ConversationCursor}. */ private class ConversationListLoaderCallbacks implements LoaderManager.LoaderCallbacks<ConversationCursor> { @Override public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY); final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY); final boolean ignoreInitialConversationLimit = args .getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false); if (account == null || folder == null) { return null; } /// TCT: for load search build a local search uri. /// default folder.conversationListUri. Uri uri = folder.conversationListUri; if (id == LOADER_LOCALSEARCH_CONVERSATION_LIST) { String filter = args.getString(SearchParams.BUNDLE_QUERY_TERM); String field = args.getString(SearchParams.BUNDLE_QUERY_FIELD); if (!TextUtils.isEmpty(filter) && !TextUtils.isEmpty(field) && (folder.localSearchUri != null)) { uri = folder.buildLocalSearchUri(filter, field); } else { uri = folder.conversationListUri; LogUtils.logFeature(LogTag.SEARCH_TAG, "create local search uri failed for some field is null, use default conversationListUri"); } } //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 MOD_S int orderBy = args.getInt(BUNDLE_CONVERSATION_ORDER_KEY, SortHelper.SORT_BY_DATE); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-S //Note: get the star toggle's status, add to uri query parameters. uri = uri.buildUpon() .appendQueryParameter(UIProvider.FLAG_FAVORITE_QUERY_PARAMETER, String.valueOf(mCheckStatus)) .appendQueryParameter(UIProvider.ORDER_QUERY_PARAMETER, String.valueOf(orderBy)).build(); //TS: junwei-xu 2015-09-02 EMAIL BUGFIX-546917 ADD-E //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 MOD_E LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onCreateLoader uri %s", uri); //[FEATURE]-Mod-BEGIN by TSNJ,Zhenhua.Fan,06/11/2014,PR-622096, Uri tmpfolderUri = folder.conversationListUri; String tmpUri = folder.conversationListUri.toString(); String accountId = mAccount.uri.getLastPathSegment(); int index = folder.conversationListUri.toString().lastIndexOf("/"); if (EmailApplication.isOrangeImapFeatureOn() && com.tct.emailcommon.provider.Account .isOrangeImapAccount(mContext, Long.parseLong(accountId))) { if (folder.isType(FolderType.DRAFT)) { tmpUri = tmpUri.substring(0, index) + "/" + Mailbox.getOrangeImapDraftboxId(mContext, Long.valueOf(accountId)); tmpfolderUri = Uri.parse(tmpUri); } else if (folder.isType(FolderType.SENT)) { tmpUri = tmpUri.substring(0, index) + "/" + Mailbox.getOrangeImapSentboxId(mContext, Long.valueOf(accountId)); tmpfolderUri = Uri.parse(tmpUri); } else if (folder.isType(FolderType.TRASH)) { tmpUri = tmpUri.substring(0, index) + "/" + Mailbox.getOrangeImapTrashboxId(mContext, Long.valueOf(accountId)); tmpfolderUri = Uri.parse(tmpUri); } } //todo verity here return new ConversationCursorLoader(mActivity, account, uri, folder.name, ignoreInitialConversationLimit); // return new ConversationCursorLoader(mActivity, account, // tmpfolderUri, folder.getTypeDescription(), // ignoreInitialConversationLimit); //[FEATURE]-Mod-BEGIN by TSNJ,Zhenhua.Fan } @Override public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) { LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s", data, loader, this); if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) { LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring."); mConversationListLoadFinishedIgnored = true; return; } // Clear our all pending destructive actions before swapping the conversation cursor destroyPending(null); mConversationListCursor = data; mConversationListCursor.addListener(AbstractActivityController.this); mDrawIdler.setListener(mConversationListCursor); mTracker.onCursorUpdated(); mConversationListObservable.notifyChanged(); // Handle actions that were deferred until after the conversation list was loaded. for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) { callback.onLoadFinished(); } mConversationListLoadFinishedCallbacks.clear(); final ConversationListFragment convList = getConversationListFragment(); if (isFragmentVisible(convList)) { // The conversation list is already listening to list changes and gets notified // in the mConversationListObservable.notifyChanged() line above. We only need to // check and inform the cursor of the change in visibility here. informCursorVisiblity(true); } perhapsShowFirstSearchResult(); /// TCT: update search count. @{ int count = data.getCount(); //TS: Gantao 2015-12-16 EMAIL BUGFIX_1171140 ADD_S mConversationCount = count; //TS: Gantao 2015-12-16 EMAIL BUGFIX_1171140 ADD_E LogUtils.d(LOG_TAG, "AAC.ConversationCursor.onLoadFinished, count [%s]", count); if (loader.getId() == LOADER_LOCALSEARCH_CONVERSATION_LIST) { LogUtils.logFeature(LogTag.SEARCH_TAG, "AAC.onLoadFinished, update result as [%s]", count); updateSearchResult(count); } else if (mConvListContext != null && mConvListContext.isLocalSearch()) { // Normal conversation loader (LOADER_CONVERSATION_LIST) but in local search mode. // This case happened: // 1. user click search menu and enter local search mode, input nothing but list refreshing. // 2. user delete query in search view, and back to initial search mode. // Any way, in initial search mode, set search result as 0. LogUtils.logFeature(LogTag.SEARCH_TAG, "AAC.onLoadFinished recovery to inital model,set count 0 "); updateSearchResult(0); } /// @} /// TCT: For local search results, we need validate selected set manually, otherwise, /// this necessary process would miss and selected number does not update, such as /// deleting items in local search and rotating phone at once. /// Because CC in local search would not be notified refreshing due to the special observing uri, /// and never trigger AAC's onDatasetChanged. @{ if (loader.getId() == LOADER_LOCALSEARCH_CONVERSATION_LIST) { mSelectedSet.validateAgainstCursor(mConversationListCursor); } /// @} /// TCT: ConversationListFragment doesn't existed in some special case, such as ui controller changed /// dynamically, re-create and show it. @{ if (getConversationListFragment() == null) { LogUtils.d(LOG_TAG, "ConversationListFragment doesn't existed in some special case, re-create and show it."); if (mActivity == null || mActivity.getFragmentManager().isDestroyed()) { LogUtils.e(LOG_TAG, " mActivity is destoryed, can't show conversation list."); return; } int mode = mViewMode.getMode(); switch (mode) { case ViewMode.CONVERSATION_LIST: case ViewMode.SEARCH_RESULTS_LIST: showConversationList(mConvListContext); break; case ViewMode.CONVERSATION: if (AbstractActivityController.this instanceof TwoPaneController) { // for two pane controller, we have to show list fragment it even if the view mode was // in conversation view, cause the fragment also be shown at the left screen ((TwoPaneController) AbstractActivityController.this).renderConversationList(); } break; default: break; } } /// @} //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-913979 MOD_S if (!mHaveSearchResults) { mHandler.postDelayed(mLoadingViewRunnable, 3000); } //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-913979 MOD_E } @Override public void onLoaderReset(Loader<ConversationCursor> loader) { LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s", mConversationListCursor, loader, this); if (mConversationListCursor != null) { // Unregister the listener mConversationListCursor.removeListener(AbstractActivityController.this); mDrawIdler.setListener(null); mConversationListCursor = null; // Inform anyone who is interested about the change mTracker.onCursorUpdated(); mConversationListObservable.notifyChanged(); } } } //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-913979 MOD_S private final Runnable mLoadingViewRunnable = new Runnable() { @Override public void run() { //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-927510 MOD_S // mConversationListCursor.readyToShowSearchResults(); if (mConversationListCursor != null) { mConversationListCursor.readyToShowSearchResults(); } //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-927510 MOD_E } }; //TS: wenggangjin 2015-02-03 EMAIL BUGFIX_-913979 MOD_E /// TCT: update search result. Both local and remote search public void updateSearchResult(int count) { // Only we are in search update the count. if (mActionBarController != null && (mActionBarController.getSearch() != null || (mViewMode.isSearchMode(mViewMode.getMode())))) { mActionBarController.updateSearchCount(count); } } /** * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects. */ private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> { @Override public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) { final String[] everything = UIProvider.FOLDERS_PROJECTION; switch (id) { case LOADER_FOLDER_CURSOR: LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created"); final ObjectCursorLoader<Folder> loader = new ObjectCursorLoader<Folder>(mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY); loader.setUpdateThrottle(mFolderItemUpdateDelayMs); return loader; case LOADER_RECENT_FOLDERS: LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created"); if (mAccount != null && mAccount.recentFolderListUri != null && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) { return new ObjectCursorLoader<Folder>(mContext, mAccount.recentFolderListUri, everything, Folder.FACTORY); } break; case LOADER_ACCOUNT_INBOX: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created"); final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings); final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ? mAccount.folderListUri : defaultInbox; LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri); if (inboxUri != null) { return new ObjectCursorLoader<Folder>(mContext, inboxUri, everything, Folder.FACTORY); } break; case LOADER_SEARCH: LogUtils.d(LOG_TAG, "LOADER_SEARCH created"); return Folder.forSearchResults(mAccount, args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), /// TCT: remote search, add search filed. @{ args.getString(SearchParams.BUNDLE_QUERY_FIELD), /// @} // We can just use current time as a unique identifier for this search Long.toString(SystemClock.uptimeMillis()), mActivity.getActivityContext()); case LOADER_FIRST_FOLDER: LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created"); final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI); mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION); if (mConversationToShow != null && mConversationToShow.position < 0) { mConversationToShow.position = 0; } return new ObjectCursorLoader<Folder>(mContext, folderUri, everything, Folder.FACTORY); default: LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id); return null; } return null; } @Override public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) { if (data == null) { LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); } switch (loader.getId()) { case LOADER_FOLDER_CURSOR: LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR->onLoadFinished", "LOADER_FOLDER_CURSOR.onLoadFinished"); if (data != null && data.moveToFirst()) { final Folder folder = data.getModel(); setHasFolderChanged(folder); mFolder = folder; mFolderObservable.notifyChanged(); } else { LogUtils.d(LOG_TAG, "Unable to get the folder %s", mFolder != null ? mFolder.name : ""); } break; case LOADER_RECENT_FOLDERS: // Few recent folders and we are running on a phone? Populate the default // recents. The number of default recent folders is at least 2: every provider // has at least two folders, and the recent folder count never decreases. // Having a single recent folder is an erroneous case, and we can gracefully // recover by populating default recents. The default recents will not stomp on // the existing value: it will be shown in addition to the default folders: // the max number of recent folders is more than 1+num(defaultRecents). LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS->onLoadFinished", "LOADER_RECENT_FOLDERS.onLoadFinished"); if (data != null && data.getCount() <= 1 && !mIsTablet) { final class PopulateDefault extends AsyncTask<Uri, Void, Void> { @Override protected Void doInBackground(Uri... uri) { // Asking for an update on the URI and ignore the result. final ContentResolver resolver = mContext.getContentResolver(); resolver.update(uri[0], null, null, null); return null; } } final Uri uri = mAccount.defaultRecentFolderListUri; LogUtils.v(LOG_TAG, "Default recents at %s", uri); new PopulateDefault().execute(uri); break; } LogUtils.v(LOG_TAG, "Reading recent folders from the cursor."); mRecentFolderList.loadFromUiProvider(data); if (isAnimating()) { mRecentsDataUpdated = true; } else { mRecentFolderObservers.notifyChanged(); } break; case LOADER_ACCOUNT_INBOX: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX->onLoadFinished", "LOADER_ACCOUNT_INBOX.onLoadFinished"); if (data != null && !data.isClosed() && data.moveToFirst()) { final Folder inbox = data.getModel(); onFolderChanged(inbox, false /* force */); // Just want to get the inbox, don't care about updates to it // as this will be tracked by the folder change listener. mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); } else { LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", mAccount != null ? mAccount.getEmailAddress() : ""); } break; case LOADER_SEARCH: LogUtils.d(LOG_TAG, "LOADER_SEARCH->onLoadFinished", "LOADER_SEARCH.onLoadFinished"); if (data != null && data.getCount() > 0) { data.moveToFirst(); final Folder search = data.getModel(); /** TCT: initialize the listcontext in advance for remote search 's highligh t snippet @ { */ mConvListContext = ConversationListContext.forSearchQuery(mAccount, search, mConvListContext.searchQuery); mConvListContext .setSearchField(mActivity.getIntent().getStringExtra(SearchParams.BUNDLE_QUERY_FIELD)); if (!ConversationListContext.isSearchResult(mConvListContext)) { // Save the error status to log for analysis. LogUtils.e(LOG_TAG, "invalid ConversationListContext=%s", mConvListContext); } /** @} */ updateFolder(search); // TS: jin.dong 2015-07-01 EMAIL BUGFIX-1019473 MOD_S // mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, // mActivity.getIntent() // .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); // TS: jin.dong 2015-07-01 EMAIL BUGFIX-1019473 MOD_E showConversationList(mConvListContext); mActivity.invalidateOptionsMenu(); mHaveSearchResults = search.totalCount > 0; mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH); } else { LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader"); } break; case LOADER_FIRST_FOLDER: LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER->onLoadFinished", "LOADER_FIRST_FOLDER.onLoadFinished"); if (data == null || data.getCount() <= 0 || !data.moveToFirst()) { return; } final Folder folder = data.getModel(); boolean handled = false; if (folder != null) { onFolderChanged(folder, false /* force */); handled = true; } if (mConversationToShow != null) { // Open the conversation. showConversation(mConversationToShow); handled = true; } if (!handled) { // We have an account, but nothing else: load the default inbox. loadAccountInbox(); } mConversationToShow = null; // And don't run this anymore. mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER); break; } } @Override public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) { } } /** * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects. */ private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> { final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION; final CursorCreator<Account> mFactory = Account.FACTORY; @Override public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_ACCOUNT_CURSOR: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created"); return new ObjectCursorLoader<Account>(mContext, MailAppProvider.getAccountsUri(), mProjection, mFactory); case LOADER_ACCOUNT_UPDATE_CURSOR: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created"); return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection, mFactory); default: LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id); break; } return null; } @Override public void onLoadFinished(Loader<ObjectCursor<Account>> loader, ObjectCursor<Account> data) { if (data == null) { LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); } switch (loader.getId()) { case LOADER_ACCOUNT_CURSOR: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR->onLoadFinished", "LOADER_ACCOUNT_CURSOR.onLoadFinished"); // We have received an update on the list of accounts. if (data == null) { // Nothing useful to do if we have no valid data. break; } final long count = data.getCount(); if (count == 0) { /** TCT: Once the Activity is finishing, we should stop to setup Account @{ */ if (mActivity.isFinishing()) { LogUtils.d(LOG_TAG, "onLoadFinished finished just skip"); break; } /** @} */ // If an empty cursor is returned, the MailAppProvider is indicating that // no accounts have been specified. We want to navigate to the // "add account" activity that will handle the intent returned by the // MailAppProvider // If the MailAppProvider believes that all accounts have been loaded, // and the account list is still empty, we want to prompt the user to add // an account. final Bundle extras = data.getExtras(); final boolean accountsLoaded = extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; if (accountsLoaded) { final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext); // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 MOD_S if (noAccountIntent != null && mFirstLoadAccount) { mActivity.startActivityForResult(noAccountIntent, ADD_ACCOUNT_REQUEST_CODE); //when first launch email,this will start twice,this field to avoid it mFirstLoadAccount = false; // TS: zhaotianyong 2014-11-26 EMAIL BUGFIX-844194 ADD_S // TS: jin.dong 2015-6-9 EMAIL BUGFIX-1013807 MOD_S mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR); // TS: jin.dong 2015-6-9 EMAIL BUGFIX-1013807 MOD_e // TS: zhaotianyong 2014-11-26 EMAIL BUGFIX-844194 ADD_E // TS: zhonghua.tuo 2015-03-18 EMAIL BUGFIX-948923 MOD_E } } } else { final boolean accountListUpdated = accountsUpdated(data); if (!mHaveAccountList || accountListUpdated) { mHaveAccountList = updateAccounts(data); } Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT, Long.toString(count)); } break; case LOADER_ACCOUNT_UPDATE_CURSOR: LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR->onLoadFinished", "LOADER_ACCOUNT_UPDATE_CURSOR.onLoadFinished"); // We have received an update for current account. if (data != null && data.moveToFirst()) { final Account updatedAccount = data.getModel(); // Make sure that this is an update for the current account if (updatedAccount.uri.equals(mAccount.uri)) { final Settings previousSettings = mAccount.settings; // Update the controller's reference to the current account mAccount = updatedAccount; /** * TCT: Once the folder sync was later than the * loadAccountInbox() first call, the inbox uri maybe * change, in this case if there is no folder, we should * load the account inbox again, otherwise just do * nothing. * * @{ */ if ((mFolder == null) && !previousSettings.defaultInbox.equals(mAccount.settings.defaultInbox)) { loadAccountInbox(); } /** @} */ LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): " + "mAccount = %s", mAccount.uri); // Only notify about a settings change if something differs if (!Objects.equal(mAccount.settings, previousSettings)) { mAccountObservers.notifyChanged(); } perhapsEnterWaitMode(); perhapsStartWelcomeTour(); } else { LogUtils.e(LOG_TAG, "Got update for account: %s with current account:" + " %s", updatedAccount.uri, mAccount.uri); // We need to restart the loader, so the correct account information // will be returned. restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY); } } break; } } @Override public void onLoaderReset(Loader<ObjectCursor<Account>> loader) { // Do nothing. In onLoadFinished() we copy the relevant data from the cursor. } } /** * Loads the preference that tells whether the welcome tour should be displayed, * and calls the callback with this value. * For this to function, the account must have been synced. */ private void perhapsStartWelcomeTour() { new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(Void... params) { if (mActivity.wasLatestWelcomeTourShownOnDeviceForAllAccounts()) { // No need to go through the WelcomeStateLoader machinery. return false; } return true; } @Override protected void onPostExecute(Boolean result) { if (result) { if (mAccount != null && mAccount.isAccountReady()) { LoaderManager.LoaderCallbacks<?> welcomeLoaderCallbacks = mActivity.getWelcomeCallbacks(); if (welcomeLoaderCallbacks != null) { // The callback is responsible for showing the tour when appropriate. mActivity.getLoaderManager().initLoader(LOADER_WELCOME_TOUR_ACCOUNTS, Bundle.EMPTY, welcomeLoaderCallbacks); } } } } }.execute(); } /** * Updates controller state based on search results and shows first conversation if required. */ private void perhapsShowFirstSearchResult() { if (mCurrentConversation == null) { // Shown for search results in two-pane mode only. mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) && mConversationListCursor.getCount() > 0; if (!shouldShowFirstConversation()) { return; } mConversationListCursor.moveToPosition(0); final Conversation conv = new Conversation(mConversationListCursor); conv.position = 0; onConversationSelected(conv, true /* checkSafeToModifyFragments */); } } /** * Destroy the pending {@link DestructiveAction} till now and assign the given action as the * next destructive action.. * @param nextAction the next destructive action to be performed. This can be null. */ private void destroyPending(DestructiveAction nextAction) { // If there is a pending action, perform that first. if (mPendingDestruction != null) { mPendingDestruction.performAction(); } mPendingDestruction = nextAction; } /** * Register a destructive action with the controller. This performs the previous destructive * action as a side effect. This method is final because we don't want the child classes to * embellish this method any more. * @param action the action to register. */ private void registerDestructiveAction(DestructiveAction action) { // TODO(viki): This is not a good idea. The best solution is for clients to request a // destructive action from the controller and for the controller to own the action. This is // a half-way solution while refactoring DestructiveAction. destroyPending(action); } //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD_S /** * start a query to sort emails by order * @param order */ public void sort(int order) { final Bundle args = new Bundle(4); args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, mIgnoreInitialConversationLimit); args.putInt(BUNDLE_CONVERSATION_ORDER_KEY, order); mIgnoreInitialConversationLimit = false; mActivity.getLoaderManager().restartLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); } //TS: zheng.zou 2016-1-14 EMAIL TASK_1431225 ADD_E @Override public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) { final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true); da.setUndoCallback(undoCallback); registerDestructiveAction(da); return da; } @Override public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) { return getDeferredAction(action, mSelectedSet.values(), true, undoCallback); } /** * Get a destructive action for a menu action. This is a temporary method, * to control the profusion of {@link DestructiveAction} classes that are * created. Please do not copy this paradigm. * @param action the resource ID of the menu action: R.id.delete, for * example * @param target the conversations to act upon. * @return a {@link DestructiveAction} that performs the specified action. */ private DestructiveAction getDeferredAction(int action, Collection<Conversation> target, boolean batch, UndoCallback callback) { ConversationAction cAction = new ConversationAction(action, target, batch); cAction.setUndoCallback(callback); return cAction; } /** * Class to change the folders that are assigned to a set of conversations. This is destructive * because the user can remove the current folder from the conversation, in which case it has * to be animated away from the current folder. */ private class FolderDestruction implements DestructiveAction { private final Collection<Conversation> mTarget; private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>(); private final boolean mIsDestructive; /** Whether this destructive action has already been performed */ private boolean mCompleted; private final boolean mIsSelectedSet; private final boolean mShowUndo; private final int mAction; private final Folder mActionFolder; private UndoCallback mUndoCallback; /** * Create a new folder destruction object to act on the given conversations. * @param target conversations to act upon. * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar */ private FolderDestruction(final Collection<Conversation> target, final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, boolean showUndo, int action, final Folder actionFolder) { mTarget = ImmutableList.copyOf(target); mFolderOps.addAll(folders); mIsDestructive = isDestructive; mIsSelectedSet = isBatch; mShowUndo = showUndo; mAction = action; mActionFolder = actionFolder; } @Override public void setUndoCallback(UndoCallback undoCallback) { mUndoCallback = undoCallback; } @Override public void performAction() { if (isPerformed()) { return; } if (mIsDestructive && mShowUndo) { ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction, ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder); onUndoAvailable(undoOp); } // For each conversation, for each operation, add/ remove the // appropriate folders. ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); ArrayList<Uri> folderUris; ArrayList<Boolean> adds; for (Conversation target : mTarget) { HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target.getRawFolders()); folderUris = new ArrayList<Uri>(); adds = new ArrayList<Boolean>(); if (mIsDestructive) { target.localDeleteOnUpdate = true; } for (FolderOperation op : mFolderOps) { folderUris.add(op.mFolder.folderUri.fullUri); adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE); if (op.mAdd) { targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder); } else { targetFolders.remove(op.mFolder.folderUri.fullUri); } } ops.add(mConversationListCursor.getConversationFolderOperation(target, folderUris, adds, targetFolders.values(), mUndoCallback)); } if (mConversationListCursor != null) { mConversationListCursor.updateBulkValues(ops); } refreshConversationList(); if (mIsSelectedSet) { mSelectedSet.clear(); } } /** * Returns true if this action has been performed, false otherwise. * */ private synchronized boolean isPerformed() { if (mCompleted) { return true; } mCompleted = true; return false; } } public final DestructiveAction getFolderChange(Collection<Conversation> target, Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, boolean showUndo, final boolean isMoveTo, final Folder actionFolder, UndoCallback undoCallback) { final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive, isBatch, showUndo, isMoveTo, actionFolder, undoCallback); registerDestructiveAction(da); return da; } public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target, Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, boolean showUndo, final boolean isMoveTo, final Folder actionFolder, UndoCallback undoCallback) { final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch, showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder); fd.setUndoCallback(undoCallback); return fd; } @Override public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target, Folder toRemove, boolean isDestructive, boolean isBatch, boolean showUndo, UndoCallback undoCallback) { Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>(); folderOps.add(new FolderOperation(toRemove, false)); final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch, showUndo, R.id.remove_folder, mFolder); da.setUndoCallback(undoCallback); return da; } @Override public final void refreshConversationList() { final ConversationListFragment convList = getConversationListFragment(); if (convList == null) { return; } convList.requestListRefresh(); } protected final ActionClickedListener getUndoClickedListener(final AnimatedAdapter listAdapter) { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoNum) { if (mAccount.undoUri != null) { // NOTE: We might want undo to return the messages affected, in which case // the resulting cursor might be interesting... // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of // commands to undo if (mConversationListCursor != null) { //TS: chaozhang 2016-01-15 EMAIL BUGFIX_1355979 MOD_S //NOTE: use 50 as critical value is good. if (undoNum >= 50) { LogUtils.d(LOG_TAG, "UNDO operation number Exceed 100,and DO NOT notify UI avoid ANR,and the number is %d ", undoNum); mConversationListCursor.undoWithoutNotify(mActivity.getActivityContext(), mAccount.undoUri); } else { mConversationListCursor.undo(mActivity.getActivityContext(), mAccount.undoUri); } //TS: chaozhang 2016-01-15 EMAIL BUGFIX_1355979 MOD_E } if (listAdapter != null) { listAdapter.setUndo(true); } } } }; } /** * Shows an error toast in the bottom when a folder was not fetched successfully. * @param folder the folder which could not be fetched. * @param replaceVisibleToast if true, this should replace any currently visible toast. */ protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) { final ActionClickedListener listener; final int actionTextResourceId; final int lastSyncResult = folder.lastSyncResult; switch (lastSyncResult & 0x0f) { case UIProvider.LastSyncResult.CONNECTION_ERROR: //TS: wenggangjin 2014-12-31 EMAIL BUGFIX_881447 MOD_S // The sync request that caused this failure. // final int syncRequest = lastSyncResult >> 4; // Show: User explicitly pressed the refresh button and there is no connection // Show: The first time the user enters the app and there is no connection // TODO(viki): Implement this. // Reference: http://b/7202801 // final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0; // Don't show: Already in the app; user switches to a synced label // Don't show: In a live label and a background sync fails // final boolean avoidToast = !showToast && (folder.syncWindow > 0 // || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0); // if (avoidToast) { // return; // } //TS: wenggangjin 2014-12-31 EMAIL BUGFIX_881447 MOD_S listener = getRetryClickedListener(folder); actionTextResourceId = R.string.retry; break; case UIProvider.LastSyncResult.AUTH_ERROR: listener = getSignInClickedListener(); actionTextResourceId = R.string.signin; break; case UIProvider.LastSyncResult.SECURITY_ERROR: return; // Currently we do nothing for security errors. case UIProvider.LastSyncResult.STORAGE_ERROR: listener = getStorageErrorClickedListener(); actionTextResourceId = R.string.info; break; case UIProvider.LastSyncResult.INTERNAL_ERROR: listener = getInternalErrorClickedListener(); //TS: yanhua.chen 2015-7-7 EMAIL BUGFIX_1027389 MOD_S //Note remove report button when internal error //actionTextResourceId = R.string.report; actionTextResourceId = R.string.report_empty; //TS: yanhua.chen 2015-7-7 EMAIL BUGFIX_1027389 MOD_E return; default: return; } mToastBar.show(listener, Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult), actionTextResourceId, replaceVisibleToast, new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder)); } //TS: zheng.zou 2015-09-01 EMAIL BUGFIX_526255 ADD_S public void showUndoToastBar(int action) { onUndoAvailable(new ToastBarOperation(1, action, ToastBarOperation.UNDO, false, null)); } //TS: zheng.zou 2015-09-01 EMAIL BUGFIX_526255 ADD_E //TS: jin.dong 2015-12-29 EMAIL FEATURE_1125784 ADD_S public void showManualRefreshInRoamingIfNeed() { final AccountPreferences accountPreferences = new AccountPreferences(mContext, mAccount.getEmailAddress()); boolean isManualSyncWhenRoaming = accountPreferences.getManualSyncWhenRoamingEnabled(); if (isManualSyncWhenRoaming && com.tct.emailcommon.utility.Utility.isRoaming(mContext)) { final ActionClickedListener listener = new ActionClickedListener() { @Override public void onActionClicked(Context context, int num) { requestFolderRefresh(); } }; mToastBar.show(listener, mContext.getString(R.string.manual_sync_when_roaming_toast), R.string.manual_sync_when_roaming_refresh, true, new ToastBarOperation(1, 0, ToastBarOperation.INFO, false, null)); } } //TS: jin.dong 2015-12-29 EMAIL FEATURE_1125784 ADD_E //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD_S public void showDiscardDraftToast(long msgId) { // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 MOD_S mSavedDraftMsgId = msgId; final ActionClickedListener listener = getDiscardClickedListener(msgId); mToastBar.show(listener, mContext.getString(R.string.draft_saved), R.string.discard, true, new ToastBarOperation(1, 0, ToastBarOperation.DISCARD, false, null)); // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 MOD_E } //TS: zheng.zou 2015-11-25 EMAIL TASK_996919 ADD_S public void showNeedPermissionToast(int descId) { final ActionClickedListener listener = new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { PermissionUtil.gotoSettings(context); } }; mToastBar.show(listener, mContext.getString(descId), R.string.permission_grant_go_setting, true, new ToastBarOperation(1, 0, ToastBarOperation.INFO, false, null)); } //TS: zheng.zou 2015-11-25 EMAIL TASK_996919 ADD_E private ActionClickedListener getDiscardClickedListener(final long msgId) { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUnm) { //when we edit a draft and go back, //we will go to next conversation or return to conversation list after we delete a draft if (mCurrentConversation != null && mCurrentConversation.id == msgId) { final Collection<Conversation> target = Conversation.listOf(mCurrentConversation); // The user is choosing a new action; commit whatever they had been // doing before. Don't animate if we are launching a new screen. commitDestructiveActions(!doesActionChangeConversationListVisibility(R.id.discard_drafts)); final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance( R.id.discard_drafts, mCurrentConversation); delete(0, target, getDeferredAction(R.id.discard_drafts, target, false, undoCallback), false); } else { // delete draft directly Uri uri = Uri.parse("content://" + EmailContent.AUTHORITY + "/uimessage/" + msgId); mContext.getContentResolver().delete(uri, null, null); } mToastBar.postDelayed(new Runnable() { @Override public void run() { final ActionClickedListener emptyListener = new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { } }; mToastBar.show(emptyListener, mContext.getString(R.string.draft_discard), R.string.empty_string, true, new ToastBarOperation(1, 0, ToastBarOperation.DISCARDED, false, null)); // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 MOD } }, 500); } }; } //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD_E // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_S private void showDiscardedToast() { final ActionClickedListener emptyListener = new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { } }; mToastBar.show(emptyListener, mContext.getString(R.string.draft_discard), R.string.empty_string, true, new ToastBarOperation(1, 0, ToastBarOperation.DISCARDED, false, null)); } // TS: zheng.zou 2015-09-01 EMAIL BUGFIX-552138 ADD_E private ActionClickedListener getRetryClickedListener(final Folder folder) { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { final Uri uri = folder.refreshUri; if (uri != null) { startAsyncRefreshTask(uri); } } }; } private ActionClickedListener getSignInClickedListener() { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { promptUserForAuthentication(mAccount); } }; } private ActionClickedListener getStorageErrorClickedListener() { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { showStorageErrorDialog(); } }; } private void showStorageErrorDialog() { DialogFragment fragment = (DialogFragment) mFragmentManager .findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); if (fragment == null) { fragment = SyncErrorDialogFragment.newInstance(); } fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG); } private ActionClickedListener getInternalErrorClickedListener() { return new ActionClickedListener() { @Override public void onActionClicked(Context context, int undoUum) { Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */); } }; } @Override public void onFooterViewErrorActionClick(Folder folder, int errorStatus) { Uri uri = null; switch (errorStatus) { case UIProvider.LastSyncResult.CONNECTION_ERROR: if (folder != null && folder.refreshUri != null) { uri = folder.refreshUri; } break; case UIProvider.LastSyncResult.AUTH_ERROR: promptUserForAuthentication(mAccount); return; case UIProvider.LastSyncResult.SECURITY_ERROR: return; // Currently we do nothing for security errors. case UIProvider.LastSyncResult.STORAGE_ERROR: showStorageErrorDialog(); return; case UIProvider.LastSyncResult.INTERNAL_ERROR: Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */); return; default: return; } /// TCT: We should retry remote search if it's in remote search context. @{ if (mConvListContext != null && mConvListContext.isRemoteSearch()) { retryRemoteSearch(); } else if (uri != null) { startAsyncRefreshTask(uri); } /// @} } /** TCT: add for remote search*/ @Override public void onFooterViewRemoteSearchClick(Folder folder) { /** TCT: can not do remote search when network disconnection.@{ */ if (!Utility.hasConnectivity(mContext)) { UiUtilities.showConnectionAlertDialog(mActivity.getFragmentManager()); return; } /** @} */ if (mConvListContext != null && !TextUtils.isEmpty(mConvListContext.searchQuery)) { //MailActivity.sRecordOpening = false; executeSearch(mConvListContext.searchQuery); } } @Override public void onFooterViewLoadMoreClick(Folder folder) { if (folder != null && folder.loadMoreUri != null) { startAsyncRefreshTask(folder.loadMoreUri); } } private void startAsyncRefreshTask(Uri uri) { if (mFolderSyncTask != null) { mFolderSyncTask.cancel(true); } mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri); mFolderSyncTask.execute(); } private void promptUserForAuthentication(Account account) { if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) { final Intent authenticationIntent = new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri); //TS: jin.dong 2015-05-29 EMAIL BUGFIX_991085 ADD_S authenticationIntent.putExtra("AUTHENTICATIONFAILED", true); //TS: jin.dong 2015-05-29 EMAIL BUGFIX_991085 ADD_E mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE); } } @Override public void onAccessibilityStateChanged() { // Clear the cache of objects. ConversationItemViewModel.onAccessibilityUpdated(); // Re-render the list if it exists. final ConversationListFragment frag = getConversationListFragment(); if (frag != null) { AnimatedAdapter adapter = frag.getAnimatedAdapter(); if (adapter != null) { adapter.notifyDataSetInvalidated(); } } } @Override public void makeDialogListener(final int action, final boolean isBatch, UndoCallback undoCallback) { final Collection<Conversation> target; if (isBatch) { target = mSelectedSet.values(); } else { LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation); target = Conversation.listOf(mCurrentConversation); } final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch, undoCallback); mDialogAction = action; mDialogFromSelectedSet = isBatch; mDialogListener = new AlertDialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { delete(action, target, destructiveAction, isBatch); // Afterwards, let's remove references to the listener and the action. setListener(null, -1); } }; } @Override public AlertDialog.OnClickListener getListener() { return mDialogListener; } /** * Sets the listener for the positive action on a confirmation dialog. Since only a single * confirmation dialog can be shown, this overwrites the previous listener. It is safe to * unset the listener; in which case action should be set to -1. * @param listener the listener that will perform the task for this dialog's positive action. * @param action the action that created this dialog. */ private void setListener(AlertDialog.OnClickListener listener, final int action) { mDialogListener = listener; mDialogAction = action; } @Override public VeiledAddressMatcher getVeiledAddressMatcher() { return mVeiledMatcher; } @Override public void setDetachedMode() { // Tell the conversation list not to select anything. final ConversationListFragment frag = getConversationListFragment(); if (frag != null) { frag.setChoiceNone(); } else if (mIsTablet) { // How did we ever land here? Detached mode, and no CLF on tablet??? LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!"); } mDetachedConvUri = mCurrentConversation.uri; } private void clearDetachedMode() { // Tell the conversation list to go back to its usual selection behavior. final ConversationListFragment frag = getConversationListFragment(); if (frag != null) { frag.revertChoiceMode(); } else if (mIsTablet) { // How did we ever land here? Detached mode, and no CLF on tablet??? LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!"); } mDetachedConvUri = null; } @Override public DrawerController getDrawerController() { return mDrawerListener; } private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener> implements DrawerLayout.DrawerListener, DrawerController { private int mDrawerState; private float mOldSlideOffset; public MailDrawerListener() { mDrawerState = DrawerLayout.STATE_IDLE; mOldSlideOffset = 0.f; } @Override public boolean isDrawerEnabled() { return AbstractActivityController.this.isDrawerEnabled(); } @Override public void registerDrawerListener(DrawerLayout.DrawerListener l) { registerObserver(l); } @Override public void unregisterDrawerListener(DrawerLayout.DrawerListener l) { unregisterObserver(l); } @Override public boolean isDrawerOpen() { return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout); } @Override public boolean isDrawerVisible() { return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout); } @Override public void toggleDrawerState() { AbstractActivityController.this.toggleDrawerState(); } @Override public void onDrawerOpened(View drawerView) { mDrawerToggle.onDrawerOpened(drawerView); for (DrawerLayout.DrawerListener l : mObservers) { l.onDrawerOpened(drawerView); } } @Override public void onDrawerClosed(View drawerView) { mDrawerToggle.onDrawerClosed(drawerView); if (mHasNewAccountOrFolder) { refreshDrawer(); } // When closed, we want to use either the burger, or up, based on where we are final int mode = mViewMode.getMode(); final boolean isTopLevel = Folder.isRoot(mFolder); mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel)); for (DrawerLayout.DrawerListener l : mObservers) { l.onDrawerClosed(drawerView); } } /** * As part of the overriden function, it will animate the alpha of the conversation list * view along with the drawer sliding when we're in the process of switching accounts or * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}. */ @Override public void onDrawerSlide(View drawerView, float slideOffset) { mDrawerToggle.onDrawerSlide(drawerView, slideOffset); if (mHasNewAccountOrFolder && mListViewForAnimating != null) { mListViewForAnimating.setAlpha(slideOffset); } // This code handles when to change the visibility of action items // based on drawer state. The basic logic is that right when we // open the drawer, we hide the action items. We show the action items // when the drawer closes. However, due to the animation of the drawer closing, // to make the reshowing of the action items feel right, we make the items visible // slightly sooner. // // However, to make the animating behavior work properly, we have to know whether // we're animating open or closed. Only if we're animating closed do we want to // show the action items early. We save the last slide offset so that we can compare // the current slide offset to it to determine if we're opening or closing. if (mDrawerState == DrawerLayout.STATE_SETTLING) { if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) { mHideMenuItems = false; mActivity.supportInvalidateOptionsMenu(); maybeEnableCabMode(); } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) { mHideMenuItems = true; mActivity.supportInvalidateOptionsMenu(); disableCabMode(); } } else { if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) { mHideMenuItems = false; mActivity.supportInvalidateOptionsMenu(); maybeEnableCabMode(); } else if (!mHideMenuItems && slideOffset > 0.f) { mHideMenuItems = true; mActivity.supportInvalidateOptionsMenu(); disableCabMode(); } } mOldSlideOffset = slideOffset; // If we're sliding, we always want to show the burger //TS: jian.xu 2015-1-7 EMAIL BUGFIX-883925 DEL_S //mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */); //TS: jian.xu 2015-1-7 EMAIL BUGFIX-883925 DEL_E for (DrawerLayout.DrawerListener l : mObservers) { l.onDrawerSlide(drawerView, slideOffset); } } /** * This condition here should only be called when the drawer is stuck in a weird state * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh * and, more importantly, unlock the drawer when this is the case. */ @Override public void onDrawerStateChanged(int newState) { LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState); mDrawerState = newState; mDrawerToggle.onDrawerStateChanged(mDrawerState); for (DrawerLayout.DrawerListener l : mObservers) { l.onDrawerStateChanged(newState); } if (mViewMode.isSearchMode()) { return; } if (mDrawerState == DrawerLayout.STATE_IDLE) { if (mHasNewAccountOrFolder) { refreshDrawer(); } if (mConversationListLoadFinishedIgnored) { mConversationListLoadFinishedIgnored = false; final Bundle args = new Bundle(); args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); mActivity.getLoaderManager().initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); } } } /** * If we've reached a stable drawer state, unlock the drawer for usage, clear the * conversation list, and finish end actions. Also, make * {@link #mHasNewAccountOrFolder} false to reflect we're done changing. */ public void refreshDrawer() { mHasNewAccountOrFolder = false; mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); ConversationListFragment conversationList = getConversationListFragment(); if (conversationList != null) { conversationList.clear(); } mFolderOrAccountObservers.notifyChanged(); } /** * Returns the most recent update of the {@link DrawerLayout}'s state provided * by {@link #onDrawerStateChanged(int)}. * @return The {@link DrawerLayout}'s current state. One of * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE}, * or {@link DrawerLayout#STATE_SETTLING}. */ public int getDrawerState() { return mDrawerState; } } @Override public boolean isDrawerPullEnabled() { return true; } @Override public boolean shouldHideMenuItems() { return mHideMenuItems; } protected void navigateUpFolderHierarchy() { new AsyncTask<Void, Void, Folder>() { @Override protected Folder doInBackground(final Void... params) { if (mInbox == null) { // We don't have an inbox, but we need it final Cursor cursor = mContext.getContentResolver().query(mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { mInbox = new Folder(cursor); } } finally { cursor.close(); } } } // Now try to load our parent final Folder folder; if (mFolder != null) { Cursor cursor = null; try { cursor = mContext.getContentResolver().query(mFolder.parent, UIProvider.FOLDERS_PROJECTION, null, null, null); if (cursor == null || !cursor.moveToFirst()) { // We couldn't load the parent, so use the inbox folder = mInbox; } else { folder = new Folder(cursor); } } finally { if (cursor != null) { cursor.close(); } } } else { folder = mInbox; } return folder; } @Override protected void onPostExecute(final Folder result) { onFolderSelected(result); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } @Override public Parcelable getConversationListScrollPosition(final String folderUri) { return mConversationListScrollPositions.getParcelable(folderUri); } @Override public void setConversationListScrollPosition(final String folderUri, final Parcelable savedPosition) { mConversationListScrollPositions.putParcelable(folderUri, savedPosition); } @Override public View.OnClickListener getNavigationViewClickListener() { return mHomeButtonListener; } // TODO: Fold this into the outer class when b/16627877 is fixed private class HomeButtonListener implements View.OnClickListener { @Override public void onClick(View v) { onUpPressed(); } } /** * TCT: use to update footer view loading status. */ private void updateFooterStatus(boolean isStarted) { ConversationListFragment listFragment = getConversationListFragment(); if (listFragment != null) { listFragment.updateFooterStatus(isStarted); } } //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD_S private class DraftSaveBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent != null && ComposeActivity.DRAFT_SAVED_ACTION.equals(intent.getAction())) { long msgId = intent.getLongExtra(BaseColumns._ID, -1); if (msgId != -1) { showDiscardDraftToast(msgId); //the event is handled, abort here abortBroadcast(); } } } } //TS: zheng.zou 2015-03-18 EMAIL FEATURE_996919 ADD_E /* * Show the toolbar and fab button */ @Override public void animateShow(ImageButton fabButton) { Toolbar toolbar = mActivity.getToolbar(); if (toolbar == null) { return; } //Remove other animation. if (hideAnimatorSet != null && hideAnimatorSet.isRunning()) { hideAnimatorSet.cancel(); } if (backAnimatorSet != null && backAnimatorSet.isRunning()) { //If the animation is running,do nothing. } else { if (!mToolbarHidden) { //toolbar is showing,no need to do the show animation. return; } backAnimatorSet = new AnimatorSet(); ArrayList<Animator> animators = new ArrayList<>(); //Show the toolbar ObjectAnimator headerAnimator = ObjectAnimator.ofFloat(toolbar, "translationY", toolbar.getTranslationY(), 0f); if (fabButton != null) { //Show the fabButton ObjectAnimator fabAnimator = ObjectAnimator.ofFloat(fabButton, "translationY", fabButton.getTranslationY(), 0f); animators.add(fabAnimator); } if (mSearchHeader != null) { //Show search header view //If we are searching the message, we should also show the search header ObjectAnimator searchHeaderAnimator = ObjectAnimator.ofFloat(mSearchHeader, "translationY", mSearchHeader.getTranslationY(), 0f); animators.add(searchHeaderAnimator); } animators.add(headerAnimator); backAnimatorSet.setDuration(200); backAnimatorSet.playTogether(animators); backAnimatorSet.start(); mToolbarHidden = false; } } //TS: tao.gan 2015-09-11 EMAIL FEATURE-559893 ADD_S /* * Hide the toolbar and the fab button */ @Override public void animateHide(ImageButton fabButton) { Toolbar toolbar = mActivity.getToolbar(); if (toolbar == null) { return; } //Remove other animations first if (backAnimatorSet != null && backAnimatorSet.isRunning()) { backAnimatorSet.cancel(); } if (hideAnimatorSet != null && hideAnimatorSet.isRunning()) { //If the animation is running, do nothing. } else { if (mToolbarHidden) { //toolbar is hidden,no need to do the hide animation return; } hideAnimatorSet = new AnimatorSet(); ArrayList<Animator> animators = new ArrayList<>(); //Hide the toolbar ObjectAnimator headerAnimator = ObjectAnimator.ofFloat(toolbar, "translationY", toolbar.getTranslationY(), -toolbar.getHeight()); //Hide the fabButton ObjectAnimator fabAnimator = ObjectAnimator.ofFloat(fabButton, "translationY", fabButton.getTranslationY(), ((View) fabButton.getParent()).getHeight()); //TS: zheng.zou 2015-10-22 EMAIL BUGFIX-721230 MOD if (mSearchHeader != null) { //Hide search header view //If we are searching the message, we should also hide the search header ObjectAnimator searchHeaderAnimator = ObjectAnimator.ofFloat(mSearchHeader, "translationY", toolbar.getTranslationY(), -mSearchHeader.getHeight()); animators.add(searchHeaderAnimator); } animators.add(headerAnimator); animators.add(fabAnimator); hideAnimatorSet.setDuration(200); hideAnimatorSet.playTogether(animators); hideAnimatorSet.start(); mToolbarHidden = true; } } //TS: tao.gan 2015-09-11 EMAIL FEATURE-559893 ADD_S @Override public ImageButton getComposeButton() { return (ImageButton) mFloatingComposeButton; } //TS: tao.gan 2015-09-11 EMAIL FEATURE-559893 ADD_E public void setSearchHeader(HorizontalScrollView header) { mSearchHeader = header; } //TS: tao.gan 2015-09-11 EMAIL FEATURE-559893 ADD_E //TS: tao.gan 2015-09-12 EMAIL BUGFIX-1080620 ADD_S public void backToList(Conversation conversation) { } //TS: tao.gan 2015-09-12 EMAIL BUGFIX-1080620 ADD_E }