Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.activity;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.app.FragmentTransaction;
     22 import android.os.Bundle;
     23 import android.util.Log;
     24 import android.view.Menu;
     25 import android.view.MenuInflater;
     26 import android.view.MenuItem;
     27 
     28 import com.android.email.Email;
     29 import com.android.email.MessageListContext;
     30 import com.android.email.R;
     31 import com.android.emailcommon.Logging;
     32 import com.android.emailcommon.provider.Account;
     33 import com.android.emailcommon.provider.EmailContent.Message;
     34 import com.android.emailcommon.provider.Mailbox;
     35 
     36 import java.util.Set;
     37 
     38 
     39 /**
     40  * UI Controller for non x-large devices.  Supports a single-pane layout.
     41  *
     42  * One one-pane, only at most one fragment can be installed at a time.
     43  *
     44  * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
     45  * so that we can easily switch between synchronous and asynchronous transactions.
     46  *
     47  * Major TODOs
     48  * - TODO Implement callbacks
     49  */
     50 class UIControllerOnePane extends UIControllerBase {
     51     private static final String BUNDLE_KEY_PREVIOUS_FRAGMENT
     52             = "UIControllerOnePane.PREVIOUS_FRAGMENT";
     53 
     54     // Our custom poor-man's back stack which has only one entry at maximum.
     55     private Fragment mPreviousFragment;
     56 
     57     // MailboxListFragment.Callback
     58     @Override
     59     public void onAccountSelected(long accountId) {
     60         // It's from combined view, so "forceShowInbox" doesn't really matter.
     61         // (We're always switching accounts.)
     62         switchAccount(accountId, true);
     63     }
     64 
     65     // MailboxListFragment.Callback
     66     @Override
     67     public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) {
     68         if (nestedNavigation) {
     69             return; // Nothing to do on 1-pane.
     70         }
     71         openMailbox(accountId, mailboxId);
     72     }
     73 
     74     // MailboxListFragment.Callback
     75     @Override
     76     public void onParentMailboxChanged() {
     77         refreshActionBar();
     78     }
     79 
     80     // MessageListFragment.Callback
     81     @Override
     82     public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
     83         // Nothing to do on 1 pane.
     84     }
     85 
     86     // MessageListFragment.Callback
     87     @Override
     88     public void onMessageOpen(
     89             long messageId, long messageMailboxId, long listMailboxId, int type) {
     90         if (type == MessageListFragment.Callback.TYPE_DRAFT) {
     91             MessageCompose.actionEditDraft(mActivity, messageId);
     92         } else {
     93             open(mListContext, messageId);
     94         }
     95     }
     96 
     97     // MessageListFragment.Callback
     98     @Override
     99     public boolean onDragStarted() {
    100         // No drag&drop on 1-pane
    101         return false;
    102     }
    103 
    104     // MessageListFragment.Callback
    105     @Override
    106     public void onDragEnded() {
    107         // No drag&drop on 1-pane
    108     }
    109 
    110     // MessageViewFragment.Callback
    111     @Override
    112     public void onForward() {
    113         MessageCompose.actionForward(mActivity, getMessageId());
    114     }
    115 
    116     // MessageViewFragment.Callback
    117     @Override
    118     public void onReply() {
    119         MessageCompose.actionReply(mActivity, getMessageId(), false);
    120     }
    121 
    122     // MessageViewFragment.Callback
    123     @Override
    124     public void onReplyAll() {
    125         MessageCompose.actionReply(mActivity, getMessageId(), true);
    126     }
    127 
    128     // MessageViewFragment.Callback
    129     @Override
    130     public void onCalendarLinkClicked(long epochEventStartTime) {
    131         ActivityHelper.openCalendar(mActivity, epochEventStartTime);
    132     }
    133 
    134     // MessageViewFragment.Callback
    135     @Override
    136     public boolean onUrlInMessageClicked(String url) {
    137         return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
    138     }
    139 
    140     // MessageViewFragment.Callback
    141     @Override
    142     public void onLoadMessageError(String errorMessage) {
    143         // TODO Auto-generated method stub
    144     }
    145 
    146     // MessageViewFragment.Callback
    147     @Override
    148     public void onLoadMessageFinished() {
    149         // TODO Auto-generated method stub
    150     }
    151 
    152     // MessageViewFragment.Callback
    153     @Override
    154     public void onLoadMessageStarted() {
    155         // TODO Auto-generated method stub
    156     }
    157 
    158     private boolean isInboxShown() {
    159         if (!isMessageListInstalled()) {
    160             return false;
    161         }
    162         return getMessageListFragment().isInboxList();
    163     }
    164 
    165     // This is all temporary as we'll have a different action bar controller for 1-pane.
    166     private class ActionBarControllerCallback implements ActionBarController.Callback {
    167         @Override
    168         public int getTitleMode() {
    169             if (isMailboxListInstalled()) {
    170                 return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL;
    171             }
    172             if (isMessageViewInstalled()) {
    173                 return TITLE_MODE_MESSAGE_SUBJECT;
    174             }
    175             return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
    176         }
    177 
    178         public String getMessageSubject() {
    179             if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
    180                 return getMessageViewFragment().getMessage().mSubject;
    181             } else {
    182                 return null;
    183             }
    184         }
    185 
    186         @Override
    187         public boolean shouldShowUp() {
    188             return isMessageViewInstalled()
    189                     || (isMessageListInstalled() && !isInboxShown())
    190                     || isMailboxListInstalled();
    191         }
    192 
    193         @Override
    194         public long getUIAccountId() {
    195             return UIControllerOnePane.this.getUIAccountId();
    196         }
    197 
    198         @Override
    199         public long getMailboxId() {
    200             return UIControllerOnePane.this.getMailboxId();
    201         }
    202 
    203         @Override
    204         public void onMailboxSelected(long accountId, long mailboxId) {
    205             if (mailboxId == Mailbox.NO_MAILBOX) {
    206                 showAllMailboxes();
    207             } else {
    208                 openMailbox(accountId, mailboxId);
    209             }
    210         }
    211 
    212         @Override
    213         public boolean isAccountSelected() {
    214             return UIControllerOnePane.this.isAccountSelected();
    215         }
    216 
    217         @Override
    218         public void onAccountSelected(long accountId) {
    219             switchAccount(accountId, true); // Always go to inbox
    220         }
    221 
    222         @Override
    223         public void onNoAccountsFound() {
    224             Welcome.actionStart(mActivity);
    225             mActivity.finish();
    226         }
    227 
    228         @Override
    229         public String getSearchHint() {
    230             if (!isMessageListInstalled()) {
    231                 return null;
    232             }
    233             return UIControllerOnePane.this.getSearchHint();
    234         }
    235 
    236         @Override
    237         public void onSearchStarted() {
    238             if (!isMessageListInstalled()) {
    239                 return;
    240             }
    241             UIControllerOnePane.this.onSearchStarted();
    242         }
    243 
    244         @Override
    245         public void onSearchSubmit(String queryTerm) {
    246             if (!isMessageListInstalled()) {
    247                 return;
    248             }
    249             UIControllerOnePane.this.onSearchSubmit(queryTerm);
    250         }
    251 
    252         @Override
    253         public void onSearchExit() {
    254             UIControllerOnePane.this.onSearchExit();
    255         }
    256     }
    257 
    258     public UIControllerOnePane(EmailActivity activity) {
    259         super(activity);
    260     }
    261 
    262     @Override
    263     protected ActionBarController createActionBarController(Activity activity) {
    264 
    265         // For now, we just reuse the same action bar controller used for 2-pane.
    266         // We may change it later.
    267 
    268         return new ActionBarController(activity, activity.getLoaderManager(),
    269                 activity.getActionBar(), new ActionBarControllerCallback());
    270     }
    271 
    272     @Override
    273     public void onSaveInstanceState(Bundle outState) {
    274         super.onSaveInstanceState(outState);
    275         if (mPreviousFragment != null) {
    276             mFragmentManager.putFragment(outState,
    277                     BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
    278         }
    279     }
    280 
    281     @Override
    282     public void onRestoreInstanceState(Bundle savedInstanceState) {
    283         super.onRestoreInstanceState(savedInstanceState);
    284         mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
    285                 BUNDLE_KEY_PREVIOUS_FRAGMENT);
    286     }
    287 
    288     @Override
    289     public int getLayoutId() {
    290         return R.layout.email_activity_one_pane;
    291     }
    292 
    293     @Override
    294     public long getUIAccountId() {
    295         if (mListContext != null) {
    296             return mListContext.mAccountId;
    297         }
    298         if (isMailboxListInstalled()) {
    299             return getMailboxListFragment().getAccountId();
    300         }
    301         return Account.NO_ACCOUNT;
    302     }
    303 
    304     private long getMailboxId() {
    305         if (mListContext != null) {
    306             return mListContext.getMailboxId();
    307         }
    308         return Mailbox.NO_MAILBOX;
    309     }
    310 
    311     @Override
    312     public boolean onBackPressed(boolean isSystemBackKey) {
    313         if (Email.DEBUG) {
    314             // This is VERY important -- no check for DEBUG_LIFECYCLE
    315             Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
    316         }
    317         // The action bar controller has precedence.  Must call it first.
    318         if (mActionBarController.onBackPressed(isSystemBackKey)) {
    319             return true;
    320         }
    321         // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
    322         if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
    323             if (DEBUG_FRAGMENTS) {
    324                 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
    325             }
    326             return true;
    327         }
    328 
    329         // Custom back stack
    330         if (shouldPopFromBackStack(isSystemBackKey)) {
    331             if (DEBUG_FRAGMENTS) {
    332                 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
    333             }
    334             popFromBackStack();
    335             return true;
    336         }
    337 
    338         // No entry in the back stack.
    339         if (isMessageViewInstalled()) {
    340             if (DEBUG_FRAGMENTS) {
    341                 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
    342             }
    343             // If the message view is shown, show the "parent" message list.
    344             // This happens when we get a deep link to a message.  (e.g. from a widget)
    345             openMailbox(mListContext.mAccountId, mListContext.getMailboxId());
    346             return true;
    347         } else if (isMailboxListInstalled()) {
    348             // If the mailbox list is shown, always go back to the inbox.
    349             switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */);
    350             return true;
    351         } else if (isMessageListInstalled() && !isInboxShown()) {
    352             // Non-inbox list. Go to inbox.
    353             switchAccount(mListContext.mAccountId, true /* force show inbox */);
    354             return true;
    355         }
    356         return false;
    357     }
    358 
    359     @Override
    360     public void openInternal(final MessageListContext listContext, final long messageId) {
    361         if (Email.DEBUG) {
    362             // This is VERY important -- don't check for DEBUG_LIFECYCLE
    363             Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId);
    364         }
    365 
    366         if (messageId != Message.NO_MESSAGE) {
    367             openMessage(messageId);
    368         } else {
    369             showFragment(MessageListFragment.newInstance(listContext));
    370         }
    371     }
    372 
    373     /**
    374      * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
    375      *         exists.
    376      */
    377     private Fragment getInstalledFragment() {
    378         if (isMailboxListInstalled()) {
    379             return getMailboxListFragment();
    380         } else if (isMessageListInstalled()) {
    381             return getMessageListFragment();
    382         } else if (isMessageViewInstalled()) {
    383             return getMessageViewFragment();
    384         }
    385         return null;
    386     }
    387 
    388     /**
    389      * Show the mailbox list.
    390      *
    391      * This is the only way to open the mailbox list on 1-pane.
    392      * {@link #open(MessageListContext, long)} will only open either the message list or the
    393      * message view.
    394      */
    395     private void openMailboxList(long accountId) {
    396         setListContext(null);
    397         showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false));
    398     }
    399 
    400     private void openMessage(long messageId) {
    401         showFragment(MessageViewFragment.newInstance(messageId));
    402     }
    403 
    404     /**
    405      * Push the installed fragment into our custom back stack (or optionally
    406      * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
    407      *
    408      * @param fragment {@link Fragment} to be added.
    409      *
    410      *  TODO Delay-call the whole method and use the synchronous transaction.
    411      */
    412     private void showFragment(Fragment fragment) {
    413         final FragmentTransaction ft = mFragmentManager.beginTransaction();
    414         final Fragment installed = getInstalledFragment();
    415         if ((installed instanceof MessageViewFragment)
    416                 && (fragment instanceof MessageViewFragment)) {
    417             // Newer/older navigation, auto-advance, etc.
    418             // In this case we want to keep the backstack untouched, so that after back navigation
    419             // we can restore the message list, including scroll position and batch selection.
    420         } else {
    421             if (DEBUG_FRAGMENTS) {
    422                 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
    423                         + " -> " + fragment);
    424             }
    425             if (mPreviousFragment != null) {
    426                 if (DEBUG_FRAGMENTS) {
    427                     Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
    428                             + mPreviousFragment);
    429                 }
    430                 removeFragment(ft, mPreviousFragment);
    431                 mPreviousFragment = null;
    432             }
    433             // Remove the current fragment or push it into the backstack.
    434             if (installed != null) {
    435                 if (installed instanceof MessageViewFragment) {
    436                     // Message view should never be pushed to the backstack.
    437                     if (DEBUG_FRAGMENTS) {
    438                         Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed);
    439                     }
    440                     ft.remove(installed);
    441                 } else {
    442                     // Other fragments should be pushed.
    443                     mPreviousFragment = installed;
    444                     if (DEBUG_FRAGMENTS) {
    445                         Log.d(Logging.LOG_TAG, this + " showFragment: detaching "
    446                                 + mPreviousFragment);
    447                     }
    448                     ft.detach(mPreviousFragment);
    449                 }
    450             }
    451         }
    452         // Show the new one
    453         if (DEBUG_FRAGMENTS) {
    454             Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment);
    455         }
    456         ft.replace(R.id.fragment_placeholder, fragment);
    457         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
    458         commitFragmentTransaction(ft);
    459     }
    460 
    461     /**
    462      * @param isSystemBackKey <code>true</code> if the system back key was pressed.
    463      *        <code>false</code> if it's caused by the "home" icon click on the action bar.
    464      * @return true if we should pop from our custom back stack.
    465      */
    466     private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
    467         if (mPreviousFragment == null) {
    468             return false; // Nothing in the back stack
    469         }
    470         if (mPreviousFragment instanceof MessageViewFragment) {
    471             throw new IllegalStateException("Message view should never be in backstack");
    472         }
    473         final Fragment installed = getInstalledFragment();
    474         if (installed == null) {
    475             // If no fragment is installed right now, do nothing.
    476             return false;
    477         }
    478 
    479         // Okay now we have 2 fragments; the one in the back stack and the one that's currently
    480         // installed.
    481         if (isInboxShown()) {
    482             // Inbox is the top level list - never go back from here.
    483             return false;
    484         }
    485 
    486         // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list
    487         // is always considered "higher" than a non-inbox MessageList
    488         if ((mPreviousFragment instanceof MessageListFragment)
    489                 && (!((MessageListFragment) mPreviousFragment).isInboxList())
    490                 && (installed  instanceof MailboxListFragment)) {
    491             return false;
    492         }
    493         return true;
    494     }
    495 
    496     /**
    497      * Pop from our custom back stack.
    498      *
    499      * TODO Delay-call the whole method and use the synchronous transaction.
    500      */
    501     private void popFromBackStack() {
    502         if (mPreviousFragment == null) {
    503             return;
    504         }
    505         final FragmentTransaction ft = mFragmentManager.beginTransaction();
    506         final Fragment installed = getInstalledFragment();
    507         if (DEBUG_FRAGMENTS) {
    508             Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
    509                     + mPreviousFragment);
    510         }
    511         removeFragment(ft, installed);
    512 
    513         // Restore listContext.
    514         if (mPreviousFragment instanceof MailboxListFragment) {
    515             setListContext(null);
    516         } else if (mPreviousFragment instanceof MessageListFragment) {
    517             setListContext(((MessageListFragment) mPreviousFragment).getListContext());
    518         } else {
    519             throw new IllegalStateException("Message view should never be in backstack");
    520         }
    521 
    522         ft.attach(mPreviousFragment);
    523         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
    524         mPreviousFragment = null;
    525         commitFragmentTransaction(ft);
    526         return;
    527     }
    528 
    529     private void showAllMailboxes() {
    530         if (!isAccountSelected()) {
    531             return; // Can happen because of asynchronous fragment transactions.
    532         }
    533 
    534         openMailboxList(getUIAccountId());
    535     }
    536 
    537     @Override
    538     protected void installMailboxListFragment(MailboxListFragment fragment) {
    539         stopMessageOrderManager();
    540         super.installMailboxListFragment(fragment);
    541     }
    542 
    543     @Override
    544     protected void installMessageListFragment(MessageListFragment fragment) {
    545         stopMessageOrderManager();
    546         super.installMessageListFragment(fragment);
    547     }
    548 
    549     @Override
    550     protected long getMailboxSettingsMailboxId() {
    551         return isMessageListInstalled()
    552                 ? getMessageListFragment().getMailboxId()
    553                 : Mailbox.NO_MAILBOX;
    554     }
    555 
    556     @Override
    557     public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
    558         // First, let the base class do what it has to do.
    559         super.onPrepareOptionsMenu(inflater, menu);
    560 
    561         // Then override
    562         final boolean messageListVisible = isMessageListInstalled();
    563         if (!messageListVisible) {
    564             menu.findItem(R.id.search).setVisible(false);
    565             menu.findItem(R.id.compose).setVisible(false);
    566             menu.findItem(R.id.refresh).setVisible(false);
    567             menu.findItem(R.id.show_all_mailboxes).setVisible(false);
    568             menu.findItem(R.id.mailbox_settings).setVisible(false);
    569         }
    570 
    571         final boolean messageViewVisible = isMessageViewInstalled();
    572         if (messageViewVisible) {
    573             final MessageOrderManager om = getMessageOrderManager();
    574             menu.findItem(R.id.newer).setVisible(true);
    575             menu.findItem(R.id.older).setVisible(true);
    576             // orderManager shouldn't be null when the message view is installed, but just in case..
    577             menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer());
    578             menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder());
    579         }
    580         return true;
    581     }
    582 
    583     @Override
    584     public boolean onOptionsItemSelected(MenuItem item) {
    585         switch (item.getItemId()) {
    586             case R.id.newer:
    587                 moveToNewer();
    588                 return true;
    589             case R.id.older:
    590                 moveToOlder();
    591                 return true;
    592             case R.id.show_all_mailboxes:
    593                 showAllMailboxes();
    594                 return true;
    595         }
    596         return super.onOptionsItemSelected(item);
    597     }
    598 
    599     @Override
    600     protected boolean isRefreshEnabled() {
    601         // Refreshable only when an actual account is selected, and message view isn't shown.
    602         // (i.e. only available on the mailbox list or the message view, but not on the combined
    603         // one)
    604         if (!isActualAccountSelected() || isMessageViewInstalled()) {
    605             return false;
    606         }
    607         return isMailboxListInstalled() || (mListContext.getMailboxId() > 0);
    608     }
    609 
    610     @Override
    611     protected void onRefresh() {
    612         if (!isRefreshEnabled()) {
    613             return;
    614         }
    615         if (isMessageListInstalled()) {
    616             mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
    617         } else {
    618             mRefreshManager.refreshMailboxList(getActualAccountId());
    619         }
    620     }
    621 
    622     @Override
    623     protected boolean isRefreshInProgress() {
    624         if (!isRefreshEnabled()) {
    625             return false;
    626         }
    627         if (isMessageListInstalled()) {
    628             return mRefreshManager.isMessageListRefreshing(getMailboxId());
    629         } else {
    630             return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
    631         }
    632     }
    633 
    634     @Override protected void navigateToMessage(long messageId) {
    635         openMessage(messageId);
    636     }
    637 
    638     @Override protected void updateNavigationArrows() {
    639         refreshActionBar();
    640     }
    641 }
    642