UIControllerTwoPane.java revision 075feb45562429cb8c7e19b43dc91e9778afeb24
1/*
2 * Copyright (C) 2010 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
17package com.android.email.activity;
18
19import com.android.email.Clock;
20import com.android.email.Email;
21import com.android.email.Preferences;
22import com.android.email.R;
23import com.android.email.RefreshManager;
24import com.android.email.activity.setup.AccountSecurity;
25import com.android.email.activity.setup.AccountSettingsXL;
26import com.android.emailcommon.Logging;
27import com.android.emailcommon.provider.EmailContent.Account;
28import com.android.emailcommon.provider.EmailContent.Mailbox;
29import com.android.emailcommon.utility.EmailAsyncTask;
30import com.android.emailcommon.utility.Utility;
31
32import android.app.ActionBar;
33import android.app.Activity;
34import android.app.Fragment;
35import android.app.FragmentManager;
36import android.app.FragmentTransaction;
37import android.app.LoaderManager.LoaderCallbacks;
38import android.content.Context;
39import android.content.Loader;
40import android.database.Cursor;
41import android.os.Bundle;
42import android.util.Log;
43import android.view.LayoutInflater;
44import android.view.Menu;
45import android.view.MenuInflater;
46import android.view.MenuItem;
47import android.view.View;
48import android.widget.TextView;
49
50import java.security.InvalidParameterException;
51import java.util.ArrayList;
52import java.util.Set;
53import java.util.Stack;
54
55/**
56 * UI Controller for x-large devices.  Supports a multi-pane layout.
57 *
58 * Note: Always use {@link #commitFragmentTransaction} to commit fragment transactions.  Currently
59 * we use synchronous transactions only, but we may want to switch back to asynchronous later.
60 *
61 * TODO: Test it.  It's testable if we implement MockFragmentTransaction, which may be too early
62 * to do so at this point.  (API may not be stable enough yet.)
63 *
64 * TODO Consider extracting out a separate class to manage the action bar
65 */
66class UIControllerTwoPane implements
67        MailboxFinder.Callback,
68        ThreePaneLayout.Callback,
69        MailboxListFragment.Callback,
70        MessageListFragment.Callback,
71        MessageViewFragment.Callback {
72    private static final String BUNDLE_KEY_ACCOUNT_ID = "UIControllerTwoPane.state.account_id";
73    private static final String BUNDLE_KEY_MAILBOX_ID = "UIControllerTwoPane.state.mailbox_id";
74    private static final String BUNDLE_KEY_MESSAGE_ID = "UIControllerTwoPane.state.message_id";
75    private static final String BUNDLE_KEY_MAILBOX_STACK
76            = "UIControllerTwoPane.state.mailbox_stack";
77
78    /* package */ static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds
79    /* package */ static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds
80
81    private static final int LOADER_ID_ACCOUNT_LIST
82            = EmailActivity.UI_CONTROLLER_LOADER_ID_BASE + 0;
83
84    /** No account selected */
85    static final long NO_ACCOUNT = -1;
86    /** No mailbox selected */
87    static final long NO_MAILBOX = -1;
88    /** No message selected */
89    static final long NO_MESSAGE = -1;
90    /** Current account id */
91    private long mAccountId = NO_ACCOUNT;
92
93    // TODO Remove this instance variable and replace it with a call to mMessageListFragment to
94    // retrieve it's mailbox ID. There's no reason we should be duplicating data
95    /**
96     * The id of the currently viewed mailbox in the mailbox list fragment.
97     * IMPORTANT: Do not confuse this with the value returned by {@link #getMessageListMailboxId()}
98     * which is the mailbox id associated with the message list fragment. The two may be different.
99     */
100    private long mMailboxListMailboxId = NO_MAILBOX;
101
102    /** Current message id */
103    private long mMessageId = NO_MESSAGE;
104
105    // Action bar
106    private ActionBar mActionBar;
107    private AccountSelectorAdapter mAccountsSelectorAdapter;
108    private final ActionBarNavigationCallback mActionBarNavigationCallback =
109        new ActionBarNavigationCallback();
110    private View mActionBarMailboxNameView;
111    private TextView mActionBarMailboxName;
112    private TextView mActionBarUnreadCount;
113
114    // Other UI elements
115    private ThreePaneLayout mThreePane;
116
117    /**
118     * Fragments that are installed.
119     *
120     * A fragment is installed when:
121     * - it is attached to the activity
122     * - the parent activity is created
123     * - and it is not scheduled to be removed.
124     *
125     * We set callbacks to fragments only when they are installed.
126     */
127    private MailboxListFragment mMailboxListFragment;
128    private MessageListFragment mMessageListFragment;
129    private MessageViewFragment mMessageViewFragment;
130
131    private MessageCommandButtonView mMessageCommandButtons;
132
133    private MailboxFinder mMailboxFinder;
134
135    private final RefreshManager mRefreshManager;
136    private final RefreshListener mRefreshListener = new RefreshListener();
137    private MessageOrderManager mOrderManager;
138    private final MessageOrderManagerCallback mMessageOrderManagerCallback =
139        new MessageOrderManagerCallback();
140    /** Mailbox IDs that the user has navigated away from; used to provide "back" functionality */
141    private final Stack<Long> mMailboxStack = new Stack<Long>();
142
143    /**
144     * List of fragments that are restored by the framework while the activity is being re-created
145     * for configuration changes (e.g. screen rotation).  We'll install them later when the activity
146     * is created in {@link #installRestoredFragments()}.
147     */
148    private final ArrayList<Fragment> mRestoredFragments = new ArrayList<Fragment>();
149
150    /**
151     * Whether fragment installation should be hold.
152     * We hold installing fragments until {@link #installRestoredFragments()} is called.
153     */
154    private boolean mHoldFragmentInstallation = true;
155
156    /** The owner activity */
157    private final EmailActivity mActivity;
158
159    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
160
161    public UIControllerTwoPane(EmailActivity activity) {
162        mActivity = activity;
163        mRefreshManager = RefreshManager.getInstance(mActivity);
164    }
165
166    // MailboxFinder$Callback
167    @Override
168    public void onAccountNotFound() {
169        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
170            Log.d(Logging.LOG_TAG, "" + this + " onAccountNotFound()");
171        }
172        // Shouldn't happen
173    }
174
175    @Override
176    public void onAccountSecurityHold(long accountId) {
177        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
178            Log.d(Logging.LOG_TAG, "" + this + " onAccountSecurityHold()");
179        }
180        mActivity.startActivity(AccountSecurity.actionUpdateSecurityIntent(mActivity, accountId,
181                true));
182    }
183
184    @Override
185    public void onMailboxFound(long accountId, long mailboxId) {
186        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
187            Log.d(Logging.LOG_TAG, "" + this + " onMailboxFound()");
188        }
189        updateMessageList(mailboxId, true, true);
190    }
191
192    @Override
193    public void onMailboxNotFound(long accountId) {
194        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
195            Log.d(Logging.LOG_TAG, "" + this + " onMailboxNotFound()");
196        }
197        // TODO: handle more gracefully.
198        Log.e(Logging.LOG_TAG, "unable to find mailbox for account " + accountId);
199    }
200
201    @Override
202    public void onMailboxNotFound() {
203        // TODO: handle more gracefully.
204        Log.e(Logging.LOG_TAG, "unable to find mailbox");
205    }
206
207    // ThreePaneLayoutCallback
208    @Override
209    public void onVisiblePanesChanged(int previousVisiblePanes) {
210
211        updateActionBar();
212
213        // If the right pane is gone, remove the message view.
214        final int visiblePanes = mThreePane.getVisiblePanes();
215        if (((visiblePanes & ThreePaneLayout.PANE_RIGHT) == 0) &&
216                ((previousVisiblePanes & ThreePaneLayout.PANE_RIGHT) != 0)) {
217            // Message view just got hidden
218            mMessageId = NO_MESSAGE;
219            if (mMessageListFragment != null) {
220                mMessageListFragment.setSelectedMessage(NO_MESSAGE);
221            }
222            uninstallMessageViewFragment(mActivity.getFragmentManager().beginTransaction())
223                    .commit();
224        }
225        // Disable CAB when the message list is not visible.
226        if (mMessageListFragment != null) {
227            mMessageListFragment.onHidden((visiblePanes & ThreePaneLayout.PANE_MIDDLE) == 0);
228        }
229    }
230
231    /**
232     * Update the action bar according to the current state.
233     *
234     * - Show/hide the "back" button next to the "Home" icon.
235     * - Show/hide the current mailbox name.
236     */
237    private void updateActionBar() {
238        final int visiblePanes = mThreePane.getVisiblePanes();
239
240        // If the left pane (mailbox list pane) is hidden, the back action on action bar will be
241        // enabled, and we also show the current mailbox name.
242        final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0);
243        boolean displayUp = leftPaneHidden || !mMailboxStack.isEmpty();
244        mActionBar.setDisplayOptions(displayUp ? ActionBar.DISPLAY_HOME_AS_UP : 0,
245                ActionBar.DISPLAY_HOME_AS_UP);
246        mActionBarMailboxNameView.setVisibility(leftPaneHidden ? View.VISIBLE : View.GONE);
247    }
248
249    // MailboxListFragment$Callback
250    @Override
251    public void onMailboxSelected(long accountId, long mailboxId, boolean navigate,
252            boolean dragDrop) {
253        if (dragDrop) {
254            // We don't want to change the message list for D&D.
255
256            // STOPSHIP fixit: the new mailbox list created here doesn't know D&D is in progress.
257
258            updateMailboxList(accountId, mailboxId, true,
259                    false /* don't clear message list and message view */);
260        } else if (mailboxId == NO_MAILBOX) {
261            // reload the top-level message list.  Always implies navigate.
262            openAccount(accountId);
263        } else if (navigate) {
264            if (mMailboxStack.isEmpty() || mailboxId != mMailboxListMailboxId) {
265                // Don't navigate to the same mailbox id twice in a row
266                mMailboxStack.push(mMailboxListMailboxId);
267                openMailbox(accountId, mailboxId);
268            }
269        } else {
270            updateMessageList(mailboxId, true, true);
271        }
272    }
273
274    @Override
275    public void onAccountSelected(long accountId) {
276        // TODO openAccount should do the check eventually, but it's necessary for now.
277        if (accountId != getUIAccountId()) {
278            openAccount(accountId);
279            loadAccounts(); // update account spinner
280        }
281    }
282
283    @Override
284    public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount) {
285        mActionBarMailboxName.setText(mailboxName);
286
287        // Note on action bar, we show only "unread count".  Some mailboxes such as Outbox don't
288        // have the idea of "unread count", in which case we just omit the count.
289        mActionBarUnreadCount.setText(
290                UiUtilities.getMessageCountForUi(mActivity, unreadCount, true));
291    }
292
293    // MessageListFragment$Callback
294    @Override
295    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
296            int type) {
297        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
298            MessageCompose.actionEditDraft(mActivity, messageId);
299        } else {
300            updateMessageView(messageId);
301        }
302    }
303
304    @Override
305    public void onEnterSelectionMode(boolean enter) {
306    }
307
308    /**
309     * Apply the auto-advance policy upon initation of a batch command that could potentially
310     * affect the currently selected conversation.
311     */
312    @Override
313    public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
314        if (!isMessageSelected()) {
315            // Do nothing if message view is not visible.
316            return;
317        }
318
319        int autoAdvanceDir = Preferences.getPreferences(mActivity).getAutoAdvanceDirection();
320        if ((autoAdvanceDir == Preferences.AUTO_ADVANCE_MESSAGE_LIST) || (mOrderManager == null)) {
321            if (affectedMessages.contains(getMessageId())) {
322                goBackToMailbox();
323            }
324            return;
325        }
326
327        // Navigate to the first unselected item in the appropriate direction.
328        switch (autoAdvanceDir) {
329            case Preferences.AUTO_ADVANCE_NEWER:
330                while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) {
331                    if (!mOrderManager.moveToNewer()) {
332                        goBackToMailbox();
333                        return;
334                    }
335                }
336                updateMessageView(mOrderManager.getCurrentMessageId());
337                break;
338
339            case Preferences.AUTO_ADVANCE_OLDER:
340                while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) {
341                    if (!mOrderManager.moveToOlder()) {
342                        goBackToMailbox();
343                        return;
344                    }
345                }
346                updateMessageView(mOrderManager.getCurrentMessageId());
347                break;
348        }
349    }
350
351    @Override
352    public void onListLoaded() {
353    }
354
355    // MessageViewFragment$Callback
356    @Override
357    public void onMessageViewShown(int mailboxType) {
358        updateMessageOrderManager();
359        updateNavigationArrows();
360    }
361
362    @Override
363    public void onMessageViewGone() {
364        stopMessageOrderManager();
365    }
366
367    @Override
368    public boolean onUrlInMessageClicked(String url) {
369        return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
370    }
371
372    @Override
373    public void onMessageSetUnread() {
374        goBackToMailbox();
375    }
376
377    @Override
378    public void onMessageNotExists() {
379        goBackToMailbox();
380    }
381
382    @Override
383    public void onLoadMessageStarted() {
384        // TODO Any nice UI for this?
385    }
386
387    @Override
388    public void onLoadMessageFinished() {
389        // TODO Any nice UI for this?
390    }
391
392    @Override
393    public void onLoadMessageError(String errorMessage) {
394    }
395
396    @Override
397    public void onRespondedToInvite(int response) {
398        onCurrentMessageGone();
399    }
400
401    @Override
402    public void onCalendarLinkClicked(long epochEventStartTime) {
403        ActivityHelper.openCalendar(mActivity, epochEventStartTime);
404    }
405
406    @Override
407    public void onBeforeMessageGone() {
408        onCurrentMessageGone();
409    }
410
411    @Override
412    public void onForward() {
413        MessageCompose.actionForward(mActivity, getMessageId());
414    }
415
416    @Override
417    public void onReply() {
418        MessageCompose.actionReply(mActivity, getMessageId(), false);
419    }
420
421    @Override
422    public void onReplyAll() {
423        MessageCompose.actionReply(mActivity, getMessageId(), true);
424    }
425
426    /**
427     * Must be called just after the activity sets up the content view.
428     *
429     * (Due to the complexity regarding class/activity initialization order, we can't do this in
430     * the constructor.)  TODO this should no longer be true when we merge activities.
431     */
432    public void onActivityViewReady() {
433        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
434            Log.d(Logging.LOG_TAG, "" + this + " onActivityViewReady");
435        }
436        // Set up action bar
437        mActionBar = mActivity.getActionBar();
438        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME);
439
440        // Set a view for the current mailbox to the action bar.
441        final LayoutInflater inflater = LayoutInflater.from(mActivity);
442        mActionBarMailboxNameView = inflater.inflate(R.layout.action_bar_current_mailbox, null);
443        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM);
444        final ActionBar.LayoutParams customViewLayout = new ActionBar.LayoutParams(
445                ActionBar.LayoutParams.WRAP_CONTENT,
446                ActionBar.LayoutParams.MATCH_PARENT);
447        customViewLayout.setMargins(mActivity.getResources().getDimensionPixelSize(
448                        R.dimen.action_bar_mailbox_name_left_margin) , 0, 0, 0);
449        mActionBar.setCustomView(mActionBarMailboxNameView, customViewLayout);
450
451        mActionBarMailboxName =
452                (TextView) mActionBarMailboxNameView.findViewById(R.id.mailbox_name);
453        mActionBarUnreadCount =
454                (TextView) mActionBarMailboxNameView.findViewById(R.id.unread_count);
455
456
457        // Set up content
458        mThreePane = (ThreePaneLayout) mActivity.findViewById(R.id.three_pane);
459        mThreePane.setCallback(this);
460
461        mMessageCommandButtons = mThreePane.getMessageCommandButtons();
462        mMessageCommandButtons.setCallback(new CommandButtonCallback());
463    }
464
465    /**
466     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
467     *
468     * @see #getActualAccountId()
469     */
470    public long getUIAccountId() {
471        return mAccountId;
472    }
473
474    /**
475     * @return the currently selected account ID.  If the current view is the combined view,
476     * it'll return {@link #NO_ACCOUNT}.
477     *
478     * @see #getUIAccountId()
479     */
480    public long getActualAccountId() {
481        return mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW ? NO_ACCOUNT : mAccountId;
482    }
483
484    /**
485     * Returns the id of the mailbox used for the message list fragment.
486     * IMPORTANT: Do not confuse this with {@link #mMailboxListMailboxId} which is the id used
487     * for the mailbox list. The two may be different.
488     */
489    public long getMessageListMailboxId() {
490        return mMessageListFragment.getMailboxId();
491    }
492
493    public long getMessageId() {
494        return mMessageId;
495    }
496
497    /**
498     * @return true if an account is selected, or the current view is the combined view.
499     */
500    public boolean isAccountSelected() {
501        return getUIAccountId() != NO_ACCOUNT;
502    }
503
504    public boolean isMailboxSelected() {
505        return getMessageListMailboxId() != NO_MAILBOX;
506    }
507
508    public boolean isMessageSelected() {
509        return getMessageId() != NO_MESSAGE;
510    }
511
512    /**
513     * @return true if refresh is in progress for the current mailbox.
514     */
515    public boolean isRefreshInProgress() {
516        long messageListMailboxId = getMessageListMailboxId();
517        return (messageListMailboxId >= 0)
518                && mRefreshManager.isMessageListRefreshing(messageListMailboxId);
519    }
520
521    /**
522     * @return true if the UI should enable the "refresh" command.
523     */
524    public boolean isRefreshEnabled() {
525        // - Don't show for combined inboxes, but
526        // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list
527        return -1 != getActualAccountId();
528    }
529
530    /**
531     * Called by the host activity at the end of {@link Activity#onCreate}.
532     */
533    public void onActivityCreated() {
534        mRefreshManager.registerListener(mRefreshListener);
535        loadAccounts();
536    }
537
538    /**
539     * Install all the fragments kept in {@link #mRestoredFragments}.
540     *
541     * Must be called at the end of {@link EmailActivity#onCreate}.
542     */
543    public void installRestoredFragments() {
544        mHoldFragmentInstallation = false;
545
546        // Install all the fragments restored by the framework.
547        for (Fragment fragment : mRestoredFragments) {
548            installFragment(fragment);
549        }
550        mRestoredFragments.clear();
551    }
552
553    /**
554     * Called by {@link EmailActivity} when a {@link Fragment} is attached.
555     *
556     * If the activity has already been created, we initialize the fragment here.  Otherwise we
557     * keep the fragment in {@link #mRestoredFragments} and initialize it after the activity's
558     * onCreate.
559     */
560    public void onAttachFragment(Fragment fragment) {
561        if (mHoldFragmentInstallation) {
562            // Fragment being restored by the framework during the activity recreation.
563            mRestoredFragments.add(fragment);
564            return;
565        }
566        installFragment(fragment);
567    }
568
569    /**
570     * Called from {@link EmailActivity#onStart}.
571     */
572    public void onStart() {
573        if (isMessageSelected()) {
574            updateMessageOrderManager();
575        }
576    }
577
578    /**
579     * Called from {@link EmailActivity#onResume}.
580     */
581    public void onResume() {
582        updateActionBar();
583    }
584
585    /**
586     * Called from {@link EmailActivity#onPause}.
587     */
588    public void onPause() {
589    }
590
591    /**
592     * Called from {@link EmailActivity#onStop}.
593     */
594    public void onStop() {
595        stopMessageOrderManager();
596    }
597
598    /**
599     * Called from {@link EmailActivity#onDestroy}.
600     */
601    public void onDestroy() {
602        mHoldFragmentInstallation = true; // No more fragment installation.
603        mTaskTracker.cancellAllInterrupt();
604        closeMailboxFinder();
605        mRefreshManager.unregisterListener(mRefreshListener);
606    }
607
608    public void onSaveInstanceState(Bundle outState) {
609        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
610            Log.d(Logging.LOG_TAG, "" + this + " onSaveInstanceState");
611        }
612        outState.putLong(BUNDLE_KEY_ACCOUNT_ID, mAccountId);
613        outState.putLong(BUNDLE_KEY_MAILBOX_ID, mMailboxListMailboxId);
614        outState.putLong(BUNDLE_KEY_MESSAGE_ID, mMessageId);
615        if (!mMailboxStack.isEmpty()) {
616            // Save the mailbox stack
617            long[] mailboxIds = Utility.toPrimitiveLongArray(mMailboxStack);
618            outState.putLongArray(BUNDLE_KEY_MAILBOX_STACK, mailboxIds);
619        }
620    }
621
622    public void restoreInstanceState(Bundle savedInstanceState) {
623        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
624            Log.d(Logging.LOG_TAG, "" + this + " restoreInstanceState");
625        }
626        mAccountId = savedInstanceState.getLong(BUNDLE_KEY_ACCOUNT_ID, NO_ACCOUNT);
627        mMailboxListMailboxId = savedInstanceState.getLong(BUNDLE_KEY_MAILBOX_ID, NO_MAILBOX);
628        mMessageId = savedInstanceState.getLong(BUNDLE_KEY_MESSAGE_ID, NO_MESSAGE);
629        long[] mailboxIds = savedInstanceState.getLongArray(BUNDLE_KEY_MAILBOX_STACK);
630        if (mailboxIds != null) {
631            // Restore the mailbox stack; ugly hack to get around 'Long' versus 'long'
632            mMailboxStack.clear();
633            for (long id : mailboxIds) {
634                mMailboxStack.push(id);
635            }
636        }
637
638        // STOPSHIP If MailboxFinder is still running, it needs restarting after loadState().
639        // This probably means we need to start MailboxFinder if mMailboxId == -1.
640    }
641
642    private void installFragment(Fragment fragment) {
643        if (fragment instanceof MailboxListFragment) {
644            mMailboxListFragment = (MailboxListFragment) fragment;
645            mMailboxListFragment.setCallback(this);
646        } else if (fragment instanceof MessageListFragment) {
647            mMessageListFragment = (MessageListFragment) fragment;
648            mMessageListFragment.setCallback(this);
649        } else if (fragment instanceof MessageViewFragment) {
650            mMessageViewFragment = (MessageViewFragment) fragment;
651            mMessageViewFragment.setCallback(this);
652        } else {
653            // Ignore -- uninteresting fragments such as dialogs.
654        }
655    }
656
657    private FragmentTransaction uninstallMailboxListFragment(FragmentTransaction ft) {
658        if (mMailboxListFragment != null) {
659            ft.remove(mMailboxListFragment);
660            mMailboxListFragment.setCallback(null);
661            mMailboxListFragment = null;
662        }
663        return ft;
664    }
665
666    private FragmentTransaction uninstallMessageListFragment(FragmentTransaction ft) {
667        if (mMessageListFragment != null) {
668            ft.remove(mMessageListFragment);
669            mMessageListFragment.setCallback(null);
670            mMessageListFragment = null;
671        }
672        return ft;
673    }
674
675    private FragmentTransaction uninstallMessageViewFragment(FragmentTransaction ft) {
676        if (mMessageViewFragment != null) {
677            ft.remove(mMessageViewFragment);
678            mMessageViewFragment.setCallback(null);
679            mMessageViewFragment = null;
680        }
681        return ft;
682    }
683
684    private void commitFragmentTransaction(FragmentTransaction ft) {
685        ft.commit();
686        mActivity.getFragmentManager().executePendingTransactions();
687    }
688
689    /**
690     * Show the default view for the account.
691     *
692     * On two-pane, it's the account's root mailboxes on the left pane with Inbox on the right pane.
693     *
694     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
695     *     Must never be {@link #NO_ACCOUNT}.
696     */
697    public void openAccount(long accountId) {
698        mMailboxStack.clear();
699        updateActionBar();
700        open(accountId, NO_MAILBOX, NO_MESSAGE);
701    }
702
703    /**
704     * Opens the given mailbox. on two-pane, this will update both the mailbox list and the
705     * message list.
706     *
707     * NOTE: It's assumed that the mailbox is associated with the specified account. If the
708     * mailbox is not associated with the account, the behaviour is undefined.
709     */
710    private void openMailbox(long accountId, long mailboxId) {
711        updateActionBar();
712        updateMailboxList(accountId, mailboxId, true, true);
713        updateMessageList(mailboxId, true, true);
714    }
715
716    /**
717     * Loads the given account and optionally selects the given mailbox and message.  Used to open
718     * a particular view at a request from outside of the activity, such as the widget.
719     *
720     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
721     *     Must never be {@link #NO_ACCOUNT}.
722     * @param mailboxId ID of the mailbox to load. If {@link #NO_MAILBOX}, load the account's inbox.
723     * @param messageId ID of the message to load. If {@link #NO_MESSAGE}, do not open a message.
724     */
725    public void open(long accountId, long mailboxId, long messageId) {
726        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
727            Log.d(Logging.LOG_TAG, "" + this + " open accountId=" + accountId
728                    + " mailboxId=" + mailboxId + " messageId=" + messageId);
729        }
730        if (accountId == NO_ACCOUNT) {
731            throw new IllegalArgumentException();
732        } else if (mailboxId == NO_MAILBOX) {
733            updateMailboxList(accountId, NO_MAILBOX, true, true);
734
735            // Show the appropriate message list
736            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
737                // When opening the Combined view, the right pane will be "combined inbox".
738                updateMessageList(Mailbox.QUERY_ALL_INBOXES, true, true);
739            } else {
740                // Try to find the inbox for the account
741                closeMailboxFinder();
742                mMailboxFinder = new MailboxFinder(mActivity, mAccountId, Mailbox.TYPE_INBOX, this);
743                mMailboxFinder.startLookup();
744            }
745        } else if (messageId == NO_MESSAGE) {
746            // STOPSHIP Use the appropriate parent mailbox ID
747            updateMailboxList(accountId, mailboxId, true, true);
748            updateMessageList(mailboxId, true, true);
749        } else {
750            // STOPSHIP Use the appropriate parent mailbox ID
751            updateMailboxList(accountId, mailboxId, false, true);
752            updateMessageList(mailboxId, false, true);
753            updateMessageView(messageId);
754        }
755    }
756
757    /**
758     * Pre-fragment transaction check.
759     *
760     * @throw IllegalStateException if updateXxx methods can't be called in the current state.
761     */
762    private void preFragmentTransactionCheck() {
763        if (mHoldFragmentInstallation) {
764            // Code assumes mMailboxListFragment/etc are set right within the
765            // commitFragmentTransaction() call (because we use synchronous transaction),
766            // so updateXxx() can't be called if fragments are not installable yet.
767            throw new IllegalStateException();
768        }
769    }
770
771    /**
772     * Loads the given account and optionally selects the given mailbox and message. If the
773     * specified account is already selected, no actions will be performed unless
774     * <code>forceReload</code> is <code>true</code>.
775     *
776     * @param accountId ID of the account to load. Must never be {@link #NO_ACCOUNT}.
777     * @param parentMailboxId ID of the mailbox to use as the parent mailbox.  Pass
778     *     {@link #NO_MAILBOX} to show the root mailboxes.
779     * @param changeVisiblePane if true, the message view will be hidden.
780     * @param clearDependentPane if true, the message list and the message view will be cleared
781     */
782
783    // TODO The name "updateMailboxList" is misleading, as it also updates members such as
784    // mAccountId.  We need better structure but let's do that after refactoring
785    // MailboxListFragment.onMailboxSelected, and removed the UI callbacks such as
786    // TargetActivity.onAccountChanged.
787
788    private void updateMailboxList(long accountId, long parentMailboxId,
789            boolean changeVisiblePane, boolean clearDependentPane) {
790        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
791            Log.d(Logging.LOG_TAG, "" + this + " updateMailboxList accountId=" + accountId
792                    + " parentMailboxId=" + parentMailboxId);
793        }
794        preFragmentTransactionCheck();
795        if (accountId == NO_ACCOUNT) {
796            throw new InvalidParameterException();
797        }
798
799        // TODO Check if the current fragment has been initialized with the same parameters, and
800        // then return.
801
802        mAccountId = accountId;
803        mMailboxListMailboxId = parentMailboxId;
804
805        // Open mailbox list, remove message list / message view
806        final FragmentManager fm = mActivity.getFragmentManager();
807        final FragmentTransaction ft = fm.beginTransaction();
808        uninstallMailboxListFragment(ft);
809        if (clearDependentPane) {
810            mMessageId = NO_MESSAGE;
811            uninstallMessageListFragment(ft);
812            uninstallMessageViewFragment(ft);
813        }
814        ft.add(mThreePane.getLeftPaneId(),
815                MailboxListFragment.newInstance(getUIAccountId(), parentMailboxId));
816        commitFragmentTransaction(ft);
817
818        if (changeVisiblePane) {
819            mThreePane.showLeftPane();
820        }
821        updateRefreshProgress();
822    }
823
824    /**
825     * Go back to a mailbox list view. If a message view is currently active, it will
826     * be hidden.
827     */
828    private void goBackToMailbox() {
829        if (isMessageSelected()) {
830            mThreePane.showLeftPane(); // Show mailbox list
831        }
832    }
833
834    /**
835     * Selects the specified mailbox and optionally loads a message within it. If a message is
836     * not loaded, a list of the messages contained within the mailbox is shown. Otherwise the
837     * given message is shown. If <code>navigateToMailbox<code> is <code>true</code>, the
838     * mailbox is navigated to and any contained mailboxes are shown.
839     *
840     * @param mailboxId ID of the mailbox to load. Must never be <code>0</code> or <code>-1</code>.
841     * @param changeVisiblePane if true, the message view will be hidden.
842     * @param clearDependentPane if true, the message view will be cleared
843     */
844    private void updateMessageList(long mailboxId, boolean changeVisiblePane,
845            boolean clearDependentPane) {
846        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
847            Log.d(Logging.LOG_TAG, "" + this + " updateMessageList mMailboxId=" + mailboxId);
848        }
849        preFragmentTransactionCheck();
850        if (mailboxId == 0 || mailboxId == -1) {
851            throw new InvalidParameterException();
852        }
853
854        // TODO Check if the current fragment has been initialized with the same parameters, and
855        // then return.
856
857        final FragmentManager fm = mActivity.getFragmentManager();
858        final FragmentTransaction ft = fm.beginTransaction();
859        uninstallMessageListFragment(ft);
860        if (clearDependentPane) {
861            uninstallMessageViewFragment(ft);
862            mMessageId = NO_MESSAGE;
863        }
864        ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(mailboxId));
865        commitFragmentTransaction(ft);
866
867        if (changeVisiblePane) {
868            mThreePane.showLeftPane();
869        }
870
871        // TODO We shouldn't select the mailbox when we're updating the message list. These two
872        // functions should be done separately. Find a better location for this call to be done.
873        mMailboxListFragment.setSelectedMailbox(mailboxId);
874        updateRefreshProgress();
875    }
876
877    /**
878     * Show a message on the message view.
879     *
880     * @param messageId ID of the mailbox to load. Must never be {@link #NO_MESSAGE}.
881     */
882    private void updateMessageView(long messageId) {
883        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
884            Log.d(Logging.LOG_TAG, "" + this + " updateMessageView messageId=" + messageId);
885        }
886        preFragmentTransactionCheck();
887        if (messageId == NO_MESSAGE) {
888            throw new InvalidParameterException();
889        }
890
891        // TODO Check if the current fragment has been initialized with the same parameters, and
892        // then return.
893
894        // Update member
895        mMessageId = messageId;
896
897        // Open message
898        final FragmentManager fm = mActivity.getFragmentManager();
899        final FragmentTransaction ft = fm.beginTransaction();
900        uninstallMessageViewFragment(ft);
901        ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(messageId));
902        commitFragmentTransaction(ft);
903
904        mThreePane.showRightPane(); // Show message view
905
906        mMessageListFragment.setSelectedMessage(mMessageId);
907    }
908
909    private void closeMailboxFinder() {
910        if (mMailboxFinder != null) {
911            mMailboxFinder.cancel();
912            mMailboxFinder = null;
913        }
914    }
915
916    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
917        @Override
918        public void onMoveToNewer() {
919            moveToNewer();
920        }
921
922        @Override
923        public void onMoveToOlder() {
924            moveToOlder();
925        }
926    }
927
928    private void onCurrentMessageGone() {
929        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
930            case Preferences.AUTO_ADVANCE_NEWER:
931                if (moveToNewer()) return;
932                break;
933            case Preferences.AUTO_ADVANCE_OLDER:
934                if (moveToOlder()) return;
935                break;
936        }
937        // Last message in the box or AUTO_ADVANCE_MESSAGE_LIST.  Go back to message list.
938        goBackToMailbox();
939    }
940
941    /**
942     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
943     * the account has changed, and sync it to the current message.
944     */
945    private void updateMessageOrderManager() {
946        if (!isMailboxSelected()) {
947            return;
948        }
949        final long mailboxId = getMessageListMailboxId();
950        if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) {
951            stopMessageOrderManager();
952            mOrderManager =
953                new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback);
954        }
955        if (isMessageSelected()) {
956            mOrderManager.moveTo(getMessageId());
957        }
958    }
959
960    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
961        @Override
962        public void onMessagesChanged() {
963            updateNavigationArrows();
964        }
965
966        @Override
967        public void onMessageNotFound() {
968            // Current message gone.
969            goBackToMailbox();
970        }
971    }
972
973    /**
974     * Stop {@link MessageOrderManager}.
975     */
976    private void stopMessageOrderManager() {
977        if (mOrderManager != null) {
978            mOrderManager.close();
979            mOrderManager = null;
980        }
981    }
982
983    /**
984     * Disable/enable the move-to-newer/older buttons.
985     */
986    private void updateNavigationArrows() {
987        if (mOrderManager == null) {
988            // shouldn't happen, but just in case
989            mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0);
990        } else {
991            mMessageCommandButtons.enableNavigationButtons(
992                    mOrderManager.canMoveToNewer(), mOrderManager.canMoveToOlder(),
993                    mOrderManager.getCurrentPosition(), mOrderManager.getTotalMessageCount());
994        }
995    }
996
997    private boolean moveToOlder() {
998        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
999            updateMessageView(mOrderManager.getCurrentMessageId());
1000            return true;
1001        }
1002        return false;
1003    }
1004
1005    private boolean moveToNewer() {
1006        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
1007            updateMessageView(mOrderManager.getCurrentMessageId());
1008            return true;
1009        }
1010        return false;
1011    }
1012
1013    /**
1014     * Load account list for the action bar.
1015     *
1016     * If there's only one account configured, show the account name in the action bar.
1017     * If more than one account are configured, show a spinner in the action bar, and select the
1018     * current account.
1019     */
1020    private void loadAccounts() {
1021        if (mAccountsSelectorAdapter == null) {
1022            mAccountsSelectorAdapter = new AccountSelectorAdapter(mActivity);
1023        }
1024        mActivity.getLoaderManager().initLoader(LOADER_ID_ACCOUNT_LIST, null,
1025                new LoaderCallbacks<Cursor>() {
1026            @Override
1027            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1028                return AccountSelectorAdapter.createLoader(mActivity);
1029            }
1030
1031            @Override
1032            public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1033                updateAccountList(data);
1034            }
1035
1036            @Override
1037            public void onLoaderReset(Loader<Cursor> loader) {
1038                mAccountsSelectorAdapter.swapCursor(null);
1039            }
1040        });
1041    }
1042
1043    /**
1044     * Called when the LOADER_ID_ACCOUNT_LIST loader loads the data.  Update the account spinner
1045     * on the action bar.
1046     */
1047    private void updateAccountList(Cursor accountsCursor) {
1048        final int count = accountsCursor.getCount();
1049        if (count == 0) {
1050            // Open Welcome, which in turn shows the adding a new account screen.
1051            Welcome.actionStart(mActivity);
1052            mActivity.finish();
1053            return;
1054        }
1055
1056        // If ony one acount, don't show dropdown.
1057        final ActionBar ab = mActionBar;
1058        if (count == 1) {
1059            accountsCursor.moveToFirst();
1060
1061            // Show the account name as the title.
1062            ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
1063            ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1064            ab.setTitle(AccountSelectorAdapter.getAccountDisplayName(accountsCursor));
1065            return;
1066        }
1067
1068        // Find the currently selected account, and select it.
1069        int defaultSelection = 0;
1070        if (isAccountSelected()) {
1071            accountsCursor.moveToPosition(-1);
1072            int i = 0;
1073            while (accountsCursor.moveToNext()) {
1074                final long accountId = AccountSelectorAdapter.getAccountId(accountsCursor);
1075                if (accountId == getUIAccountId()) {
1076                    defaultSelection = i;
1077                    break;
1078                }
1079                i++;
1080            }
1081        }
1082
1083        // Update the dropdown list.
1084        mAccountsSelectorAdapter.swapCursor(accountsCursor);
1085
1086        // Don't show the title.
1087        ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
1088        ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1089        ab.setListNavigationCallbacks(mAccountsSelectorAdapter, mActionBarNavigationCallback);
1090        ab.setSelectedNavigationItem(defaultSelection);
1091    }
1092
1093    private class ActionBarNavigationCallback implements ActionBar.OnNavigationListener {
1094        @Override
1095        public boolean onNavigationItemSelected(int itemPosition, long accountId) {
1096            // TODO openAccount should do the check eventually, but it's necessary for now.
1097            if (accountId != getUIAccountId()) {
1098                openAccount(accountId);
1099            }
1100            return true;
1101        }
1102    }
1103
1104    /**
1105     * Handles {@link android.app.Activity#onCreateOptionsMenu} callback.
1106     */
1107    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
1108        inflater.inflate(R.menu.message_list_xl_option, menu);
1109        return true;
1110    }
1111
1112    /**
1113     * Handles {@link android.app.Activity#onPrepareOptionsMenu} callback.
1114     */
1115    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
1116        ActivityHelper.updateRefreshMenuIcon(menu.findItem(R.id.refresh),
1117                isRefreshEnabled(),
1118                isRefreshInProgress());
1119        return true;
1120    }
1121
1122    /**
1123     * Handles {@link android.app.Activity#onOptionsItemSelected} callback.
1124     *
1125     * @return true if the option item is handled.
1126     */
1127    public boolean onOptionsItemSelected(MenuItem item) {
1128        switch (item.getItemId()) {
1129            case android.R.id.home:
1130                // Comes from the action bar when the app icon on the left is pressed.
1131                // It works like a back press, but it won't close the activity.
1132                return onBackPressed(false);
1133            case R.id.compose:
1134                return onCompose();
1135            case R.id.refresh:
1136                onRefresh();
1137                return true;
1138            case R.id.account_settings:
1139                return onAccountSettings();
1140        }
1141        return false;
1142    }
1143
1144    /**
1145     * Performs the back action.
1146     *
1147     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
1148     * <code>true</code> if it's caused by the "home" icon click on the action bar.
1149     */
1150    public boolean onBackPressed(boolean isSystemBackKey) {
1151        if (mThreePane.onBackPressed(isSystemBackKey)) {
1152            return true;
1153        } else if (!mMailboxStack.isEmpty()) {
1154            long mailboxId = mMailboxStack.pop();
1155            if (mailboxId == NO_MAILBOX) {
1156                // No mailbox; reload the top-level message list
1157                openAccount(mAccountId);
1158            } else {
1159                openMailbox(mAccountId, mailboxId);
1160            }
1161            return true;
1162        }
1163        return false;
1164    }
1165
1166    /**
1167     * Handles the "Compose" option item.  Opens the message compose activity.
1168     */
1169    private boolean onCompose() {
1170        if (!isAccountSelected()) {
1171            return false; // this shouldn't really happen
1172        }
1173        MessageCompose.actionCompose(mActivity, getActualAccountId());
1174        return true;
1175    }
1176
1177    /**
1178     * Handles the "Compose" option item.  Opens the settings activity.
1179     */
1180    private boolean onAccountSettings() {
1181        AccountSettingsXL.actionSettings(mActivity, getActualAccountId());
1182        return true;
1183    }
1184
1185    /**
1186     * Handles the "refresh" option item.  Opens the settings activity.
1187     */
1188    // TODO used by experimental code in the activity -- otherwise can be private.
1189    public void onRefresh() {
1190        // Cancel previously running instance if any.
1191        new RefreshTask(mTaskTracker, mActivity, getActualAccountId(),
1192                getMessageListMailboxId()).cancelPreviousAndExecuteParallel();
1193    }
1194
1195    /**
1196     * Start/stop the "refresh" animation on the action bar according to the current refresh state.
1197     *
1198     * (We start the animation if {@link UIControllerTwoPane#isRefreshInProgress} returns true,
1199     * and stop otherwise.)
1200     */
1201    private void updateRefreshProgress() {
1202        mActivity.invalidateOptionsMenu();
1203    }
1204
1205    private class RefreshListener
1206            implements RefreshManager.Listener {
1207        @Override
1208        public void onMessagingError(final long accountId, long mailboxId, final String message) {
1209            updateRefreshProgress();
1210        }
1211
1212        @Override
1213        public void onRefreshStatusChanged(long accountId, long mailboxId) {
1214            updateRefreshProgress();
1215        }
1216    }
1217
1218    /**
1219     * Class to handle refresh.
1220     *
1221     * When the user press "refresh",
1222     * <ul>
1223     *   <li>Refresh the current mailbox, if it's refreshable.  (e.g. don't refresh combined inbox,
1224     *       drafts, etc.
1225     *   <li>Refresh the mailbox list, if it hasn't been refreshed in the last
1226     *       {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
1227     *   <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last
1228     *       {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
1229     * </ul>
1230     */
1231    /* package */ static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> {
1232        private final Clock mClock;
1233        private final Context mContext;
1234        private final long mAccountId;
1235        private final long mMailboxId;
1236        private final RefreshManager mRefreshManager;
1237        /* package */ long mInboxId;
1238
1239        public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
1240                long mailboxId) {
1241            this(tracker, context, accountId, mailboxId, Clock.INSTANCE,
1242                    RefreshManager.getInstance(context));
1243        }
1244
1245        /* package */ RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
1246                long mailboxId, Clock clock, RefreshManager refreshManager) {
1247            super(tracker);
1248            mClock = clock;
1249            mContext = context;
1250            mRefreshManager = refreshManager;
1251            mAccountId = accountId;
1252            mMailboxId = mailboxId;
1253        }
1254
1255        /**
1256         * Do DB access on a worker thread.
1257         */
1258        @Override
1259        protected Boolean doInBackground(Void... params) {
1260            mInboxId = Account.getInboxId(mContext, mAccountId);
1261            return Mailbox.isRefreshable(mContext, mMailboxId);
1262        }
1263
1264        /**
1265         * Do the actual refresh.
1266         */
1267        @Override
1268        protected void onPostExecute(Boolean isCurrentMailboxRefreshable) {
1269            if (isCancelled() || isCurrentMailboxRefreshable == null) {
1270                return;
1271            }
1272            if (isCurrentMailboxRefreshable) {
1273                mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false);
1274            }
1275            // Refresh mailbox list
1276            if (mAccountId != -1) {
1277                if (shouldRefreshMailboxList()) {
1278                    mRefreshManager.refreshMailboxList(mAccountId);
1279                }
1280            }
1281            // Refresh inbox
1282            if (shouldAutoRefreshInbox()) {
1283                mRefreshManager.refreshMessageList(mAccountId, mInboxId, false);
1284            }
1285        }
1286
1287        /**
1288         * @return true if the mailbox list of the current account hasn't been refreshed
1289         * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
1290         */
1291        /* package */ boolean shouldRefreshMailboxList() {
1292            if (mRefreshManager.isMailboxListRefreshing(mAccountId)) {
1293                return false;
1294            }
1295            final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId)
1296                    + MAILBOX_REFRESH_MIN_INTERVAL;
1297            if (nextRefreshTime > mClock.getTime()) {
1298                return false;
1299            }
1300            return true;
1301        }
1302
1303        /**
1304         * @return true if the inbox of the current account hasn't been refreshed
1305         * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
1306         */
1307        /* package */ boolean shouldAutoRefreshInbox() {
1308            if (mInboxId == mMailboxId) {
1309                return false; // Current ID == inbox.  No need to auto-refresh.
1310            }
1311            if (mRefreshManager.isMessageListRefreshing(mInboxId)) {
1312                return false;
1313            }
1314            final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId)
1315                    + INBOX_AUTO_REFRESH_MIN_INTERVAL;
1316            if (nextRefreshTime > mClock.getTime()) {
1317                return false;
1318            }
1319            return true;
1320        }
1321    }
1322}
1323