loader) {
}
/**
* {@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).
*
* 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
*/
private void restartOptionalLoader(int id) {
final LoaderManager lm = mActivity.getLoaderManager();
lm.destroyLoader(id);
lm.restartLoader(id, Bundle.EMPTY, this);
}
/**
* Start a loader with the given id. This should be called when we know that the previous
* state of the application matches this state, and we are happy if we get the previously
* created loader with this id. If that is not true, consider calling
* {@link #restartOptionalLoader(int)} instead.
* @param id
*/
private void startLoader(int id) {
final LoaderManager lm = mActivity.getLoaderManager();
lm.initLoader(id, Bundle.EMPTY, this);
}
@Override
public void registerConversationListObserver(DataSetObserver observer) {
mConversationListObservable.registerObserver(observer);
}
@Override
public void unregisterConversationListObserver(DataSetObserver observer) {
mConversationListObservable.unregisterObserver(observer);
}
private boolean accountsUpdated(Cursor 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 Uri accountUri =
Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
foundCurrentAccount = true;
}
if (!mCurrentAccountUris.contains(accountUri)) {
return true;
}
} while (accountCursor.moveToNext());
// As long as we found the current account, the list hasn't been updated
return !foundCurrentAccount;
}
/**
* Update the accounts on the device. This currently loads the first account
* in the list.
*
* @param loader
* @param accounts cursor into the AccountCache
* @return true if the update was successful, false otherwise
*/
private boolean updateAccounts(Loader loader, Cursor accounts) {
if (accounts == null || !accounts.moveToFirst()) {
return false;
}
final Account[] allAccounts = Account.getAllAccounts(accounts);
// Save the uris for the accounts
mCurrentAccountUris.clear();
for (Account account : allAccounts) {
mCurrentAccountUris.add(account.uri);
}
// 1. current account is already set and is in allAccounts -> no-op
// 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
// 3. saved pref has an account -> pick that one
// 4. otherwise just pick first
Account newAccount = null;
if (mAccount != null) {
if (!mCurrentAccountUris.contains(mAccount.uri)) {
newAccount = allAccounts[0];
} else {
newAccount = mAccount;
}
} else {
final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
if (lastAccountUri != null) {
for (int i = 0; i < allAccounts.length; i++) {
final Account acct = allAccounts[i];
if (lastAccountUri.equals(acct.uri.toString())) {
newAccount = acct;
break;
}
}
}
if (newAccount == null) {
newAccount = allAccounts[0];
}
}
onAccountChanged(newAccount);
mActionBarView.setAccounts(allAccounts);
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);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(Loader loader, Cursor data) {
// We want to reinitialize only if we haven't ever been initialized, or
// if the current account has vanished.
if (data == null) {
LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
}
switch (loader.getId()) {
case LOADER_ACCOUNT_CURSOR:
// If the account list is not not null, and the account list cursor is empty,
// we need to start the specified activity.
if (data != null && data.getCount() == 0) {
// 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);
if (noAccountIntent != null) {
mActivity.startActivityForResult(noAccountIntent,
ADD_ACCOUNT_REQUEST_CODE);
}
}
} else {
final boolean accountListUpdated = accountsUpdated(data);
if (!isLoaderInitialized || accountListUpdated) {
isLoaderInitialized = updateAccounts(loader, data);
}
}
break;
case LOADER_ACCOUNT_UPDATE_CURSOR:
// We have gotten an update for current account.
// Make sure that this is an update for what is the current account
if (data != null && data.moveToFirst()) {
final Account updatedAccount = new Account(data);
if (updatedAccount.uri.equals(mAccount.uri)) {
// Update the controller's reference to the current account
mAccount = updatedAccount;
dispatchSettingsChange(mAccount.settings);
// Got an update for the current account
final boolean inWaitingMode = inWaitMode();
if (!updatedAccount.isAccountIntialized() && !inWaitingMode) {
// Transition to waiting mode
showWaitForInitialization();
} else if (updatedAccount.isAccountIntialized() && inWaitingMode) {
// Dismiss waiting mode
hideWaitForInitialization();
} else if (!updatedAccount.isAccountIntialized() && inWaitingMode) {
// Update the WaitFragment's account object
updateWaitMode();
}
} 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);
}
}
break;
case LOADER_FOLDER_CURSOR:
// Check status of the cursor.
data.moveToFirst();
Folder folder = new Folder(data);
if (folder.isSyncInProgress()) {
mActionBarView.onRefreshStarted();
} else {
// Stop the spinner here.
mActionBarView.onRefreshStopped(folder.lastSyncResult);
}
mActionBarView.onFolderUpdated(folder);
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
convList.onFolderUpdated(folder);
}
LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
break;
case LOADER_RECENT_FOLDERS:
mRecentFolderList.loadFromUiProvider(data);
mActionBarView.requestRecentFoldersAndRedraw();
break;
case LOADER_ACCOUNT_INBOX:
if (data.moveToFirst() && !data.isClosed()) {
Folder inbox = new Folder(data);
onFolderChanged(inbox);
// 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.name : "");
}
break;
case LOADER_SEARCH:
data.moveToFirst();
Folder search = new Folder(data);
setFolder(search);
mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
mActivity.getIntent()
.getStringExtra(UIProvider.SearchQueryParameters.QUERY));
showConversationList(mConvListContext);
mActivity.invalidateOptionsMenu();
mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
break;
}
}
protected abstract class AbstractDestructiveAction implements DestructiveAction {
protected final int mAction;
/**
* Create a listener object. action is one of four constants: R.id.y_button (archive),
* R.id.delete , R.id.mute, and R.id.report_spam.
* @param action
*/
public AbstractDestructiveAction(int action) {
mAction = action;
}
public void performConversationAction(Collection single) {
switch (mAction) {
case R.id.archive:
LogUtils.d(LOG_TAG, "Archiving conversation %s", mCurrentConversation);
mConversationListCursor.archive(mContext, single);
break;
case R.id.delete:
LogUtils.d(LOG_TAG, "Deleting conversation %s", mCurrentConversation);
mConversationListCursor.delete(mContext, single);
break;
case R.id.mute:
LogUtils.d(LOG_TAG, "Muting conversation %s", mCurrentConversation);
if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE))
mCurrentConversation.localDeleteOnUpdate = true;
mConversationListCursor.mute(mContext, single);
break;
case R.id.report_spam:
LogUtils.d(LOG_TAG, "reporting spam conversation %s", mCurrentConversation);
mConversationListCursor.reportSpam(mContext, single);
break;
}
}
/**
* Get the next conversation according to the AutoAdvance settings and the list of
* conversations available in the folder.
* @return
*/
public Conversation getNextConversation() {
final int pref = getAutoAdvanceSetting(mCachedSettings);
final boolean getNewer = (pref == AutoAdvance.NEWER && mTracker.hasNewer());
final boolean getOlder = (pref == AutoAdvance.OLDER && mTracker.hasOlder());
final Conversation next = getNewer ? mTracker.getNewer() :
(getOlder ? mTracker.getOlder() : null);
return next;
}
@Override
public abstract void performAction();
}
// Called from the FolderSelectionDialog after a user is done changing
// folders.
@Override
public void onFolderChangesCommit(ArrayList folderChangeList) {
// Get currently active folder info and compare it to the list
// these conversations have been given; if they no longer contain
// the selected folder, delete them from the list.
HashSet folderUris = new HashSet();
if (folderChangeList != null && !folderChangeList.isEmpty()) {
for (Folder f : folderChangeList) {
folderUris.add(f.uri.toString());
}
}
final boolean destructiveChange = !folderUris.contains(mFolder.uri.toString());
DestructiveAction listener = getFolderDestructiveAction();
StringBuilder foldersUrisString = new StringBuilder();
boolean first = true;
for (Folder f : folderChangeList) {
if (first) {
first = false;
} else {
foldersUrisString.append(',');
}
foldersUrisString.append(f.uri.toString());
}
updateCurrentConversation(ConversationColumns.FOLDER_LIST, foldersUrisString.toString());
updateCurrentConversation(ConversationColumns.RAW_FOLDERS,
Folder.getSerializedFolderString(mFolder, folderChangeList));
// TODO: (mindyp): set ConversationColumns.RAW_FOLDERS like in
// SelectedConversationsActionMenu
if (destructiveChange) {
mCurrentConversation.localDeleteOnUpdate = true;
requestDelete(listener);
} else {
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
convList.requestListRefresh();
}
}
}
@Override
public void onRefreshRequired() {
if (mIsConversationListScrolling) {
LogUtils.d(LOG_TAG, "onRefreshRequired: delay until scrolling done");
return;
}
// Refresh the query in the background
long now = System.currentTimeMillis();
long sinceLastRefresh = now - mConversationListRefreshTime;
// if (sinceLastRefresh > CONVERSATION_LIST_THROTTLE_MS) {
if (getConversationListCursor().isRefreshRequired()) {
getConversationListCursor().refresh();
mTracker.updateCursor(mConversationListCursor);
mConversationListRefreshTime = now;
}
// } else {
// long delay = CONVERSATION_LIST_THROTTLE_MS - sinceLastRefresh;
// LogUtils.d(LOG_TAG, "onRefreshRequired: delay for %sms", delay);
// mConversationListRefreshTask = new RefreshTimerTask(this, mHandler);
// mConversationListTimer.schedule(mConversationListRefreshTask, delay);
// }
}
/**
* Called when the {@link ConversationCursor} is changed or has new data in it.
*
* {@inheritDoc}
*/
@Override
public void onRefreshReady() {
final ArrayList deletedRows = mConversationListCursor.getRefreshDeletions();
// If we have any deletions from the server, and the conversations are in the list view,
// remove them from a selected set, if any
if (!deletedRows.isEmpty() && !mSelectedSet.isEmpty()) {
mSelectedSet.delete(deletedRows);
}
// If we have any deletions from the server, animate them away
final ConversationListFragment convList = getConversationListFragment();
if (!deletedRows.isEmpty() && convList != null) {
final AnimatedAdapter adapter = convList.getAnimatedAdapter();
if (adapter != null) {
adapter.delete(deletedRows, this);
}
} if (!mIsConversationListScrolling) {
// Swap cursors
mConversationListCursor.sync();
refreshAdapter();
}
mTracker.updateCursor(mConversationListCursor);
}
@Override
public void onDataSetChanged() {
refreshAdapter();
mConversationListObservable.notifyChanged();
}
private void refreshAdapter() {
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
final AnimatedAdapter adapter = convList.getAnimatedAdapter();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
}
/**
* 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 onScrollStateChanged(AbsListView view, int scrollState) {
boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE);
if (!isScrolling) {
ConversationCursor cc = getConversationListCursor();
if (cc.isRefreshRequired()) {
LogUtils.d(LOG_TAG, "Stop scrolling: refresh");
cc.refresh();
} else if (cc.isRefreshReady()) {
LogUtils.d(LOG_TAG, "Stop scrolling: try sync");
onRefreshReady();
}
}
mIsConversationListScrolling = isScrolling;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
}
@Override
public void onSetEmpty() {
}
@Override
public void onSetPopulated(ConversationSelectionSet set) {
final ConversationListFragment convList = getConversationListFragment();
if (convList == null) {
return;
}
mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set,
convList.getAnimatedAdapter(), this, this,
mAccount, mFolder, (SwipeableListView) convList.getListView());
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() {
if (mCabActionMenu != null) {
mCabActionMenu.deactivate();
}
}
/**
* Re-enable the CAB menu if required. The selection set is not changed.
*/
protected void enableCabMode() {
if (mCabActionMenu != null) {
mCabActionMenu.activate();
}
}
@Override
public void performAction() {
if (getConversationListCursor().isRefreshReady()) {
refreshAdapter();
}
}
@Override
public void startSearch() {
if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
| mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
onSearchRequested(mActionBarView.getQuery());
} else {
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)
&& folder.supportsCapability
(UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
&& !mFolder.uri.equals(folder.uri));
}
/**
* Handles dropping conversations to a label.
*/
@Override
public void handleDrop(DragEvent event, final Folder folder) {
/*
* Expect clip data has form: [conversations_uri, conversationId1,
* maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
*/
if (!supportsDrag(event, folder)) {
return;
}
ClipData data = event.getClipData();
ArrayList conversationPositions = Lists.newArrayList();
for (int i = 1; i < data.getItemCount(); i += 3) {
int position = Integer.parseInt(data.getItemAt(i).getText().toString());
conversationPositions.add(position);
}
final Collection conversations = mSelectedSet.values();
final ConversationListFragment convList = getConversationListFragment();
if (convList == null) {
return;
}
convList.requestDelete(conversations,
new DestructiveAction() {
@Override
public void performAction() {
AbstractActivityController.this.performAction();
ArrayList changes = new ArrayList();
changes.add(folder);
mConversationListCursor.updateString(mContext, conversations,
ConversationColumns.FOLDER_LIST, folder.uri.toString());
mConversationListCursor.updateString(mContext, conversations,
ConversationColumns.RAW_FOLDERS,
Folder.getSerializedFolderString(mFolder, changes));
onUndoAvailable(new UndoOperation(conversations
.size(), R.id.change_folder));
mSelectedSet.clear();
}
});
}
@Override
public void onUndoCancel() {
mUndoBarView.hide(false);
}
@Override
public void onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mUndoBarView != null && !mUndoBarView.isEventInUndo(event)) {
mUndoBarView.hide(true);
}
}
}
@Override
public void onConversationSeen(Conversation conv) {
mPagerController.onConversationSeen(conv);
}
private class ConversationListLoaderCallbacks implements
LoaderManager.LoaderCallbacks {
@Override
public Loader onCreateLoader(int id, Bundle args) {
Loader result = new ConversationCursorLoader((Activity) mActivity,
mAccount, mFolder.conversationListUri, mFolder.name);
return result;
}
@Override
public void onLoadFinished(Loader loader, ConversationCursor data) {
LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
data, loader);
mConversationListCursor = data;
mConversationListCursor.addListener(AbstractActivityController.this);
// Register the AbstractActivityController as a listener to changes in
// data in the cursor.
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
convList.onCursorUpdated();
convList.getListView().setOnScrollListener(AbstractActivityController.this);
}
// Shown for search results in two-pane mode only.
if (shouldShowFirstConversation()) {
if (mConversationListCursor.getCount() > 0) {
mConversationListCursor.moveToPosition(0);
if (convList != null) {
convList.getListView().setItemChecked(0, true);
}
final Conversation conv = new Conversation(mConversationListCursor);
conv.position = 0;
onConversationSelected(conv);
}
}
}
@Override
public void onLoaderReset(Loader loader) {
final ConversationListFragment convList = getConversationListFragment();
if (convList == null) {
return;
}
convList.onCursorUpdated();
}
}
@Override
public void sendConversationRead(String toFragment, Conversation conversation, boolean state,
boolean local) {
if (toFragment.equals(TAG_CONVERSATION_LIST)) {
ConversationCursor cc = getConversationListCursor();
if (cc != null) {
if (local) {
cc.setConversationColumn(conversation.uri.toString(), ConversationColumns.READ,
state);
} else {
cc.markRead(mContext, state, conversation);
}
}
} else if (toFragment.equals(TAG_CONVERSATION)) {
// TODO Handle setting read in conversation view
}
}
@Override
public void sendConversationUriStarred(String toFragment, String conversationUri,
boolean state, boolean local) {
if (toFragment.equals(TAG_CONVERSATION_LIST)) {
ConversationCursor cc = getConversationListCursor();
if (cc != null) {
if (local) {
cc.setConversationColumn(conversationUri, ConversationColumns.STARRED, state);
} else {
cc.updateBoolean(mContext, conversationUri, ConversationColumns.STARRED, state);
}
}
} else if (toFragment.equals(TAG_CONVERSATION)) {
// TODO Handle setting starred in conversation view
}
}
}