diff options
author | Andy Huang <ath@google.com> | 2014-11-02 20:51:14 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2014-11-02 20:51:14 +0000 |
commit | d4600afbe1725d4323f4fc21b17e327c3aede6a8 (patch) | |
tree | d71f7de238f5f099c63a69bd004647f5b490a66e /src | |
parent | dbace83bed6f02f015b5a7b9267c7a4158ffb6b5 (diff) | |
parent | c9f48dd7d2ca53e330bc41b4a81c130c8e39d498 (diff) | |
download | UnifiedEmail-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')
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. */ |