summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndy Huang <ath@google.com>2014-11-02 20:51:14 +0000
committerAndroid Git Automerger <android-git-automerger@android.com>2014-11-02 20:51:14 +0000
commitd4600afbe1725d4323f4fc21b17e327c3aede6a8 (patch)
treed71f7de238f5f099c63a69bd004647f5b490a66e /src
parentdbace83bed6f02f015b5a7b9267c7a4158ffb6b5 (diff)
parentc9f48dd7d2ca53e330bc41b4a81c130c8e39d498 (diff)
downloadUnifiedEmail-d4600afbe1725d4323f4fc21b17e327c3aede6a8.tar.gz
am c9f48dd7: am 308e06c6: am e35c5abb: am 652b8c69: am 3c8e398e: Merge "Peek mode for 2-pane landscape" into ub-gmail-ur14-dev
* commit 'c9f48dd7d2ca53e330bc41b4a81c130c8e39d498': Peek mode for 2-pane landscape
Diffstat (limited to 'src')
-rw-r--r--src/com/android/mail/browse/ConversationPagerAdapter.java64
-rw-r--r--src/com/android/mail/browse/ConversationPagerController.java23
-rw-r--r--src/com/android/mail/ui/AbstractActivityController.java130
-rw-r--r--src/com/android/mail/ui/AbstractConversationViewFragment.java70
-rw-r--r--src/com/android/mail/ui/ActionBarController.java6
-rw-r--r--src/com/android/mail/ui/ActivityController.java5
-rw-r--r--src/com/android/mail/ui/ConversationListCallbacks.java1
-rw-r--r--src/com/android/mail/ui/ConversationUpdater.java9
-rw-r--r--src/com/android/mail/ui/OnePaneController.java13
-rw-r--r--src/com/android/mail/ui/TwoPaneController.java263
-rw-r--r--src/com/android/mail/utils/FragmentStatePagerAdapter2.java2
-rw-r--r--src/com/android/mail/utils/Utils.java7
12 files changed, 482 insertions, 111 deletions
diff --git a/src/com/android/mail/browse/ConversationPagerAdapter.java b/src/com/android/mail/browse/ConversationPagerAdapter.java
index a6c794b9f..6282ff470 100644
--- a/src/com/android/mail/browse/ConversationPagerAdapter.java
+++ b/src/com/android/mail/browse/ConversationPagerAdapter.java
@@ -19,6 +19,7 @@ package com.android.mail.browse;
import android.app.Fragment;
import android.app.FragmentManager;
+import android.app.FragmentTransaction;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
@@ -37,6 +38,7 @@ import com.android.mail.ui.AbstractConversationViewFragment;
import com.android.mail.ui.ActivityController;
import com.android.mail.ui.ConversationViewFragment;
import com.android.mail.ui.SecureConversationViewFragment;
+import com.android.mail.ui.TwoPaneController;
import com.android.mail.utils.FragmentStatePagerAdapter2;
import com.android.mail.utils.HtmlSanitizer;
import com.android.mail.utils.LogUtils;
@@ -108,15 +110,33 @@ public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
*/
private int mLastKnownCount;
+ /**
+ * Once this adapter is connected to a ViewPager's saved state (from a previous
+ * {@link #saveState()}), this field keeps the state around in case it later needs to be used
+ * to find and kill page fragments.
+ */
+ private Bundle mRestoredState;
+
+ private final FragmentManager mFragmentManager;
+
+ private boolean mPageChangeListenerEnabled;
+
private static final String LOG_TAG = ConversationPagerController.LOG_TAG;
private static final String BUNDLE_DETACHED_MODE =
ConversationPagerAdapter.class.getName() + "-detachedmode";
+ /**
+ * This is the bundle key prefix for the saved pager fragments as stashed by the parent class.
+ * See the implementation of {@link FragmentStatePagerAdapter2#saveState()}. This assumes that
+ * value!!!
+ */
+ private static final String BUNDLE_FRAGMENT_PREFIX = "f";
public ConversationPagerAdapter(Context context, FragmentManager fm, Account account,
Folder folder, Conversation initialConversation) {
super(fm, false /* enableSavedStates */);
mContext = context;
+ mFragmentManager = fm;
mCommonFragmentArgs = AbstractConversationViewFragment.makeBasicArgs(account);
mInitialConversation = initialConversation;
mAccount = account;
@@ -282,10 +302,46 @@ public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
b.setClassLoader(loader);
final boolean detached = b.getBoolean(BUNDLE_DETACHED_MODE);
setDetachedMode(detached);
+
+ // save off the bundle in case it later needs to be consulted for fragments-to-kill
+ mRestoredState = b;
}
LogUtils.d(LOG_TAG, "OUT PagerAdapter.restoreState. this=%s", this);
}
+ /**
+ * Part of an inelegant dance to clean up restored fragments after realizing
+ * we don't want the ViewPager around after all in 2-pane. See docs for
+ * {@link ConversationPagerController#killRestoredFragments()} and
+ * {@link TwoPaneController#restoreConversation}.
+ */
+ public void killRestoredFragments() {
+ if (mRestoredState == null) {
+ return;
+ }
+
+ FragmentTransaction ft = null;
+ for (String key : mRestoredState.keySet()) {
+ // WARNING: this code assumes implementation details in
+ // FragmentStatePagerAdapter2#restoreState
+ if (!key.startsWith(BUNDLE_FRAGMENT_PREFIX)) {
+ continue;
+ }
+ final Fragment f = mFragmentManager.getFragment(mRestoredState, key);
+ if (f != null) {
+ if (ft == null) {
+ ft = mFragmentManager.beginTransaction();
+ }
+ ft.remove(f);
+ }
+ }
+ if (ft != null) {
+ ft.commitAllowingStateLoss();
+ mFragmentManager.executePendingTransactions();
+ }
+ mRestoredState = null;
+ }
+
private void setDetachedMode(boolean detached) {
if (mDetachedMode == detached) {
return;
@@ -481,6 +537,10 @@ public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
LogUtils.d(LOG_TAG, "CPA.stopListening, this=%s", this);
}
+ public void enablePageChangeListener(boolean enable) {
+ mPageChangeListenerEnabled = enable;
+ }
+
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// no-op
@@ -488,7 +548,7 @@ public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
@Override
public void onPageSelected(int position) {
- if (mController == null) {
+ if (mController == null || !mPageChangeListenerEnabled) {
return;
}
final ConversationCursor cursor = getCursor();
@@ -499,7 +559,7 @@ public class ConversationPagerAdapter extends FragmentStatePagerAdapter2
final Conversation c = cursor.getConversation();
c.position = position;
LogUtils.d(LOG_TAG, "pager adapter setting current conv: %s", c);
- mController.setCurrentConversation(c);
+ mController.onConversationViewSwitched(c);
}
@Override
diff --git a/src/com/android/mail/browse/ConversationPagerController.java b/src/com/android/mail/browse/ConversationPagerController.java
index 016f12421..0daee9bb6 100644
--- a/src/com/android/mail/browse/ConversationPagerController.java
+++ b/src/com/android/mail/browse/ConversationPagerController.java
@@ -120,7 +120,7 @@ public class ConversationPagerController {
&& !mPagerAdapter.isDetached()) {
final int pos = mPagerAdapter.getConversationPosition(initialConversation);
if (pos >= 0) {
- mPager.setCurrentItem(pos);
+ setCurrentItem(pos);
return;
}
}
@@ -165,7 +165,7 @@ public class ConversationPagerController {
final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
if (initialPos >= 0) {
LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
- mPager.setCurrentItem(initialPos);
+ setCurrentItem(initialPos);
}
}
Utils.sConvLoadTimer.mark("pager setAdapter");
@@ -196,11 +196,30 @@ public class ConversationPagerController {
cleanup();
}
+ /**
+ * Part of a delicate dance to kill fragments on restore after rotation if
+ * the device configuration no longer calls for them. You must call
+ * {@link #show(Account, Folder, Conversation, boolean, boolean)} first, and you probably want
+ * to call {@link #hide(boolean)} afterwards to finish the cleanup. See go/xqaxk. Sorry...
+ *
+ */
+ public void killRestoredFragments() {
+ mPagerAdapter.killRestoredFragments();
+ }
+
// Explicitly set the focus to the conversation pager, specifically the conv overlay.
public void focusPager() {
mPager.requestFocus();
}
+ private void setCurrentItem(int pos) {
+ // disable onPageSelected notifications during this operation. that listener is only there
+ // to update the rest of the app when the user swipes to another page.
+ mPagerAdapter.enablePageChangeListener(false);
+ mPager.setCurrentItem(pos);
+ mPagerAdapter.enablePageChangeListener(true);
+ }
+
public boolean isInitialConversationLoading() {
return mInitialConversationLoading;
}
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index fb7733049..ff97f586e 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -70,6 +70,7 @@ import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationCursor.ConversationOperation;
import com.android.mail.browse.ConversationItemViewModel;
import com.android.mail.browse.ConversationMessage;
+import com.android.mail.browse.ConversationPagerAdapter;
import com.android.mail.browse.ConversationPagerController;
import com.android.mail.browse.SelectedConversationsActionMenu;
import com.android.mail.browse.SyncErrorDialogFragment;
@@ -464,7 +465,7 @@ public abstract class AbstractActivityController implements ActivityController,
/** A wait fragment we added, if any. */
private WaitFragment mWaitFragment;
/** True if we have results from a search query */
- private boolean mHaveSearchResults = false;
+ protected boolean mHaveSearchResults = false;
/** If a confirmation dialog is being show, the listener for the positive action. */
private OnClickListener mDialogListener;
/**
@@ -537,6 +538,19 @@ public abstract class AbstractActivityController implements ActivityController,
mConversationListLoadFinishedIgnored = false;
}
+ @Override
+ public final String toString() {
+ final StringBuilder sb = new StringBuilder(super.toString());
+ sb.append("{");
+ sb.append("mCurrentConversation=");
+ sb.append(mCurrentConversation);
+ appendToString(sb);
+ sb.append("}");
+ return sb.toString();
+ }
+
+ protected void appendToString(StringBuilder sb) {}
+
public Account getCurrentAccount() {
return mAccount;
}
@@ -1279,7 +1293,7 @@ public abstract class AbstractActivityController implements ActivityController,
* {@inheritDoc}
*/
@Override
- public boolean onCreate(Bundle savedState) {
+ public void onCreate(Bundle savedState) {
initializeActionBar();
initializeDevLoggingService();
// Allow shortcut keys to function for the ActionBar and menus.
@@ -1351,7 +1365,6 @@ public abstract class AbstractActivityController implements ActivityController,
// Create the accounts loader; this loads the account switch spinner.
mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY,
mAccountCallbacks);
- return true;
}
/**
@@ -1756,9 +1769,7 @@ public abstract class AbstractActivityController implements ActivityController,
@Override
public void markConversationMessagesUnread(final Conversation conv,
final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
- // The only caller of this method is the conversation view, from where marking unread should
- // *always* take you back to list mode.
- showConversation(null);
+ onPreMarkUnread();
// locally mark conversation unread (the provider is supposed to propagate message unread
// to conversation unread)
@@ -1779,6 +1790,18 @@ public abstract class AbstractActivityController implements ActivityController,
}
}
+ /**
+ * Hook to do stuff before actually marking a conversation unread (only called from within
+ * conversation view). Most configurations do the default behavior of popping out of
+ * CV to go back to TL.
+ *
+ */
+ protected void onPreMarkUnread() {
+ // The only caller of this method is the conversation view, from where marking unread should
+ // take you back to list mode in most cases. Two-pane view is the exception.
+ showConversation(null);
+ }
+
private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
byte[] originalConversationInfo) {
// Only do a granular 'mark unread' if a subset of messages are unread
@@ -1836,6 +1859,24 @@ public abstract class AbstractActivityController implements ActivityController,
}
}
+ /**
+ * Mark a single conversation 'seen', which is a combination of 'viewed' and 'read'. In some
+ * configurations (peek mode), this operation may be prevented and the method will return false.
+ *
+ * @param conv the conversation to mark seen
+ * @return true if the operation was a success
+ */
+ @Override
+ public boolean markConversationSeen(Conversation conv) {
+ if (isCurrentConversationJustPeeking()) {
+ LogUtils.i(LOG_TAG, "AAC is in peek mode, not marking seen. conv=%s", conv);
+ return false;
+ } else {
+ markConversationsRead(Arrays.asList(conv), true /* read */, true /* viewed */);
+ return true;
+ }
+ }
+
@Override
public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
final boolean viewed) {
@@ -1964,18 +2005,30 @@ public abstract class AbstractActivityController implements ActivityController,
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);
+ doShowNextConversation(target, autoAdvance);
return (mAutoAdvanceOp == null);
}
return true;
}
+ /**
+ * Do the actual work of selecting a next conversation to show and showing it. Two-pane
+ * overrides this in landscape to prefer peeking rather than staring at an empty CV pane when
+ * auto-advance=LIST.
+ *
+ * @param target conversations being destroyed, of which the current convo is one
+ * @param autoAdvance auto-advance pref value
+ */
+ protected void doShowNextConversation(Collection<Conversation> target, int autoAdvance) {
+ final Conversation next = mTracker.getNextConversation(autoAdvance, target);
+ LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
+ showConversation(next);
+ }
+
@Override
public void starMessage(ConversationMessage msg, boolean starred) {
if (msg.starred == starred) {
@@ -2105,8 +2158,8 @@ public abstract class AbstractActivityController implements ActivityController,
}
@Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- return mActionBarController.onPrepareOptionsMenu(menu);
+ public void onPrepareOptionsMenu(Menu menu) {
+ mActionBarController.onPrepareOptionsMenu(menu);
}
@Override
@@ -2348,12 +2401,7 @@ public abstract class AbstractActivityController implements ActivityController,
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);
+ restoreConversation(conversation);
}
if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
@@ -2480,7 +2528,7 @@ public abstract class AbstractActivityController implements ActivityController,
* Returns true if we should enter conversation mode with search.
*/
protected final boolean shouldEnterSearchConvMode() {
- return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
+ return mHaveSearchResults && shouldShowFirstConversation();
}
/**
@@ -2503,6 +2551,15 @@ public abstract class AbstractActivityController implements ActivityController,
mCheckedSet.putAll(selectedSet);
}
+ protected void restoreConversation(Conversation 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);
+ }
+
/**
* 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.
@@ -2660,6 +2717,22 @@ public abstract class AbstractActivityController implements ActivityController,
}
/**
+ * Invoked by {@link ConversationPagerAdapter} when a new page in the ViewPager is selected.
+ *
+ * @param conversation the conversation of the now currently visible fragment
+ *
+ */
+ @Override
+ public void onConversationViewSwitched(Conversation conversation) {
+ setCurrentConversation(conversation);
+ }
+
+ @Override
+ public boolean isCurrentConversationJustPeeking() {
+ return false;
+ }
+
+ /**
* {@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
@@ -3122,7 +3195,7 @@ public abstract class AbstractActivityController implements ActivityController,
mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
}
mTracker.onCursorUpdated();
- perhapsShowFirstSearchResult();
+ perhapsShowFirstConversation();
}
@Override
@@ -3374,7 +3447,7 @@ public abstract class AbstractActivityController implements ActivityController,
// check and inform the cursor of the change in visibility here.
informCursorVisiblity(true);
}
- perhapsShowFirstSearchResult();
+ perhapsShowFirstConversation();
}
@Override
@@ -3673,20 +3746,11 @@ public abstract class AbstractActivityController implements ActivityController,
/**
* Updates controller state based on search results and shows first conversation if required.
+ * Be sure to call the super-implementation if overriding.
*/
- 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 */);
- }
+ protected void perhapsShowFirstConversation() {
+ mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
+ && mConversationListCursor.getCount() > 0;
}
/**
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 86e54ab15..9332c7bfb 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -164,6 +164,50 @@ public abstract class AbstractConversationViewFragment extends Fragment implemen
}
/**
+ * Marks a conversation either 'seen' (force=false), as in when the conversation is made visible
+ * and should be marked read, or 'read' (force=true), as in when the action bar menu item to
+ * mark this conversation read is selected.
+ *
+ * @param force true to force marking it read, false to allow peek mode to prevent it
+ */
+ private final void markRead(boolean force) {
+ final ControllableActivity activity = (ControllableActivity) getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ // mark viewed/read if not previously marked viewed by this conversation view,
+ // or if unread messages still exist in the message list cursor
+ // we don't want to keep marking viewed on rotation or restore
+ // but we do want future re-renders to mark read (e.g. "New message from X" case)
+ final MessageCursor cursor = getMessageCursor();
+ LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
+ + "cursor null = %b, cursor.isConversationRead() = %b",
+ mConversation.isViewed(), cursor == null,
+ cursor != null && cursor.isConversationRead());
+ if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
+ // Mark the conversation read no matter what if force=true.
+ // else only mark it seen if appropriate (2-pane peek=true doesn't mark things seen)
+ final boolean convMarkedRead;
+ if (force) {
+ activity.getConversationUpdater()
+ .markConversationsRead(Arrays.asList(mConversation), true /* read */,
+ true /* viewed */);
+ convMarkedRead = true;
+ } else {
+ convMarkedRead = activity.getConversationUpdater()
+ .markConversationSeen(mConversation);
+ }
+
+ // and update the Message objects in the cursor so the next time a cursor update
+ // happens with these messages marked read, we know to ignore it
+ if (convMarkedRead && cursor != null && !cursor.isClosed()) {
+ cursor.markMessagesRead();
+ }
+ }
+ }
+
+ /**
* Subclasses must override this, since they may want to display a single or
* many messages related to this conversation.
*/
@@ -331,6 +375,9 @@ public abstract class AbstractConversationViewFragment extends Fragment implemen
final int itemId = item.getItemId();
if (itemId == R.id.inside_conversation_unread || itemId == R.id.toggle_read_unread) {
markUnread();
+ } else if (itemId == R.id.read) {
+ markRead(true /* force */);
+ mActivity.supportInvalidateOptionsMenu();
} else if (itemId == R.id.show_original) {
showUntransformedConversation();
} else if (itemId == R.id.print_all) {
@@ -554,7 +601,7 @@ public abstract class AbstractConversationViewFragment extends Fragment implemen
mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages());
}
- protected void onConversationSeen() {
+ protected final void onConversationSeen() {
LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
// Ignore unsafe calls made after a fragment is detached from an activity
@@ -580,26 +627,7 @@ public abstract class AbstractConversationViewFragment extends Fragment implemen
// do not want a later mark-read operation to undo this. So we check this variable which
// is set in #markUnread() which suppresses automatic mark-read.
if (!mSuppressMarkingViewed) {
- // mark viewed/read if not previously marked viewed by this conversation view,
- // or if unread messages still exist in the message list cursor
- // we don't want to keep marking viewed on rotation or restore
- // but we do want future re-renders to mark read (e.g. "New message from X" case)
- final MessageCursor cursor = getMessageCursor();
- LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
- + "cursor null = %b, cursor.isConversationRead() = %b",
- mConversation.isViewed(), cursor == null,
- cursor != null && cursor.isConversationRead());
- if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
- // Mark the conversation viewed and read.
- activity.getConversationUpdater()
- .markConversationsRead(Arrays.asList(mConversation), true, true);
-
- // and update the Message objects in the cursor so the next time a cursor update
- // happens with these messages marked read, we know to ignore it
- if (cursor != null && !cursor.isClosed()) {
- cursor.markMessagesRead();
- }
- }
+ markRead(false /* force */);
}
activity.getListHandler().onConversationSeen();
diff --git a/src/com/android/mail/ui/ActionBarController.java b/src/com/android/mail/ui/ActionBarController.java
index 299f720f5..217c52172 100644
--- a/src/com/android/mail/ui/ActionBarController.java
+++ b/src/com/android/mail/ui/ActionBarController.java
@@ -242,7 +242,7 @@ public class ActionBarController implements ViewMode.ModeChangeListener {
|| mController.getConversationListCursor().getCount() > 0));
}
- public boolean onPrepareOptionsMenu(Menu menu) {
+ public void onPrepareOptionsMenu(Menu menu) {
menu.setQwertyMode(true);
// We start out with every option enabled. Based on the current view, we disable actions
// that are possible.
@@ -256,7 +256,7 @@ public class ActionBarController implements ViewMode.ModeChangeListener {
final MenuItem item = menu.getItem(i);
item.setVisible(false);
}
- return false;
+ return;
}
validateVolatileMenuOptionVisibility();
@@ -276,7 +276,7 @@ public class ActionBarController implements ViewMode.ModeChangeListener {
mAccount.supportsSearch() && !mIsOnTablet);
}
- return false;
+ return;
}
/**
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index ac6b518a5..f64b27533 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -93,9 +93,8 @@ public interface ActivityController extends LayoutListener,
*
* @see android.app.Activity#onCreate
* @param savedState
- * @return true if the controller was able to initialize successfully, false otherwise.
*/
- boolean onCreate(Bundle savedState);
+ void onCreate(Bundle savedState);
/**
* @see android.app.Activity#onPostCreate
@@ -174,7 +173,7 @@ public interface ActivityController extends LayoutListener,
* @param menu
* @return
*/
- boolean onPrepareOptionsMenu(Menu menu);
+ void onPrepareOptionsMenu(Menu menu);
/**
* Called by the Mail activity on Activity resume.
diff --git a/src/com/android/mail/ui/ConversationListCallbacks.java b/src/com/android/mail/ui/ConversationListCallbacks.java
index 7def04393..1b6ca1082 100644
--- a/src/com/android/mail/ui/ConversationListCallbacks.java
+++ b/src/com/android/mail/ui/ConversationListCallbacks.java
@@ -60,6 +60,7 @@ public interface ConversationListCallbacks {
Conversation getCurrentConversation();
void setCurrentConversation(Conversation c);
+ void onConversationViewSwitched(Conversation c);
/**
* Returns whether the initial conversation has begun but not finished loading. If this returns
diff --git a/src/com/android/mail/ui/ConversationUpdater.java b/src/com/android/mail/ui/ConversationUpdater.java
index da2204c59..745d7d2a2 100644
--- a/src/com/android/mail/ui/ConversationUpdater.java
+++ b/src/com/android/mail/ui/ConversationUpdater.java
@@ -111,6 +111,15 @@ public interface ConversationUpdater extends ConversationListCallbacks {
byte[] originalConversationInfo);
/**
+ * Mark a single conversation 'seen', which is a combination of 'viewed' and 'read'. In some
+ * configurations (peek mode), this operation may be prevented and the method will return false.
+ *
+ * @param conv the conversation to mark seen
+ * @return true if the operation was a success
+ */
+ boolean markConversationSeen(Conversation conv);
+
+ /**
* Star a single message within a conversation. This method requires a
* {@link ConversationMessage} to propagate the change to the owning {@link Conversation}.
*
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index 582daeeac..ef42ab9b0 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -169,7 +169,7 @@ public final class OnePaneController extends AbstractActivityController {
}
@Override
- public boolean onCreate(Bundle savedInstanceState) {
+ public void onCreate(Bundle savedInstanceState) {
mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
mDrawerContainer.setDrawerTitle(Gravity.START,
mActivity.getActivityContext().getString(R.string.drawer_title));
@@ -182,7 +182,7 @@ public final class OnePaneController extends AbstractActivityController {
mActivity.findViewById(R.id.conversation_pager).setVisibility(View.GONE);
// The parent class sets the correct viewmode and starts the application off.
- return super.onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
}
@Override
@@ -222,12 +222,9 @@ public final class OnePaneController extends AbstractActivityController {
}
@Override
- public String toString() {
- final StringBuilder sb = new StringBuilder(super.toString());
+ protected void appendToString(StringBuilder sb) {
sb.append(" lastConvListTransId=");
sb.append(mLastConversationListTransactionId);
- sb.append("}");
- return sb.toString();
}
@Override
@@ -562,8 +559,4 @@ public final class OnePaneController extends AbstractActivityController {
// Do nothing
}
- @Override
- public boolean isCurrentConversationJustPeeking() {
- return false;
- }
}
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 289590295..7c2795da7 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -27,6 +27,7 @@ import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.v7.app.ActionBar;
import android.view.KeyEvent;
+import android.view.Menu;
import android.view.View;
import android.widget.ImageView;
import android.widget.ListView;
@@ -36,13 +37,16 @@ import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider.AutoAdvance;
import com.android.mail.providers.UIProvider.ConversationListIcon;
import com.android.mail.utils.EmptyStateUtils;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.collect.Lists;
+import java.util.Collection;
import java.util.List;
+import java.util.Objects;
/**
* Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
@@ -54,13 +58,13 @@ public final class TwoPaneController extends AbstractActivityController implemen
private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID =
"saved-miscellaneous-view-transaction-id";
+ private static final String SAVED_PEEK_MODE = "saved-peeking";
+ private static final String SAVED_PEEKING_CONVERSATION = "saved-peeking-conv";
private TwoPaneLayout mLayout;
private ImageView mEmptyCvView;
private List<TwoPaneLayout.ConversationListLayoutListener> mConversationListLayoutListeners =
Lists.newArrayList();
- @Deprecated
- private Conversation mConversationToShow;
/**
* 2-pane, in wider configurations, allows peeking at a conversation view without having the
@@ -71,12 +75,28 @@ public final class TwoPaneController extends AbstractActivityController implemen
* conversation, peeking is implied (in certain view configurations) and this value is
* meaningless.
*/
- // TODO: save in instance state
private boolean mCurrentConversationJustPeeking;
- // For peeking conversations, we'll put it in a separate runnable.
- private static final int PEEK_CONVERSATION_DELAY_MS = 500;
- private final Runnable mPeekConversationRunnable = new Runnable() {
+ /**
+ * When rotating from land->port->back to land while peeking at a conversation, typically we
+ * would lose the pointer to the conversation being seen in portrait (because in port, we're in
+ * TL mode so conv=null). This is bad if we ever want to go back to landscape, since the user
+ * expectation is that the original peek conversation should appear.
+ * <br>
+ * <p>So save the previous peeking conversation (if any) when restoring in portrait so that a
+ * future landscape restore can load it up.
+ */
+ private Conversation mSavedPeekingConversation;
+
+ /**
+ * The conversation to show (and any extra information about its presentation, like how it was
+ * triggered). Kept here during a transition animation to take effect afterwards.
+ */
+ private ToShow mToShow;
+
+ // For keyboard-focused conversations, we'll put it in a separate runnable.
+ private static final int FOCUSED_CONVERSATION_DELAY_MS = 500;
+ private final Runnable mFocusedConversationRunnable = new Runnable() {
@Override
public void run() {
if (!mActivity.isFinishing()) {
@@ -98,6 +118,20 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
+ protected void appendToString(StringBuilder sb) {
+ sb.append(" mPeeking=");
+ sb.append(mCurrentConversationJustPeeking);
+ sb.append(" mSavedPeekConv=");
+ sb.append(mSavedPeekingConversation);
+ if (mToShow != null) {
+ sb.append(" mToShow.conv=");
+ sb.append(mToShow.conversation);
+ sb.append(" mToShow.dueToKeyboard=");
+ sb.append(mToShow.dueToKeyboard);
+ }
+ }
+
+ @Override
public boolean isCurrentConversationJustPeeking() {
return mCurrentConversationJustPeeking;
}
@@ -169,13 +203,13 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
- public boolean onCreate(Bundle savedState) {
+ public void onCreate(Bundle savedState) {
mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
mEmptyCvView = (ImageView) mActivity.findViewById(R.id.conversation_pane_no_message_view);
if (mLayout == null) {
// We need the layout for everything. Crash/Return early if it is null.
LogUtils.wtf(LOG_TAG, "mLayout is null!");
- return false;
+ return;
}
mLayout.setController(this);
mActivity.getWindow().setBackgroundDrawable(null);
@@ -196,13 +230,21 @@ public final class TwoPaneController extends AbstractActivityController implemen
// (onConversationVisibilityChanged, onConversationListVisibilityChanged)
mViewMode.addListener(mLayout);
- return super.onCreate(savedState);
+ super.onCreate(savedState);
+
+ // Restore peek-related state *after* the super-implementation naively restores view mode.
+ if (savedState != null) {
+ mCurrentConversationJustPeeking = savedState.getBoolean(SAVED_PEEK_MODE,
+ false /* defaultValue */);
+ mSavedPeekingConversation = savedState.getParcelable(SAVED_PEEKING_CONVERSATION);
+ // do the remaining restore work in restoreConversation()
+ }
}
@Override
public void onDestroy() {
super.onDestroy();
- mHandler.removeCallbacks(mPeekConversationRunnable);
+ mHandler.removeCallbacks(mFocusedConversationRunnable);
}
@Override
@@ -211,6 +253,8 @@ public final class TwoPaneController extends AbstractActivityController implemen
outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
+ outState.putBoolean(SAVED_PEEK_MODE, mCurrentConversationJustPeeking);
+ outState.putParcelable(SAVED_PEEKING_CONVERSATION, mSavedPeekingConversation);
}
@Override
@@ -222,6 +266,41 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
+ protected void restoreConversation(Conversation conversation) {
+ // When handling restoration as part of rotation, if the destination orientation doesn't
+ // support peek (i.e. portrait), remap the view mode to list-mode if previously peeking.
+ // We still want to keep the peek state around in case the user rotates back to
+ // landscape, in which case the app should remember that peek mode was on and which
+ // conversation to peek at.
+ if (mCurrentConversationJustPeeking && !mIsTabletLandscape
+ && mViewMode.isConversationMode()) {
+ LogUtils.i(LOG_TAG, "restoring peek to port orientation");
+
+ // Restore the pager saved state, extract the Fragments out of it, kill each one
+ // manually, and finally tear down the pager and go back to the list.
+ //
+ // Need to tear down the restored CV fragments or else they will leak since the
+ // fragment manager will have a reference to them but nobody else does.
+ // normally, CPC.show() connects the new pager to the restored fragments, so a future
+ // CPC.hide() correctly clears them.
+
+ mPagerController.show(mAccount, mFolder, conversation, false /* changeVisibility */,
+ null /* pagerAnimationListener */);
+ mPagerController.killRestoredFragments();
+ mPagerController.hide(false /* changeVisibility */);
+
+ // but first, save off the conversation in a separate slot for later restoration if
+ // we then end up back in peek mode
+ mSavedPeekingConversation = conversation;
+
+ mViewMode.enterConversationListMode();
+
+ return;
+ }
+ super.restoreConversation(conversation);
+ }
+
+ @Override
public void switchToDefaultInboxOrChangeAccount(Account account) {
if (mViewMode.isSearchMode()) {
// We are in an activity on top of the main navigation activity.
@@ -317,6 +396,21 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ if (mCurrentConversation != null) {
+ if (mCurrentConversationJustPeeking) {
+ Utils.setMenuItemPresent(menu, R.id.read, !mCurrentConversation.read);
+ Utils.setMenuItemPresent(menu, R.id.inside_conversation_unread,
+ mCurrentConversation.read);
+ } else {
+ // in normal conv mode, always hide the extra 'mark-read' item
+ Utils.setMenuItemPresent(menu, R.id.read, false);
+ }
+ }
+ }
+
+ @Override
public void onViewModeChanged(int newMode) {
if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
final FragmentManager fragmentManager = mActivity.getFragmentManager();
@@ -351,10 +445,10 @@ public final class TwoPaneController extends AbstractActivityController implemen
super.onConversationVisibilityChanged(visible);
if (!visible) {
mPagerController.hide(false /* changeVisibility */);
- } else if (mConversationToShow != null) {
- if (mCurrentConversationJustPeeking) {
- mHandler.removeCallbacks(mPeekConversationRunnable);
- mHandler.postDelayed(mPeekConversationRunnable, PEEK_CONVERSATION_DELAY_MS);
+ } else if (mToShow != null) {
+ if (mToShow.dueToKeyboard) {
+ mHandler.removeCallbacks(mFocusedConversationRunnable);
+ mHandler.postDelayed(mFocusedConversationRunnable, FOCUSED_CONVERSATION_DELAY_MS);
} else {
showCurrentConversationInPager();
}
@@ -367,10 +461,10 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
private void showCurrentConversationInPager() {
- if (mConversationToShow != null) {
- mPagerController.show(mAccount, mFolder, mConversationToShow,
+ if (mToShow != null) {
+ mPagerController.show(mAccount, mFolder, mToShow.conversation,
false /* changeVisibility */, null /* pagerAnimationListener */);
- mConversationToShow = null;
+ mToShow = null;
}
}
@@ -429,6 +523,21 @@ public final class TwoPaneController extends AbstractActivityController implemen
@Override
protected void showConversationWithPeek(Conversation conversation, boolean peek) {
+ showConversation(conversation, peek, false /* fromKeyboard */);
+ }
+
+ private void showConversation(Conversation conversation, boolean peek, boolean fromKeyboard) {
+ // transition from peek mode to normal mode if we're already peeking at this convo
+ // and this was a request to switch to normal mode
+ if (mViewMode.isConversationMode() && mCurrentConversationJustPeeking && !peek
+ && conversation != null && conversation.equals(mCurrentConversation)) {
+ LogUtils.i(LOG_TAG, "peek->normal: marking current CV seen. conv=%s",
+ mCurrentConversation);
+ mCurrentConversationJustPeeking = false;
+ markConversationSeen(mCurrentConversation);
+ return;
+ }
+
// Make sure that we set the peeking flag before calling super (since some functionality
// in super depends on the flag.
mCurrentConversationJustPeeking = peek;
@@ -441,7 +550,7 @@ public final class TwoPaneController extends AbstractActivityController implemen
return;
}
if (conversation == null) {
- handleBackPress();
+ handleBackPress(true /* preventClose */);
return;
}
// If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
@@ -457,10 +566,10 @@ public final class TwoPaneController extends AbstractActivityController implemen
// When a mode change is required, wait for onConversationVisibilityChanged(), the signal
// that the mode change animation has finished, before rendering the conversation.
- mConversationToShow = conversation;
+ mToShow = new ToShow(conversation, fromKeyboard);
final int mode = mViewMode.getMode();
- LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow);
+ LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mToShow.conversation);
if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
mViewMode.enterSearchResultsConversationMode();
} else {
@@ -486,7 +595,7 @@ public final class TwoPaneController extends AbstractActivityController implemen
@Override
public void onConversationFocused(Conversation conversation) {
if (mIsTabletLandscape) {
- showConversationWithPeek(conversation, true /* peek */);
+ showConversation(conversation, true /* peek */, true /* fromKeyboard */);
}
}
@@ -498,6 +607,11 @@ public final class TwoPaneController extends AbstractActivityController implemen
final long newId = conversation != null ? conversation.id : -1;
final boolean different = oldId != newId;
+ if (different) {
+ LogUtils.i(LOG_TAG, "TPC.setCurrentConv w/ new conv. new=%s old=%s newPeek=%s",
+ conversation, mCurrentConversation, mCurrentConversationJustPeeking);
+ }
+
// This call might change mCurrentConversation.
super.setCurrentConversation(conversation);
@@ -505,6 +619,7 @@ public final class TwoPaneController extends AbstractActivityController implemen
if (convList != null && conversation != null) {
if (mCurrentConversationJustPeeking) {
convList.clearChoicesAndActivated();
+ // TODO: set the list highlight to this new item
} else {
convList.setActivated(conversation.position, different);
}
@@ -512,6 +627,38 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
+ public void onConversationViewSwitched(Conversation conversation) {
+ // swiping on CV to flip through CV pages should reset the peeking flag; the next
+ // conversation should be marked read when visible
+ //
+ // it's also possible to get here when the dataset changes and the current CV is
+ // repositioned in the dataset, so make sure the current conv is actually being switched
+ // before clearing the peek state
+ if (!Objects.equals(conversation, mCurrentConversation)) {
+ LogUtils.i(LOG_TAG, "CPA reported a page change. resetting peek to false. new conv=%s",
+ conversation);
+ mCurrentConversationJustPeeking = false;
+ }
+ super.onConversationViewSwitched(conversation);
+ }
+
+ @Override
+ protected void doShowNextConversation(Collection<Conversation> target, int autoAdvance) {
+ // in portrait, and in landscape when auto-advance is set, do the regular thing
+ if (!isTwoPaneLandscape() || autoAdvance != AutoAdvance.LIST) {
+ super.doShowNextConversation(target, autoAdvance);
+ return;
+ }
+
+ // special case for two-pane landscape with LIST auto-advance: prefer to peek at the
+ // next-oldest conversation instead. showConversation() will resort to an empty CV pane when
+ // destroying the very last conversation.
+ final Conversation next = mTracker.getNextConversation(AutoAdvance.OLDER, target);
+ LogUtils.i(LOG_TAG, "showNextConversation(2P-land): showing %s next.", next);
+ showConversationWithPeek(next, true /* peek */);
+ }
+
+ @Override
protected void showWaitForInitialization() {
super.showWaitForInitialization();
@@ -563,12 +710,16 @@ public final class TwoPaneController extends AbstractActivityController implemen
@Override
public boolean handleBackPress() {
+ return handleBackPress(false /* preventClose */);
+ }
+
+ private boolean handleBackPress(boolean preventClose) {
// Clear any visible undo bars.
mToastBar.hide(false, false /* actionClicked */);
if (isDrawerOpen()) {
toggleDrawerState();
} else {
- popView(false);
+ popView(preventClose);
}
return true;
}
@@ -585,11 +736,15 @@ public final class TwoPaneController extends AbstractActivityController implemen
int mode = mViewMode.getMode();
if (mode == ViewMode.SEARCH_RESULTS_LIST) {
mActivity.finish();
- } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
- // Go to conversation list.
- mViewMode.enterConversationListMode();
- } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
- mViewMode.enterSearchResultsListMode();
+ } else if (ViewMode.isConversationMode(mode) || mViewMode.isAdMode()) {
+ // die if in two-pane landscape and the back button was pressed
+ if (isTwoPaneLandscape() && !preventClose) {
+ mActivity.finish();
+ } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
+ mViewMode.enterSearchResultsListMode();
+ } else {
+ mViewMode.enterConversationListMode();
+ }
} else {
// The Folder List fragment can be null for monkeys where we get a back before the
// folder list has had a chance to initialize.
@@ -612,9 +767,43 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
@Override
+ protected void onPreMarkUnread() {
+ // stay in CV when marking unread in two-pane mode
+ if (isTwoPaneLandscape()) {
+ // TODO: need to update the list item state to switch from activated to peeking
+ mCurrentConversationJustPeeking = true;
+ mActivity.supportInvalidateOptionsMenu();
+ } else {
+ super.onPreMarkUnread();
+ }
+ }
+
+ @Override
+ protected void perhapsShowFirstConversation() {
+ super.perhapsShowFirstConversation();
+ if (mCurrentConversation == null && isTwoPaneLandscape()
+ && mConversationListCursor.getCount() > 0) {
+ final Conversation conv;
+
+ // restore the saved peeking conversation if present from the previous rotation
+ if (mCurrentConversationJustPeeking && mSavedPeekingConversation != null) {
+ conv = mSavedPeekingConversation;
+ mSavedPeekingConversation = null;
+ LogUtils.i(LOG_TAG, "peeking at saved conv=%s", conv);
+ } else {
+ mConversationListCursor.moveToPosition(0);
+ conv = mConversationListCursor.getConversation();
+ conv.position = 0;
+ LogUtils.i(LOG_TAG, "peeking at default/zeroth conv=%s", conv);
+ }
+
+ showConversationWithPeek(conv, true /* peek */);
+ }
+ }
+
+ @Override
public boolean shouldShowFirstConversation() {
- return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
- && shouldEnterSearchConvMode();
+ return mLayout.shouldShowPreviewPanel();
}
@Override
@@ -744,4 +933,20 @@ public final class TwoPaneController extends AbstractActivityController implemen
}
return false;
}
+
+ /**
+ * The conversation to show (and other associated bits) when performing a TL->CV transition.
+ *
+ */
+ private static class ToShow {
+ public final Conversation conversation;
+ public final boolean dueToKeyboard;
+
+ public ToShow(Conversation c, boolean fromKeyboard) {
+ conversation = c;
+ dueToKeyboard = fromKeyboard;
+ }
+
+ }
+
}
diff --git a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
index a8cba208e..752e127b3 100644
--- a/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
+++ b/src/com/android/mail/utils/FragmentStatePagerAdapter2.java
@@ -43,7 +43,7 @@ import java.util.ArrayList;
* </ul>
*/
public abstract class FragmentStatePagerAdapter2 extends PagerAdapter {
- private static final String TAG = "FragmentStatePagerAdapter";
+ private static final String TAG = "FSPA"; // the support lib's tag is too long and crashes :)
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index 6c18da994..0bfec5858 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -918,13 +918,6 @@ public class Utils {
}
/**
- * @return whether to show two pane or single pane search results.
- */
- public static boolean showTwoPaneSearchResults(Context context) {
- return context.getResources().getBoolean(R.bool.show_two_pane_search_results);
- }
-
- /**
* Sets the layer type of a view to hardware if the view is attached and hardware acceleration
* is enabled. Does nothing otherwise.
*/