Back navigation on 1 pane

- Allow going back from the message view to the message list with restoring
  all the state on the message list. (batch selection and scroll position)

- Also make "back" work for the message list <-> mailbox list navigation,
  but only 1 level at most.
  (Only the system back key works for this; the action bar home icon will
   not.)

- As discussed offline, it uses our custum "back stack" (which can hold
  at most 1 fragment) using the new fragment APIs, attach and detach.

- Removed commitFragmentTransaction() from the base class, as now there's
  nothing really in common between the two UI controllers in terms of how
  they use the fragment transaction.

Change-Id: Id626ce99beb1f4dceb999dc04bf7d3e5d57a8198
This commit is contained in:
Makoto Onuki 2011-06-07 17:55:06 -07:00
parent 4431a74be5
commit e06e122441
3 changed files with 232 additions and 57 deletions

View File

@ -41,12 +41,11 @@ import java.util.List;
/**
* Base class for the UI controller.
*
* Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
* so that we can easily switch between synchronous and asynchronous transactions.
*/
abstract class UIControllerBase implements MailboxListFragment.Callback,
MessageListFragment.Callback, MessageViewFragment.Callback {
static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE
protected static final String BUNDLE_KEY_RESUME_INBOX_LOOKUP
= "UIController.state.resumeInboxLookup";
protected static final String BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID
@ -124,6 +123,9 @@ abstract class UIControllerBase implements MailboxListFragment.Callback,
mActivity = activity;
mRefreshManager = RefreshManager.getInstance(mActivity);
mActionBarController = createActionBarController(activity);
if (DEBUG_FRAGMENTS) {
FragmentManager.enableDebugLogging(true);
}
}
/**
@ -318,7 +320,7 @@ abstract class UIControllerBase implements MailboxListFragment.Callback,
*
* Do nothing if {@code fragment} is null.
*/
private void removeFragment(FragmentTransaction ft, Fragment fragment) {
protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
}
@ -332,7 +334,8 @@ abstract class UIControllerBase implements MailboxListFragment.Callback,
}
/**
* Remove a {@link Fragment} from {@link #mRemovedFragments}.
* Remove a {@link Fragment} from {@link #mRemovedFragments}. No-op if {@code fragment} is
* null.
*
* {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
* {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
@ -401,15 +404,6 @@ abstract class UIControllerBase implements MailboxListFragment.Callback,
return mMessageViewFragment;
}
/**
* Commit a {@link FragmentTransaction}.
* Subclass may override this and optionally call
* {@link FragmentManager#executePendingTransactions}.
*/
protected void commitFragmentTransaction(FragmentTransaction ft) {
ft.commit();
}
/**
* @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
*
@ -665,4 +659,9 @@ abstract class UIControllerBase implements MailboxListFragment.Callback,
}
mActivity.invalidateOptionsMenu();
}
@Override
public String toString() {
return getClass().getSimpleName(); // Shown on logcat
}
}

View File

@ -31,9 +31,6 @@ import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.Set;
@ -41,22 +38,22 @@ import java.util.Set;
/**
* UI Controller for non x-large devices. Supports a single-pane layout.
*
* One one-pane, multiple fragments can be installed at the same time, but only one of them
* can be "visible" at a time. Others are in the back stack. Use {@link #isMailboxListVisible()},
* {@link #isMessageListVisible()} and {@link #isMessageViewVisible()} to determine which is
* visible.
* One one-pane, only at most one fragment can be installed at a time.
*
* Note due to the asynchronous nature of the fragment transaction, there is a window when
* there is no installed or visible fragments.
*
* TODO Use the back stack for the message list -> message view navigation, so that the list
* position/selection will be restored on back.
*
* Major TODOs
* - TODO Newer/Older for message view with swipe!
* - TODO Implement callbacks
*/
class UIControllerOnePane extends UIControllerBase {
private static final String BUNDLE_KEY_PREVIOUS_FRAGMENT
= "UIControllerOnePane.PREVIOUS_FRAGMENT";
// Our custom poor-man's back stack which has only one entry at maximum.
private Fragment mPreviousFragment;
// MailboxListFragment.Callback
@Override
public void onAccountSelected(long accountId) {
@ -280,11 +277,17 @@ class UIControllerOnePane extends UIControllerBase {
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mPreviousFragment != null) {
mActivity.getFragmentManager().putFragment(outState,
BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
}
}
@Override
public void restoreInstanceState(Bundle savedInstanceState) {
super.restoreInstanceState(savedInstanceState);
mPreviousFragment = mActivity.getFragmentManager().getFragment(savedInstanceState,
BUNDLE_KEY_PREVIOUS_FRAGMENT);
}
@Override
@ -390,20 +393,46 @@ class UIControllerOnePane extends UIControllerBase {
@Override
public boolean onBackPressed(boolean isSystemBackKey) {
if (isMessageViewVisible()) {
if (Email.DEBUG) {
// This is VERY important -- no check for DEBUG_LIFECYCLE
Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
}
// If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
}
return true;
}
// Custom back stack
if (shouldPopFromBackStack(isSystemBackKey)) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
}
popFromBackStack();
return true;
}
// No entry in the back stack.
// If the message view is shown, show the "parent" message list.
// This happens when we get a deep link to a message. (e.g. from a widget)
if (isMessageViewInstalled()) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
}
openMailbox(getMessageViewFragment().getOpenerAccountId(),
getMessageViewFragment().getOpenerMailboxId());
return true;
} else if (isMailboxListVisible() && getMailboxListFragment().navigateUp()) {
return true;
}
return false;
}
@Override
public void open(final long accountId, final long mailboxId, final long messageId) {
if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
Log.d(Logging.LOG_TAG, this + " open accountId=" + accountId
if (Email.DEBUG) {
// This is VERY important -- no check for DEBUG_LIFECYCLE
Log.i(Logging.LOG_TAG, this + " open accountId=" + accountId
+ " mailboxId=" + mailboxId + " messageId=" + messageId);
}
if (accountId == Account.NO_ACCOUNT) {
@ -415,58 +444,192 @@ class UIControllerOnePane extends UIControllerBase {
return;
}
final boolean accountChanging = (getUIAccountId() != accountId);
if (messageId != Message.NO_MESSAGE) {
showMessageView(accountId, mailboxId, messageId);
showMessageView(accountId, mailboxId, messageId, accountChanging);
} else if (mailboxId != Mailbox.NO_MAILBOX) {
showMessageList(accountId, mailboxId);
showMessageList(accountId, mailboxId, accountChanging);
} else {
// Mailbox not specified. Open Inbox or Combined Inbox.
if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
showMessageList(accountId, Mailbox.QUERY_ALL_INBOXES);
showMessageList(accountId, Mailbox.QUERY_ALL_INBOXES, accountChanging);
} else {
startInboxLookup(accountId);
}
}
}
private void removeAllFragments(FragmentTransaction ft) {
/**
* @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
* exists.
*/
private Fragment getInstalledFragment() {
if (isMailboxListInstalled()) {
removeMailboxListFragment(ft);
return getMailboxListFragment();
} else if (isMessageListInstalled()) {
return getMessageListFragment();
} else if (isMessageViewInstalled()) {
return getMessageViewFragment();
}
if (isMessageListInstalled()) {
removeMessageListFragment(ft);
return null;
}
/**
* Remove currently installed {@link Fragment} (1-pane has only one at most), or no-op if none
* exists.
*/
private void removeInstalledFragment(FragmentTransaction ft) {
removeFragment(ft, getInstalledFragment());
}
private void showMailboxList(long accountId, long mailboxId, boolean clearBackStack) {
showFragment(MailboxListFragment.newInstance(accountId, mailboxId, false), clearBackStack);
}
private void showMessageList(long accountId, long mailboxId, boolean clearBackStack) {
showFragment(MessageListFragment.newInstance(accountId, mailboxId), clearBackStack);
}
private void showMessageView(long accountId, long mailboxId, long messageId,
boolean clearBackStack) {
showFragment(MessageViewFragment.newInstance(accountId, mailboxId, messageId),
clearBackStack);
}
/**
* Use this instead of {@link FragmentTransaction#commit}. We may switch to the synchronous
* transaction some day.
*/
private void commitFragmentTransaction(FragmentTransaction ft) {
ft.commit();
}
/**
* Push the installed fragment into our custom back stack (or optionally
* {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
*
* @param fragment {@link Fragment} to be added.
* @param clearBackStack set {@code true} to remove the currently installed fragment.
* {@code false} to push it into the backstack.
*
* TODO Delay-call the whole method and use the synchronous transaction.
*/
private void showFragment(Fragment fragment, boolean clearBackStack) {
if (DEBUG_FRAGMENTS) {
if (clearBackStack) {
Log.i(Logging.LOG_TAG, this + " backstack: [clear] showing " + fragment);
} else {
Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
+ " -> " + fragment);
}
}
if (isMessageViewInstalled()) {
removeMessageViewFragment(ft);
}
}
private void showMailboxList(long accountId, long mailboxId) {
showFragment(MailboxListFragment.newInstance(accountId, mailboxId, false));
}
private void showMessageList(long accountId, long mailboxId) {
showFragment(MessageListFragment.newInstance(accountId, mailboxId));
}
private void showMessageView(long accountId, long mailboxId, long messageId) {
showFragment(MessageViewFragment.newInstance(accountId, mailboxId, messageId));
}
private void showFragment(Fragment fragment) {
final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
removeAllFragments(ft);
if (mPreviousFragment != null) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
+ mPreviousFragment);
}
removeFragment(ft, mPreviousFragment);
mPreviousFragment = null;
}
// Remove or push the current one
if (clearBackStack) {
// Really remove the currently installed one.
removeInstalledFragment(ft);
} else {
// Instead of removing, detach the current one and push into our back stack.
mPreviousFragment = getInstalledFragment();
if (mPreviousFragment != null) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " showFragment: detaching " + mPreviousFragment);
}
ft.detach(mPreviousFragment);
}
}
// Add the new one
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " showFragment: adding " + fragment);
}
ft.add(R.id.fragment_placeholder, fragment);
commitFragmentTransaction(ft);
}
/**
* @param isSystemBackKey <code>true</code> if the system back key was pressed.
* <code>false</code> if it's caused by the "home" icon click on the action bar.
* @return true if we should pop from our custom back stack.
*/
private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
if (mPreviousFragment == null) {
return false; // Nothing in the back stack
}
// Never go back to Message View
if (mPreviousFragment instanceof MessageViewFragment) {
return false;
}
final Fragment installed = getInstalledFragment();
if (installed == null) {
// If no fragment is installed right now, do nothing.
return false;
}
// Okay now we have 2 fragments; the one in the back stack and the one that's currently
// installed.
if (mPreviousFragment.getClass() == installed.getClass()) {
// We never want to go back to the same kind of fragment, which happens when the user
// is on the message list, and selects another mailbox on the action bar.
return false;
}
if (isSystemBackKey) {
// In other cases, the system back key should always work.
return true;
} else {
// Home icon press -- there are cases where we don't want it to work.
// Disallow the Message list <-> mailbox list transition
if ((mPreviousFragment instanceof MailboxListFragment)
&& (installed instanceof MessageListFragment)) {
return false;
}
if ((mPreviousFragment instanceof MessageListFragment)
&& (installed instanceof MailboxListFragment)) {
return false;
}
return true;
}
}
/**
* Pop from our custom back stack.
*
* TODO Delay-call the whole method and use the synchronous transaction.
*/
private void popFromBackStack() {
if (mPreviousFragment == null) {
return;
}
final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
final Fragment installed = getInstalledFragment();
if (DEBUG_FRAGMENTS) {
Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
+ mPreviousFragment);
}
removeFragment(ft, installed);
ft.attach(mPreviousFragment);
commitFragmentTransaction(ft);
mPreviousFragment = null;
return;
}
private void showAllMailboxes() {
if (!isAccountSelected()) {
return; // Can happen because of asynchronous fragment transactions.
}
// Don't use open(account, NO_MAILBOX, NO_MESSAGE). This is used to open the default
// view, which is Inbox on the message list.
showMailboxList(getUIAccountId(), Mailbox.NO_MAILBOX);
// view, which is Inbox on the message list. (There's actually no way to open the mainbox
// list with open(long,long,long))
showMailboxList(getUIAccountId(), Mailbox.NO_MAILBOX, false);
}
/*

View File

@ -40,6 +40,9 @@ import java.util.Set;
/**
* UI Controller for x-large devices. Supports a multi-pane layout.
*
* Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
* so that we can easily switch between synchronous and asynchronous transactions.
*/
class UIControllerTwoPane extends UIControllerBase implements
MailboxFinder.Callback,
@ -525,6 +528,16 @@ class UIControllerTwoPane extends UIControllerBase implements
super.uninstallMessageViewFragment();
}
/**
* Commit a {@link FragmentTransaction}.
*/
private void commitFragmentTransaction(FragmentTransaction ft) {
if (DEBUG_FRAGMENTS) {
Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
}
ft.commit();
}
/**
* {@inheritDoc}
*/