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