| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.email.activity; |
| |
| import android.app.Activity; |
| 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 com.android.email.Email; |
| import com.android.email.MessageListContext; |
| import com.android.email.R; |
| import com.android.emailcommon.Logging; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent.Message; |
| import com.android.emailcommon.provider.Mailbox; |
| |
| import java.util.Set; |
| |
| |
| /** |
| * UI Controller for non x-large devices. Supports a single-pane layout. |
| * |
| * One one-pane, only at most one fragment can be installed at a time. |
| * |
| * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions, |
| * so that we can easily switch between synchronous and asynchronous transactions. |
| * |
| * Major TODOs |
| * - 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) { |
| // It's from combined view, so "forceShowInbox" doesn't really matter. |
| // (We're always switching accounts.) |
| switchAccount(accountId, true); |
| } |
| |
| // MailboxListFragment.Callback |
| @Override |
| public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) { |
| if (nestedNavigation) { |
| return; // Nothing to do on 1-pane. |
| } |
| openMailbox(accountId, mailboxId); |
| } |
| |
| // MailboxListFragment.Callback |
| @Override |
| public void onParentMailboxChanged() { |
| refreshActionBar(); |
| } |
| |
| // MessageListFragment.Callback |
| @Override |
| public void onAdvancingOpAccepted(Set<Long> affectedMessages) { |
| // Nothing to do on 1 pane. |
| } |
| |
| // MessageListFragment.Callback |
| @Override |
| public void onMessageOpen( |
| long messageId, long messageMailboxId, long listMailboxId, int type) { |
| if (type == MessageListFragment.Callback.TYPE_DRAFT) { |
| MessageCompose.actionEditDraft(mActivity, messageId); |
| } else { |
| open(mListContext, messageId); |
| } |
| } |
| |
| // MessageListFragment.Callback |
| @Override |
| public boolean onDragStarted() { |
| // No drag&drop on 1-pane |
| return false; |
| } |
| |
| // MessageListFragment.Callback |
| @Override |
| public void onDragEnded() { |
| // No drag&drop on 1-pane |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onForward() { |
| MessageCompose.actionForward(mActivity, getMessageId()); |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onReply() { |
| MessageCompose.actionReply(mActivity, getMessageId(), false); |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onReplyAll() { |
| MessageCompose.actionReply(mActivity, getMessageId(), true); |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onCalendarLinkClicked(long epochEventStartTime) { |
| ActivityHelper.openCalendar(mActivity, epochEventStartTime); |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public boolean onUrlInMessageClicked(String url) { |
| return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId()); |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onLoadMessageError(String errorMessage) { |
| // TODO Auto-generated method stub |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onLoadMessageFinished() { |
| // TODO Auto-generated method stub |
| } |
| |
| // MessageViewFragment.Callback |
| @Override |
| public void onLoadMessageStarted() { |
| // TODO Auto-generated method stub |
| } |
| |
| private boolean isInboxShown() { |
| if (!isMessageListInstalled()) { |
| return false; |
| } |
| return getMessageListFragment().isInboxList(); |
| } |
| |
| // This is all temporary as we'll have a different action bar controller for 1-pane. |
| private class ActionBarControllerCallback implements ActionBarController.Callback { |
| @Override |
| public int getTitleMode() { |
| if (isMailboxListInstalled()) { |
| return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL; |
| } |
| if (isMessageViewInstalled()) { |
| return TITLE_MODE_MESSAGE_SUBJECT; |
| } |
| return TITLE_MODE_ACCOUNT_WITH_MAILBOX; |
| } |
| |
| public String getMessageSubject() { |
| if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) { |
| return getMessageViewFragment().getMessage().mSubject; |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean shouldShowUp() { |
| return isMessageViewInstalled() |
| || (isMessageListInstalled() && !isInboxShown()) |
| || isMailboxListInstalled(); |
| } |
| |
| @Override |
| public long getUIAccountId() { |
| return UIControllerOnePane.this.getUIAccountId(); |
| } |
| |
| @Override |
| public long getMailboxId() { |
| return UIControllerOnePane.this.getMailboxId(); |
| } |
| |
| @Override |
| public void onMailboxSelected(long accountId, long mailboxId) { |
| if (mailboxId == Mailbox.NO_MAILBOX) { |
| showAllMailboxes(); |
| } else { |
| openMailbox(accountId, mailboxId); |
| } |
| } |
| |
| @Override |
| public boolean isAccountSelected() { |
| return UIControllerOnePane.this.isAccountSelected(); |
| } |
| |
| @Override |
| public void onAccountSelected(long accountId) { |
| switchAccount(accountId, true); // Always go to inbox |
| } |
| |
| @Override |
| public void onNoAccountsFound() { |
| Welcome.actionStart(mActivity); |
| mActivity.finish(); |
| } |
| |
| @Override |
| public String getSearchHint() { |
| if (!isMessageListInstalled()) { |
| return null; |
| } |
| return UIControllerOnePane.this.getSearchHint(); |
| } |
| |
| @Override |
| public void onSearchStarted() { |
| if (!isMessageListInstalled()) { |
| return; |
| } |
| UIControllerOnePane.this.onSearchStarted(); |
| } |
| |
| @Override |
| public void onSearchSubmit(String queryTerm) { |
| if (!isMessageListInstalled()) { |
| return; |
| } |
| UIControllerOnePane.this.onSearchSubmit(queryTerm); |
| } |
| |
| @Override |
| public void onSearchExit() { |
| UIControllerOnePane.this.onSearchExit(); |
| } |
| } |
| |
| public UIControllerOnePane(EmailActivity activity) { |
| super(activity); |
| } |
| |
| @Override |
| protected ActionBarController createActionBarController(Activity activity) { |
| |
| // For now, we just reuse the same action bar controller used for 2-pane. |
| // We may change it later. |
| |
| return new ActionBarController(activity, activity.getLoaderManager(), |
| activity.getActionBar(), new ActionBarControllerCallback()); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| if (mPreviousFragment != null) { |
| mFragmentManager.putFragment(outState, |
| BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment); |
| } |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Bundle savedInstanceState) { |
| super.onRestoreInstanceState(savedInstanceState); |
| mPreviousFragment = mFragmentManager.getFragment(savedInstanceState, |
| BUNDLE_KEY_PREVIOUS_FRAGMENT); |
| } |
| |
| @Override |
| public int getLayoutId() { |
| return R.layout.email_activity_one_pane; |
| } |
| |
| @Override |
| public long getUIAccountId() { |
| if (mListContext != null) { |
| return mListContext.mAccountId; |
| } |
| if (isMailboxListInstalled()) { |
| return getMailboxListFragment().getAccountId(); |
| } |
| return Account.NO_ACCOUNT; |
| } |
| |
| private long getMailboxId() { |
| if (mListContext != null) { |
| return mListContext.getMailboxId(); |
| } |
| return Mailbox.NO_MAILBOX; |
| } |
| |
| @Override |
| public boolean onBackPressed(boolean isSystemBackKey) { |
| if (Email.DEBUG) { |
| // This is VERY important -- no check for DEBUG_LIFECYCLE |
| Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey); |
| } |
| // The action bar controller has precedence. Must call it first. |
| if (mActionBarController.onBackPressed(isSystemBackKey)) { |
| return true; |
| } |
| // 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 (isMessageViewInstalled()) { |
| if (DEBUG_FRAGMENTS) { |
| Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List"); |
| } |
| // 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) |
| openMailbox(mListContext.mAccountId, mListContext.getMailboxId()); |
| return true; |
| } else if (isMailboxListInstalled()) { |
| // If the mailbox list is shown, always go back to the inbox. |
| switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */); |
| return true; |
| } else if (isMessageListInstalled() && !isInboxShown()) { |
| // Non-inbox list. Go to inbox. |
| switchAccount(mListContext.mAccountId, true /* force show inbox */); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void openInternal(final MessageListContext listContext, final long messageId) { |
| if (Email.DEBUG) { |
| // This is VERY important -- don't check for DEBUG_LIFECYCLE |
| Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId); |
| } |
| |
| if (messageId != Message.NO_MESSAGE) { |
| openMessage(messageId); |
| } else { |
| showFragment(MessageListFragment.newInstance(listContext)); |
| } |
| } |
| |
| /** |
| * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none |
| * exists. |
| */ |
| private Fragment getInstalledFragment() { |
| if (isMailboxListInstalled()) { |
| return getMailboxListFragment(); |
| } else if (isMessageListInstalled()) { |
| return getMessageListFragment(); |
| } else if (isMessageViewInstalled()) { |
| return getMessageViewFragment(); |
| } |
| return null; |
| } |
| |
| /** |
| * Show the mailbox list. |
| * |
| * This is the only way to open the mailbox list on 1-pane. |
| * {@link #open(MessageListContext, long)} will only open either the message list or the |
| * message view. |
| */ |
| private void openMailboxList(long accountId) { |
| setListContext(null); |
| showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false)); |
| } |
| |
| private void openMessage(long messageId) { |
| showFragment(MessageViewFragment.newInstance(messageId)); |
| } |
| |
| /** |
| * 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. |
| * |
| * TODO Delay-call the whole method and use the synchronous transaction. |
| */ |
| private void showFragment(Fragment fragment) { |
| final FragmentTransaction ft = mFragmentManager.beginTransaction(); |
| final Fragment installed = getInstalledFragment(); |
| if ((installed instanceof MessageViewFragment) |
| && (fragment instanceof MessageViewFragment)) { |
| // Newer/older navigation, auto-advance, etc. |
| // In this case we want to keep the backstack untouched, so that after back navigation |
| // we can restore the message list, including scroll position and batch selection. |
| } else { |
| if (DEBUG_FRAGMENTS) { |
| Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment() |
| + " -> " + fragment); |
| } |
| if (mPreviousFragment != null) { |
| if (DEBUG_FRAGMENTS) { |
| Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment " |
| + mPreviousFragment); |
| } |
| removeFragment(ft, mPreviousFragment); |
| mPreviousFragment = null; |
| } |
| // Remove the current fragment or push it into the backstack. |
| if (installed != null) { |
| if (installed instanceof MessageViewFragment) { |
| // Message view should never be pushed to the backstack. |
| if (DEBUG_FRAGMENTS) { |
| Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed); |
| } |
| ft.remove(installed); |
| } else { |
| // Other fragments should be pushed. |
| mPreviousFragment = installed; |
| if (DEBUG_FRAGMENTS) { |
| Log.d(Logging.LOG_TAG, this + " showFragment: detaching " |
| + mPreviousFragment); |
| } |
| ft.detach(mPreviousFragment); |
| } |
| } |
| } |
| // Show the new one |
| if (DEBUG_FRAGMENTS) { |
| Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment); |
| } |
| ft.replace(R.id.fragment_placeholder, fragment); |
| ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); |
| 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 |
| } |
| if (mPreviousFragment instanceof MessageViewFragment) { |
| throw new IllegalStateException("Message view should never be in backstack"); |
| } |
| 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 (isInboxShown()) { |
| // Inbox is the top level list - never go back from here. |
| return false; |
| } |
| |
| // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list |
| // is always considered "higher" than a non-inbox MessageList |
| if ((mPreviousFragment instanceof MessageListFragment) |
| && (!((MessageListFragment) mPreviousFragment).isInboxList()) |
| && (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 = mFragmentManager.beginTransaction(); |
| final Fragment installed = getInstalledFragment(); |
| if (DEBUG_FRAGMENTS) { |
| Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> " |
| + mPreviousFragment); |
| } |
| removeFragment(ft, installed); |
| |
| // Restore listContext. |
| if (mPreviousFragment instanceof MailboxListFragment) { |
| setListContext(null); |
| } else if (mPreviousFragment instanceof MessageListFragment) { |
| setListContext(((MessageListFragment) mPreviousFragment).getListContext()); |
| } else { |
| throw new IllegalStateException("Message view should never be in backstack"); |
| } |
| |
| ft.attach(mPreviousFragment); |
| ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); |
| mPreviousFragment = null; |
| commitFragmentTransaction(ft); |
| return; |
| } |
| |
| private void showAllMailboxes() { |
| if (!isAccountSelected()) { |
| return; // Can happen because of asynchronous fragment transactions. |
| } |
| |
| openMailboxList(getUIAccountId()); |
| } |
| |
| @Override |
| protected void installMailboxListFragment(MailboxListFragment fragment) { |
| stopMessageOrderManager(); |
| super.installMailboxListFragment(fragment); |
| } |
| |
| @Override |
| protected void installMessageListFragment(MessageListFragment fragment) { |
| stopMessageOrderManager(); |
| super.installMessageListFragment(fragment); |
| } |
| |
| @Override |
| protected long getMailboxSettingsMailboxId() { |
| return isMessageListInstalled() |
| ? getMessageListFragment().getMailboxId() |
| : Mailbox.NO_MAILBOX; |
| } |
| |
| /** |
| * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. |
| */ |
| public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { |
| if (isMessageListInstalled()) { |
| inflater.inflate(R.menu.message_list_fragment_option, menu); |
| return true; |
| } |
| if (isMessageViewInstalled()) { |
| inflater.inflate(R.menu.message_view_fragment_option, menu); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { |
| // First, let the base class do what it has to do. |
| super.onPrepareOptionsMenu(inflater, menu); |
| |
| final boolean messageViewVisible = isMessageViewInstalled(); |
| if (messageViewVisible) { |
| final MessageOrderManager om = getMessageOrderManager(); |
| menu.findItem(R.id.newer).setVisible(true); |
| menu.findItem(R.id.older).setVisible(true); |
| // orderManager shouldn't be null when the message view is installed, but just in case.. |
| menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer()); |
| menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder()); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case R.id.newer: |
| moveToNewer(); |
| return true; |
| case R.id.older: |
| moveToOlder(); |
| return true; |
| case R.id.show_all_mailboxes: |
| showAllMailboxes(); |
| return true; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| @Override |
| protected boolean isRefreshEnabled() { |
| // Refreshable only when an actual account is selected, and message view isn't shown. |
| // (i.e. only available on the mailbox list or the message view, but not on the combined |
| // one) |
| if (!isActualAccountSelected() || isMessageViewInstalled()) { |
| return false; |
| } |
| return isMailboxListInstalled() || (mListContext.getMailboxId() > 0); |
| } |
| |
| @Override |
| protected void onRefresh() { |
| if (!isRefreshEnabled()) { |
| return; |
| } |
| if (isMessageListInstalled()) { |
| mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true); |
| } else { |
| mRefreshManager.refreshMailboxList(getActualAccountId()); |
| } |
| } |
| |
| @Override |
| protected boolean isRefreshInProgress() { |
| if (!isRefreshEnabled()) { |
| return false; |
| } |
| if (isMessageListInstalled()) { |
| return mRefreshManager.isMessageListRefreshing(getMailboxId()); |
| } else { |
| return mRefreshManager.isMailboxListRefreshing(getActualAccountId()); |
| } |
| } |
| |
| @Override protected void navigateToMessage(long messageId) { |
| openMessage(messageId); |
| } |
| |
| @Override protected void updateNavigationArrows() { |
| refreshActionBar(); |
| } |
| } |