Merge "Check for null before clearing selection set."
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index b17de1e..9da3337 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -39,8 +39,6 @@
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
import com.android.mail.providers.UIProvider.ConversationOperations;
-import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.ConversationPositionTracker;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
@@ -82,6 +80,9 @@
// The index of the Uri whose data is reflected in the cached row
// Updates/Deletes to this Uri are cached
private static int sUriColumnIndex;
+ // The listeners registered for this cursor
+ private static ArrayList<ConversationListener> sListeners =
+ new ArrayList<ConversationListener>();
// The ConversationProvider instance
@VisibleForTesting
static ConversationProvider sProvider;
@@ -110,10 +111,6 @@
private boolean mCursorObserverRegistered = false;
// Whether or not sync from underlying provider should be deferred
private static boolean sDeferSync = false;
- // The adapter using this cursor
- private static AnimatedAdapter sAdapter;
- // The position tracker that's watching this cursor
- private static ConversationPositionTracker sTracker;
// The current position of the cursor
private int mPosition = -1;
@@ -142,6 +139,7 @@
sUnderlyingCursor.close();
}
sUnderlyingCursor = cursor;
+ sListeners.clear();
sRefreshRequired = false;
sRefreshReady = false;
sRefreshTask = null;
@@ -166,22 +164,6 @@
}
/**
- * Set the adapter using this ConversationCursor
- * @param adapter the adapter
- */
- public void setAdapter(AnimatedAdapter adapter) {
- sAdapter = adapter;
- }
-
- /**
- * Set the position tracker for this cursor; we notify it at every sync
- * @param tracker the tracker
- */
- public static void setTracker(ConversationPositionTracker tracker) {
- sTracker = tracker;
- }
-
- /**
* Create a ConversationCursor; this should be called by the ListActivity using that cursor
* @param activity the activity creating the cursor
* @param messageListColumn the column used for individual cursor items
@@ -431,6 +413,28 @@
}
/**
+ * Add a listener for this cursor; we'll notify it when our data changes
+ */
+ public void addListener(ConversationListener listener) {
+ synchronized (sListeners) {
+ if (!sListeners.contains(listener)) {
+ sListeners.add(listener);
+ } else {
+ LogUtils.i(TAG, "Ignoring duplicate add of listener");
+ }
+ }
+ }
+
+ /**
+ * Remove a listener for this cursor
+ */
+ public void removeListener(ConversationListener listener) {
+ synchronized(sListeners) {
+ sListeners.remove(listener);
+ }
+ }
+
+ /**
* Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by
* changing the authority to ours, but otherwise leaving the Uri intact.
* NOTE: This won't handle query parameters, so the functionality will need to be added if
@@ -601,8 +605,12 @@
if (DEBUG) {
LogUtils.i(TAG, "[Notify: onRefreshRequired()]");
}
- if (!sDeferSync && sAdapter != null) {
- sAdapter.onRefreshRequired();
+ if (!sDeferSync) {
+ synchronized(sListeners) {
+ for (ConversationListener listener: sListeners) {
+ listener.onRefreshRequired();
+ }
+ }
}
}
@@ -613,8 +621,10 @@
if (DEBUG) {
LogUtils.i(TAG, "[Notify: onRefreshReady()]");
}
- if (sAdapter != null) {
- sAdapter.onRefreshReady();
+ synchronized(sListeners) {
+ for (ConversationListener listener: sListeners) {
+ listener.onRefreshReady();
+ }
}
}
@@ -625,8 +635,10 @@
if (DEBUG) {
LogUtils.i(TAG, "[Notify: onDataSetChanged()]");
}
- if (sAdapter != null) {
- sAdapter.notifyDataSetChanged();
+ synchronized(sListeners) {
+ for (ConversationListener listener: sListeners) {
+ listener.onDataSetChanged();
+ }
}
}
@@ -652,9 +664,6 @@
sRefreshReady = false;
}
notifyDataChanged();
- if (sTracker != null) {
- sTracker.updateCursor(sConversationCursor);
- }
}
public boolean isRefreshRequired() {
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index ab64a70..3b30c9e 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -118,6 +118,8 @@
private static final String EXTRA_BODY = "body";
+ private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
+
// Extra that we can get passed from other activities
private static final String EXTRA_TO = "to";
private static final String EXTRA_CC = "cc";
@@ -534,13 +536,19 @@
}
private void initFromSpinner(Bundle bundle, int action) {
+ String accountString = null;
if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
action = COMPOSE;
}
mFromSpinner.asyncInitFromSpinner(action, mAccount);
- if (bundle != null && bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
- mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
- bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
+ if (bundle != null) {
+ if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
+ mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
+ bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
+ } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
+ accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
+ mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
+ }
}
if (mReplyFromAccount == null) {
if (mDraft != null) {
@@ -553,7 +561,9 @@
mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
mAccount.name, true, false);
}
+
mFromSpinner.setCurrentAccount(mReplyFromAccount);
+
if (mFromSpinner.getCount() > 1) {
// If there is only 1 account, just show that account.
// Otherwise, give the user the ability to choose which account to
@@ -1032,7 +1042,9 @@
private void showCcBcc(Bundle state) {
if (state != null && state.containsKey(EXTRA_SHOW_CC_BCC)) {
boolean show = state.getBoolean(EXTRA_SHOW_CC_BCC);
- mCcBccView.show(false, show, show);
+ if (show) {
+ mCcBccView.show(false, show, show);
+ }
}
}
diff --git a/src/com/android/mail/compose/FromAddressSpinner.java b/src/com/android/mail/compose/FromAddressSpinner.java
index 2abafd6..ec8cd11 100644
--- a/src/com/android/mail/compose/FromAddressSpinner.java
+++ b/src/com/android/mail/compose/FromAddressSpinner.java
@@ -71,6 +71,17 @@
}
}
+ public ReplyFromAccount getMatchingReplyFromAccount(String accountString) {
+ if (!TextUtils.isEmpty(accountString)) {
+ for (ReplyFromAccount acct : mReplyFromAccounts) {
+ if (accountString.equals(acct.name)) {
+ return acct;
+ }
+ }
+ }
+ return null;
+ }
+
public ReplyFromAccount getCurrentAccount() {
return mAccount;
}
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index ae5bf13..a0eff82 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -45,11 +45,14 @@
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
import android.widget.Toast;
import com.android.mail.ConversationListContext;
import com.android.mail.R;
import com.android.mail.browse.ConversationCursor;
+import com.android.mail.browse.ConversationCursor.ConversationListener;
import com.android.mail.browse.SelectedConversationsActionMenu;
import com.android.mail.compose.ComposeActivity;
import com.android.mail.providers.Account;
@@ -73,6 +76,8 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
/**
@@ -91,7 +96,8 @@
* In the Gmail codebase, this was called BaseActivityController
* </p>
*/
-public abstract class AbstractActivityController implements ActivityController {
+public abstract class AbstractActivityController implements ActivityController,
+ ConversationListener, OnScrollListener {
// Keys for serialization of various information in Bundles.
private static final String SAVED_ACCOUNT = "saved-account";
private static final String SAVED_FOLDER = "saved-folder";
@@ -108,6 +114,8 @@
/** Tag used when loading a folder list fragment. */
protected static final String TAG_FOLDER_LIST = "tag-folder-list";
+ private static final long CONVERSATION_LIST_THROTTLE_MS = 4000L;
+
/** Are we on a tablet device or not. */
public final boolean IS_TABLET_DEVICE;
@@ -138,6 +146,12 @@
private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
protected Settings mCachedSettings;
protected ConversationCursor mConversationListCursor;
+ protected boolean mConversationListenerAdded = false;
+
+ private boolean mIsConversationListScrolling = false;
+ private long mConversationListRefreshTime = 0;
+ private Timer mConversationListTimer = new Timer();
+ private RefreshTimerTask mConversationListRefreshTask;
/**
* Selected conversations, if any.
@@ -254,18 +268,6 @@
}
return null;
}
- /**
- * Get the animated adapter for this activity. If the conversation list fragment
- * is not attached, this method returns null
- * @return
- */
- protected AnimatedAdapter getConversationListAdapter() {
- final Fragment fragment = getConversationListFragment();
- if (fragment != null) {
- return ((ConversationListFragment) fragment).getAnimatedAdapter();
- }
- return null;
- }
/**
* Returns the conversation view fragment attached with this activity. If no such fragment
@@ -328,6 +330,7 @@
// Current account is different from the new account, restart loaders and show
// the account Inbox.
mAccount = account;
+ cancelRefreshTask();
onSettingsChanged(mAccount.settings);
mActionBarView.setAccount(mAccount);
loadAccountInbox();
@@ -408,6 +411,7 @@
// the list is shown to the user, this could fire in one pane if the user goes directly
// to a conversation
updateRecentFolderList();
+ cancelRefreshTask();
}
}
@@ -1004,7 +1008,6 @@
protected void setCurrentConversation(Conversation conversation) {
mCurrentConversation = conversation;
mTracker.initialize(mCurrentConversation);
- ConversationCursor.setTracker(mTracker);
}
/**
@@ -1411,6 +1414,126 @@
protected abstract DestructiveActionListener getFolderDestructiveActionListener();
@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.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRefreshReady() {
+ final ArrayList<Integer> 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();
+ }
+
+ 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() {
}
@@ -1454,18 +1577,11 @@
}
}
- private void checkRefreshConversationList() {
- if (mConversationListCursor != null && mConversationListCursor.isRefreshReady()) {
- AnimatedAdapter adapter = getConversationListAdapter();
- if (adapter != null) {
- adapter.onRefreshReady();
- }
- }
- }
-
@Override
public void onActionComplete() {
- checkRefreshConversationList();
+ if (getConversationListCursor().isRefreshReady()) {
+ refreshAdapter();
+ }
}
@Override
@@ -1572,13 +1688,24 @@
mConversationListCursor = data;
// Call the method that updates things when values in the cursor change
- checkRefreshConversationList();
+ if (mConversationListCursor.isRefreshReady()) {
+ onRefreshReady();
+ }
// Register the AbstractActivityController as a listener to changes in
// data in the cursor.
final ConversationListFragment convList = getConversationListFragment();
if (convList != null) {
convList.onCursorUpdated();
+ if (!mConversationListenerAdded) {
+ // TODO(mindyp): when we move to the cursor loader, we need
+ // to add/remove the listener when we create/ destroy loaders.
+ mConversationListCursor
+ .addListener(AbstractActivityController.this);
+ convList.getListView().setOnScrollListener(
+ AbstractActivityController.this);
+ mConversationListenerAdded = true;
+ }
}
// Shown for search results in two-pane mode only.
if (shouldShowFirstConversation()) {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index a68c15b..0172109 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -24,21 +24,20 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.OnScrollListener;
import android.widget.SimpleCursorAdapter;
import com.android.mail.R;
import com.android.mail.browse.ConversationCursor;
-import com.android.mail.browse.ConversationCursor.ConversationListener;
import com.android.mail.browse.ConversationItemView;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.Settings;
import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.SwipeableListView.SwipeCompleteListener;
import com.android.mail.ui.UndoBarView.OnUndoCancelListener;
import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,8 +45,7 @@
import java.util.HashSet;
public class AnimatedAdapter extends SimpleCursorAdapter implements
- android.animation.Animator.AnimatorListener, OnUndoCancelListener, ConversationListener,
- OnScrollListener {
+ android.animation.Animator.AnimatorListener, OnUndoCancelListener {
private final static int TYPE_VIEW_CONVERSATION = 0;
private final static int TYPE_VIEW_DELETING = 1;
private final static int TYPE_VIEW_UNDOING = 2;
@@ -59,6 +57,7 @@
private Context mContext;
private ConversationSelectionSet mBatchConversations;
private ActionCompleteListener mActionCompleteListener;
+ private boolean mUndo = false;
private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>();
private ViewMode mViewMode;
private View mFooter;
@@ -70,10 +69,6 @@
private DragListener mDragListener;
private HashMap<Long, LeaveBehindItem> mLeaveBehindItems = new HashMap<Long, LeaveBehindItem>();
- private ConversationCursor mConversationCursor;
- // TODO: Hook this up (set list view's onScrollListener)
- private boolean mIsConversationListScrolling = false;
-
/**
* Used only for debugging.
*/
@@ -96,10 +91,6 @@
mCachedSettings = settings;
mDragListener = dragListener;
mSwipeEnabled = account.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE);
- mConversationCursor = cursor;
- if (cursor != null) {
- cursor.setAdapter(this);
- }
}
@Override
@@ -453,54 +444,4 @@
mLeaveBehindItems.remove(item.id);
notifyDataSetChanged();
}
-
- @Override
- public void onRefreshRequired() {
- if (mConversationCursor.isRefreshRequired()) {
- mConversationCursor.refresh();
- }
- }
-
- /**
- * Called when the {@link ConversationCursor} is changed or has new data in it.
- * <p>
- * {@inheritDoc}
- */
- @Override
- public void onRefreshReady() {
- // Note: mIsConversationListScrolling will always be false until the listener is hooked up
- if (!mIsConversationListScrolling) {
- // Swap cursors
- mConversationCursor.sync();
- notifyDataSetChanged();
- }
- }
-
- @Override
- public void onDataSetChanged() {
- notifyDataSetChanged();
- }
-
- // TODO: Hook this up (currently not used)
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- if (mConversationCursor != null) {
- boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE);
- if (!isScrolling) {
- if (mConversationCursor.isRefreshRequired()) {
- LogUtils.d(LOG_TAG, "Stop scrolling: refresh");
- mConversationCursor.refresh();
- } else if (mConversationCursor.isRefreshReady()) {
- LogUtils.d(LOG_TAG, "Stop scrolling: try sync");
- onRefreshReady();
- }
- }
- mIsConversationListScrolling = isScrolling;
- }
- }
-
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
- int totalItemCount) {
- }
}
diff --git a/src/com/android/mail/ui/ConversationCursorLoader.java b/src/com/android/mail/ui/ConversationCursorLoader.java
index c50b697..cf25a8e 100644
--- a/src/com/android/mail/ui/ConversationCursorLoader.java
+++ b/src/com/android/mail/ui/ConversationCursorLoader.java
@@ -53,12 +53,12 @@
@Override
protected void onStartLoading() {
forceLoad();
- ConversationCursor.resume();
+ //ConversationCursor.resume();
}
@Override
protected void onStopLoading() {
cancelLoad();
- ConversationCursor.pause();
+ //ConversationCursor.pause();
}
}
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index ba7658e..c8beeb9 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -25,7 +25,6 @@
import android.os.Handler;
import android.os.Parcelable;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
@@ -296,7 +295,8 @@
public void onDestroyView() {
mListSavedState = mListView.onSaveInstanceState();
- // Clear the adapter.
+ // Set a null cursor in the dapter, and clear the list's adapter
+ mListAdapter.swapCursor(null);
mListView.setAdapter(null);
mActivity.unsetViewModeListener(this);