TwoPaneController.java revision 7d81600cbce3cfd366cbff9ecd1b7317ff957221
1/*******************************************************************************
2 *      Copyright (C) 2012 Google Inc.
3 *      Licensed to The Android Open Source Project.
4 *
5 *      Licensed under the Apache License, Version 2.0 (the "License");
6 *      you may not use this file except in compliance with the License.
7 *      You may obtain a copy of the License at
8 *
9 *           http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *      Unless required by applicable law or agreed to in writing, software
12 *      distributed under the License is distributed on an "AS IS" BASIS,
13 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *      See the License for the specific language governing permissions and
15 *      limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
20import com.android.mail.ConversationListContext;
21import com.android.mail.R;
22import com.android.mail.providers.Account;
23import com.android.mail.providers.Conversation;
24import com.android.mail.providers.Folder;
25import com.android.mail.providers.Settings;
26import com.android.mail.providers.UIProvider;
27import com.android.mail.providers.UIProvider.AutoAdvance;
28import com.android.mail.providers.UIProvider.ConversationColumns;
29import com.android.mail.utils.LogUtils;
30
31import java.util.ArrayList;
32import java.util.Collections;
33
34import android.app.Fragment;
35import android.app.FragmentManager;
36import android.app.FragmentTransaction;
37import android.database.Cursor;
38import android.net.Uri;
39import android.os.Bundle;
40import android.view.Gravity;
41import android.view.MenuItem;
42import android.widget.FrameLayout;
43
44/**
45 * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
46 * limited.
47 */
48
49// Called OnePaneActivityController in Gmail.
50public final class TwoPaneController extends AbstractActivityController {
51    private TwoPaneLayout mLayout;
52    private final ActionCompleteListener mDeleteListener = new TwoPaneDestructiveActionListener(
53            R.id.delete);
54    private final ActionCompleteListener mArchiveListener = new TwoPaneDestructiveActionListener(
55            R.id.archive);
56    private final ActionCompleteListener mMuteListener = new TwoPaneDestructiveActionListener(
57            R.id.mute);
58    private final ActionCompleteListener mSpamListener = new TwoPaneDestructiveActionListener(
59            R.id.report_spam);
60    private final TwoPaneDestructiveActionListener mFolderChangeListener =
61            new TwoPaneDestructiveActionListener(R.id.change_folder);
62
63    /**
64     * @param activity
65     * @param viewMode
66     */
67    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
68        super(activity, viewMode);
69    }
70
71    /**
72     * Display the conversation list fragment.
73     * @param show
74     */
75    private void initializeConversationListFragment(boolean show) {
76        if (show) {
77            if (mConvListContext != null && mConvListContext.isSearchResult()) {
78                mViewMode.enterSearchResultsListMode();
79            } else {
80                mViewMode.enterConversationListMode();
81            }
82        }
83        renderConversationList();
84    }
85
86    /**
87     * Render the conversation list in the correct pane.
88     */
89    private void renderConversationList() {
90        if (mActivity == null) {
91            return;
92        }
93        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
94        // Use cross fading animation.
95        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
96        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
97        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
98                TAG_CONVERSATION_LIST);
99        fragmentTransaction.commitAllowingStateLoss();
100    }
101
102    /**
103     * Render the folder list in the correct pane.
104     */
105    private void renderFolderList() {
106        if (mActivity == null) {
107            return;
108        }
109        createFolderListFragment(null, mAccount.folderListUri);
110    }
111
112    private void createFolderListFragment(Folder parent, Uri uri) {
113        FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri);
114        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
115        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
116        fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST);
117        fragmentTransaction.commitAllowingStateLoss();
118        // Since we are showing the folder list, we are at the start of the view
119        // stack.
120        resetActionBarIcon();
121        if (getCurrentListContext() != null) {
122            folderListFragment.selectFolder(getCurrentListContext().folder);
123        }
124    }
125
126    @Override
127    protected boolean isConversationListVisible() {
128        // TODO(viki): Auto-generated method stub
129        return false;
130    }
131
132    @Override
133    public void showConversationList(ConversationListContext listContext) {
134        super.showConversationList(listContext);
135        initializeConversationListFragment(true);
136    }
137
138    @Override
139    public void showFolderList() {
140        // On two-pane layouts, showing the folder list takes you to the top level of the
141        // application, which is the same as pressing the Up button
142        onUpPressed();
143    }
144
145    @Override
146    public boolean onCreate(Bundle savedState) {
147        mActivity.setContentView(R.layout.two_pane_activity);
148        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
149        if (mLayout == null) {
150            LogUtils.d(LOG_TAG, "mLayout is null!");
151        }
152        mLayout.initializeLayout(mActivity.getApplicationContext());
153
154        // The tablet layout needs to refer to mode changes.
155        mViewMode.addListener(mLayout);
156        // The activity controller needs to listen to layout changes.
157        mLayout.setListener(this);
158        final boolean isParentInitialized = super.onCreate(savedState);
159        return isParentInitialized;
160    }
161
162    @Override
163    public void onAccountChanged(Account account) {
164        super.onAccountChanged(account);
165        renderFolderList();
166    }
167
168    @Override
169    public void onFolderSelected(Folder folder, boolean childView) {
170        if (!childView && folder.hasChildren) {
171            // Replace this fragment with a new FolderListFragment
172            // showing this folder's children if we are not already looking
173            // at the child view for this folder.
174            createFolderListFragment(folder, folder.childFoldersListUri);
175            // Show the up affordance when digging into child folders.
176            mActionBarView.setBackButton();
177            return;
178        }
179        final FolderListFragment folderList = getFolderListFragment();
180        if (folderList != null) {
181            folderList.selectFolder(folder);
182        }
183        super.onFolderChanged(folder);
184    }
185
186    @Override
187    public void onViewModeChanged(int newMode) {
188        super.onViewModeChanged(newMode);
189        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
190            // Clear the wait fragment
191            hideWaitForInitialization();
192        }
193        resetActionBarIcon();
194    }
195
196    @Override
197    public void resetActionBarIcon() {
198        if (mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
199            mActionBarView.removeBackButton();
200        } else {
201            mActionBarView.setBackButton();
202        }
203    }
204
205    @Override
206    public void showConversation(Conversation conversation) {
207        if (mActivity == null) {
208            return;
209        }
210        super.showConversation(conversation);
211        int mode = mViewMode.getMode();
212        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
213            mViewMode.enterSearchResultsConversationMode();
214            unhideConversationList();
215        } else {
216            mViewMode.enterConversationMode();
217        }
218        Fragment convFragment = ConversationViewFragment.newInstance(mAccount, conversation,
219                mFolder);
220        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
221        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
222        fragmentTransaction.replace(R.id.conversation_pane, convFragment, TAG_CONVERSATION);
223        fragmentTransaction.commitAllowingStateLoss();
224    }
225
226    @Override
227    public void showWaitForInitialization() {
228        super.showWaitForInitialization();
229
230        Fragment waitFragment = WaitFragment.newInstance(mAccount);
231        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
232        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
233        fragmentTransaction.replace(R.id.two_pane_activity, waitFragment, TAG_WAIT);
234        fragmentTransaction.commitAllowingStateLoss();
235    }
236
237    @Override
238    public void hideWaitForInitialization() {
239        final FragmentManager manager = mActivity.getFragmentManager();
240        final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
241        if (waitFragment != null) {
242            FragmentTransaction fragmentTransaction =
243                    mActivity.getFragmentManager().beginTransaction();
244            fragmentTransaction.remove(waitFragment);
245            fragmentTransaction.commitAllowingStateLoss();
246        }
247    }
248
249    /**
250     * Show the conversation list if it can be shown in the current orientation.
251     * @return true if the conversation list was shown
252     */
253    private boolean unhideConversationList() {
254        // Find if the conversation list can be shown
255        int mode = mViewMode.getMode();
256        final boolean isConversationListShowable = (mode == ViewMode.CONVERSATION
257                && mLayout.isConversationListCollapsible()
258                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION));
259        if (isConversationListShowable) {
260            return mLayout.uncollapseList();
261        }
262        return false;
263    }
264
265    /**
266     * Up works as follows:
267     * 1) If the user is in a conversation and:
268     *  a) the conversation list is hidden (portrait mode), shows the conv list and
269     *  stays in conversation view mode.
270     *  b) the conversation list is shown, goes back to conversation list mode.
271     * 2) If the user is in search results, up exits search.
272     * mode and returns the user to whatever view they were in when they began search.
273     * 3) If the user is in conversation list mode, there is no up.
274     */
275    @Override
276    public boolean onUpPressed() {
277        int mode = mViewMode.getMode();
278        if (mode == ViewMode.CONVERSATION) {
279            if (!mLayout.isConversationListVisible()) {
280                commitLeaveBehindItems();
281                unhideConversationList();
282            } else {
283                mActivity.onBackPressed();
284            }
285        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
286            if (!mLayout.isConversationListVisible()) {
287                commitLeaveBehindItems();
288                unhideConversationList();
289            } else {
290                mActivity.finish();
291            }
292        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
293            mActivity.finish();
294        } else if (mode == ViewMode.CONVERSATION_LIST) {
295            // This case can only happen if the user is looking at child folders.
296            createFolderListFragment(null, mAccount.folderListUri);
297            loadAccountInbox();
298        }
299        return true;
300    }
301
302    @Override
303    public boolean onBackPressed() {
304        // Clear any visible undo bars.
305        mUndoBarView.hide(false);
306        popView(false);
307        return true;
308    }
309
310    /**
311     * Pops the "view stack" to the last screen the user was viewing.
312     *
313     * @param preventClose Whether to prevent closing the app if the stack is empty.
314     */
315    protected void popView(boolean preventClose) {
316        // If the user is in search query entry mode, or the user is viewing search results, exit
317        // the mode.
318        int mode = mViewMode.getMode();
319        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
320            mActivity.finish();
321        } else if (mViewMode.getMode() == ViewMode.CONVERSATION) {
322            // Go to conversation list.
323            mViewMode.enterConversationListMode();
324        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
325            mViewMode.enterSearchResultsListMode();
326        } else {
327            // There is nothing else to pop off the stack.
328            if (!preventClose) {
329                mActivity.finish();
330            }
331        }
332    }
333
334    @Override
335    public boolean shouldShowFirstConversation() {
336        return mConvListContext != null && mConvListContext.isSearchResult();
337    }
338
339    @Override
340    public boolean onOptionsItemSelected(MenuItem item) {
341        boolean handled = true;
342        final int id = item.getItemId();
343        switch (id) {
344            case R.id.y_button: {
345                final boolean showDialog =
346                        (mCachedSettings != null && mCachedSettings.confirmArchive);
347                confirmAndDelete(showDialog, R.plurals.confirm_archive_conversation,
348                        mArchiveListener);
349                break;
350            }
351            case R.id.delete: {
352                final boolean showDialog =
353                        (mCachedSettings != null && mCachedSettings.confirmDelete);
354                confirmAndDelete(showDialog, R.plurals.confirm_delete_conversation,
355                        mDeleteListener);
356                break;
357            }
358            case R.id.change_folders:
359                new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
360                        Collections.singletonList(mCurrentConversation)).show();
361                break;
362            case R.id.inside_conversation_unread:
363                updateCurrentConversation(ConversationColumns.READ, false);
364                break;
365            case R.id.mark_important:
366                updateCurrentConversation(ConversationColumns.PRIORITY,
367                        UIProvider.ConversationPriority.HIGH);
368                break;
369            case R.id.mark_not_important:
370                updateCurrentConversation(ConversationColumns.PRIORITY,
371                        UIProvider.ConversationPriority.LOW);
372                break;
373            case R.id.mute:
374                ConversationListFragment convList = getConversationListFragment();
375                if (convList != null) {
376                    convList.requestDelete(mMuteListener);
377                }
378                break;
379            case R.id.report_spam:
380                convList = getConversationListFragment();
381                if (convList != null) {
382                    convList.requestDelete(mSpamListener);
383                }
384                break;
385            default:
386                handled = false;
387                break;
388        }
389        return handled || super.onOptionsItemSelected(item);
390    }
391
392    /**
393     * An object that performs an action on the conversation database. This is an
394     * ActionCompleteListener since this is called <b>after</a> the conversation list has animated
395     * the conversation away. Once the animation is completed, the {@link #onActionComplete()}
396     * method is called which performs the correct data operation.
397     */
398    private class TwoPaneDestructiveActionListener extends DestructiveActionListener {
399        public TwoPaneDestructiveActionListener(int action) {
400            super(action);
401        }
402
403        @Override
404        public void onActionComplete() {
405            final ArrayList<Conversation> single = new ArrayList<Conversation>();
406            single.add(mCurrentConversation);
407            int next = -1;
408            final int pref = getAutoAdvanceSetting(mCachedSettings);
409            final Cursor c = mConversationListCursor;
410            int updatedPosition = -1;
411            final int position = mCurrentConversation.position;
412            if (c != null) {
413                switch (pref) {
414                    case AutoAdvance.NEWER:
415                        if (position - 1 >= 0) {
416                            // This conversation was deleted, so to get to the previous
417                            // conversation, show what is now in its position - 1.
418                            next = position - 1;
419                            // The position is correct, since no items before this have
420                            // been deleted.
421                            updatedPosition = position - 1;
422                        }
423                        break;
424                    case AutoAdvance.OLDER:
425                        if (position + 1 < c.getCount()) {
426                            // This conversation was deleted, so to get to the next
427                            // conversation, show what is now in position + 1.
428                            next = position + 1;
429                            // Since this conversation was deleted, update the conversation
430                            // we are showing to have the position this conversation was in.
431                            updatedPosition = position;
432                        }
433                        break;
434                }
435            }
436            TwoPaneController.this.onActionComplete();
437            final ConversationListFragment convList = getConversationListFragment();
438            if (next != -1) {
439                if (convList != null) {
440                    convList.viewConversation(next);
441                }
442                mCurrentConversation.position = updatedPosition;
443                onUndoAvailable(new UndoOperation(1, mAction));
444            } else {
445                onBackPressed();
446                mHandler.post(new Runnable() {
447                    @Override
448                    public void run() {
449                        onUndoAvailable(new UndoOperation(1, mAction));
450                    }
451                });
452            }
453            performConversationAction(single);
454            if (convList != null) {
455                convList.requestListRefresh();
456            }
457        }
458    }
459
460    @Override
461    protected void requestDelete(final ActionCompleteListener listener) {
462        final ConversationListFragment convList = getConversationListFragment();
463        if (convList != null) {
464            convList.requestDelete(listener);
465        }
466    }
467
468    @Override
469    protected DestructiveActionListener getFolderDestructiveActionListener() {
470        return mFolderChangeListener;
471    }
472
473    @Override
474    public void onUndoAvailable(UndoOperation op) {
475        int mode = mViewMode.getMode();
476        FrameLayout.LayoutParams params;
477        final ConversationListFragment convList = getConversationListFragment();
478        switch (mode) {
479            case ViewMode.CONVERSATION_LIST:
480                params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
481                params.width = mLayout.computeConversationListWidth();
482                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
483                mUndoBarView.setLayoutParams(params);
484                if (convList != null) {
485                    mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
486                        convList.getAnimatedAdapter());
487                }
488                break;
489            case ViewMode.CONVERSATION:
490                final ConversationViewFragment convView = getConversationViewFragment();
491                if (op.mBatch) {
492                    // Show undo bar in the conversation list.
493                    params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
494                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
495                    params.width = mLayout.computeConversationListWidth();
496                } else {
497                    // Show undo bar in the conversation.
498                    params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
499                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
500                    if (convView != null) {
501                        params.width = convView.getView().getWidth();
502                    }
503                }
504                mUndoBarView.setLayoutParams(params);
505                if (convView != null) {
506                    mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
507                        convList.getAnimatedAdapter());
508                }
509                break;
510        }
511    }
512}
513