TwoPaneController.java revision c7694221dfa5cec3f4ae290f2266b081b2639d80
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.database.Cursor;
24import android.net.Uri;
25import android.os.Bundle;
26import android.view.Gravity;
27import android.view.MenuItem;
28import android.widget.FrameLayout;
29
30import com.android.mail.ConversationListContext;
31import com.android.mail.R;
32import com.android.mail.providers.Account;
33import com.android.mail.providers.Conversation;
34import com.android.mail.providers.Folder;
35import com.android.mail.providers.Settings;
36import com.android.mail.providers.UIProvider;
37import com.android.mail.providers.UIProvider.AutoAdvance;
38import com.android.mail.providers.UIProvider.ConversationColumns;
39import com.android.mail.utils.LogUtils;
40
41import java.util.ArrayList;
42import java.util.Collections;
43
44/**
45 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
46 * abounds.
47 */
48
49// Called TwoPaneActivityController in Gmail.
50public final class TwoPaneController extends AbstractActivityController {
51    private TwoPaneLayout mLayout;
52    private final DestructiveAction mDeleteListener = new TwoPaneDestructiveAction(
53            R.id.delete);
54    private final DestructiveAction mArchiveListener = new TwoPaneDestructiveAction(
55            R.id.archive);
56    private final DestructiveAction mMuteListener = new TwoPaneDestructiveAction(
57            R.id.mute);
58    private final DestructiveAction mSpamListener = new TwoPaneDestructiveAction(
59            R.id.report_spam);
60    private final DestructiveAction mFolderChangeListener =
61            new TwoPaneDestructiveAction(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 onConversationVisibilityChanged(boolean visible) {
198        super.onConversationVisibilityChanged(visible);
199
200        if (!visible) {
201            mPagerController.hide();
202        }
203    }
204
205    @Override
206    public void resetActionBarIcon() {
207        if (mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
208            mActionBarView.removeBackButton();
209        } else {
210            mActionBarView.setBackButton();
211        }
212    }
213
214    @Override
215    public void showConversation(Conversation conversation) {
216        if (mActivity == null) {
217            return;
218        }
219        super.showConversation(conversation);
220        int mode = mViewMode.getMode();
221        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
222            mViewMode.enterSearchResultsConversationMode();
223            unhideConversationList();
224        } else {
225            mViewMode.enterConversationMode();
226        }
227
228        mPagerController.show(mAccount, mFolder, conversation);
229    }
230
231    @Override
232    public void showWaitForInitialization() {
233        super.showWaitForInitialization();
234
235        Fragment waitFragment = WaitFragment.newInstance(mAccount);
236        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
237        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
238        fragmentTransaction.replace(R.id.two_pane_activity, waitFragment, TAG_WAIT);
239        fragmentTransaction.commitAllowingStateLoss();
240    }
241
242    @Override
243    public void hideWaitForInitialization() {
244        final FragmentManager manager = mActivity.getFragmentManager();
245        final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
246        if (waitFragment != null) {
247            FragmentTransaction fragmentTransaction =
248                    mActivity.getFragmentManager().beginTransaction();
249            fragmentTransaction.remove(waitFragment);
250            fragmentTransaction.commitAllowingStateLoss();
251        }
252    }
253
254    /**
255     * Show the conversation list if it can be shown in the current orientation.
256     * @return true if the conversation list was shown
257     */
258    private boolean unhideConversationList() {
259        // Find if the conversation list can be shown
260        int mode = mViewMode.getMode();
261        final boolean isConversationListShowable = (mode == ViewMode.CONVERSATION
262                && mLayout.isConversationListCollapsible()
263                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION));
264        if (isConversationListShowable) {
265            return mLayout.uncollapseList();
266        }
267        return false;
268    }
269
270    /**
271     * Up works as follows:
272     * 1) If the user is in a conversation and:
273     *  a) the conversation list is hidden (portrait mode), shows the conv list and
274     *  stays in conversation view mode.
275     *  b) the conversation list is shown, goes back to conversation list mode.
276     * 2) If the user is in search results, up exits search.
277     * mode and returns the user to whatever view they were in when they began search.
278     * 3) If the user is in conversation list mode, there is no up.
279     */
280    @Override
281    public boolean onUpPressed() {
282        int mode = mViewMode.getMode();
283        if (mode == ViewMode.CONVERSATION) {
284            if (!mLayout.isConversationListVisible()) {
285                commitLeaveBehindItems();
286                unhideConversationList();
287            } else {
288                mActivity.onBackPressed();
289            }
290        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
291            if (!mLayout.isConversationListVisible()) {
292                commitLeaveBehindItems();
293                unhideConversationList();
294            } else {
295                mActivity.finish();
296            }
297        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
298            mActivity.finish();
299        } else if (mode == ViewMode.CONVERSATION_LIST) {
300            // This case can only happen if the user is looking at child folders.
301            createFolderListFragment(null, mAccount.folderListUri);
302            loadAccountInbox();
303        }
304        return true;
305    }
306
307    @Override
308    public boolean onBackPressed() {
309        // Clear any visible undo bars.
310        mUndoBarView.hide(false);
311        popView(false);
312        return true;
313    }
314
315    /**
316     * Pops the "view stack" to the last screen the user was viewing.
317     *
318     * @param preventClose Whether to prevent closing the app if the stack is empty.
319     */
320    protected void popView(boolean preventClose) {
321        // If the user is in search query entry mode, or the user is viewing search results, exit
322        // the mode.
323        int mode = mViewMode.getMode();
324        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
325            mActivity.finish();
326        } else if (mViewMode.getMode() == ViewMode.CONVERSATION) {
327            // Go to conversation list.
328            mViewMode.enterConversationListMode();
329        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
330            mViewMode.enterSearchResultsListMode();
331        } else {
332            // There is nothing else to pop off the stack.
333            if (!preventClose) {
334                mActivity.finish();
335            }
336        }
337    }
338
339    @Override
340    public boolean shouldShowFirstConversation() {
341        return mConvListContext != null && mConvListContext.isSearchResult();
342    }
343
344    @Override
345    public boolean onOptionsItemSelected(MenuItem item) {
346        boolean handled = true;
347        final int id = item.getItemId();
348        switch (id) {
349            case R.id.y_button: {
350                final boolean showDialog =
351                        (mCachedSettings != null && mCachedSettings.confirmArchive);
352                confirmAndDelete(showDialog, R.plurals.confirm_archive_conversation,
353                        mArchiveListener);
354                break;
355            }
356            case R.id.delete: {
357                final boolean showDialog =
358                        (mCachedSettings != null && mCachedSettings.confirmDelete);
359                confirmAndDelete(showDialog, R.plurals.confirm_delete_conversation,
360                        mDeleteListener);
361                break;
362            }
363            case R.id.change_folders:
364                new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
365                        Collections.singletonList(mCurrentConversation)).show();
366                break;
367            case R.id.inside_conversation_unread:
368                updateCurrentConversation(ConversationColumns.READ, false);
369                break;
370            case R.id.mark_important:
371                updateCurrentConversation(ConversationColumns.PRIORITY,
372                        UIProvider.ConversationPriority.HIGH);
373                break;
374            case R.id.mark_not_important:
375                updateCurrentConversation(ConversationColumns.PRIORITY,
376                        UIProvider.ConversationPriority.LOW);
377                break;
378            case R.id.mute:
379                ConversationListFragment convList = getConversationListFragment();
380                if (convList != null) {
381                    convList.requestDelete(mMuteListener);
382                }
383                break;
384            case R.id.report_spam:
385                convList = getConversationListFragment();
386                if (convList != null) {
387                    convList.requestDelete(mSpamListener);
388                }
389                break;
390            default:
391                handled = false;
392                break;
393        }
394        return handled || super.onOptionsItemSelected(item);
395    }
396
397    /**
398     * An object that performs an action on the conversation database. This is a
399     * {@link DestructiveAction}: this is called <b>after</a> the conversation list has animated
400     * the conversation away. Once the animation is completed, the {@link #performAction()}
401     * method is called which performs the correct data operation.
402     */
403    private class TwoPaneDestructiveAction extends AbstractDestructiveAction {
404        public TwoPaneDestructiveAction(int action) {
405            super(action);
406        }
407
408        @Override
409        public void performAction() {
410            final ArrayList<Conversation> single = new ArrayList<Conversation>();
411            single.add(mCurrentConversation);
412            final Conversation nextConversation = mTracker.getNextConversation(mCachedSettings);
413            TwoPaneController.this.performAction();
414            final ConversationListFragment convList = getConversationListFragment();
415            if (nextConversation != null) {
416                // We have a conversation to auto advance to
417                if (convList != null) {
418                    convList.viewConversation(nextConversation.position);
419                }
420                onUndoAvailable(new UndoOperation(1, mAction));
421            } else {
422                // We don't have a conversation to show: show conversation list instead.
423                onBackPressed();
424                mHandler.post(new Runnable() {
425                    @Override
426                    public void run() {
427                        onUndoAvailable(new UndoOperation(1, mAction));
428                    }
429                });
430            }
431            performConversationAction(single);
432            if (convList != null) {
433                convList.requestListRefresh();
434            }
435        }
436    }
437
438    @Override
439    protected void requestDelete(final DestructiveAction listener) {
440        final ConversationListFragment convList = getConversationListFragment();
441        if (convList != null) {
442            convList.requestDelete(listener);
443        }
444    }
445
446    @Override
447    public DestructiveAction getFolderDestructiveAction() {
448        return mFolderChangeListener;
449    }
450
451    @Override
452    public void onUndoAvailable(UndoOperation op) {
453        int mode = mViewMode.getMode();
454        FrameLayout.LayoutParams params;
455        final ConversationListFragment convList = getConversationListFragment();
456        switch (mode) {
457            case ViewMode.CONVERSATION_LIST:
458                params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
459                params.width = mLayout.computeConversationListWidth();
460                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
461                mUndoBarView.setLayoutParams(params);
462                if (convList != null) {
463                    mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
464                        convList.getAnimatedAdapter(), mConversationListCursor);
465                }
466                break;
467            case ViewMode.CONVERSATION:
468                if (op.mBatch) {
469                    // Show undo bar in the conversation list.
470                    params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
471                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
472                    params.width = mLayout.computeConversationListWidth();
473                } else {
474                    // Show undo bar in the conversation.
475                    params = (FrameLayout.LayoutParams) mUndoBarView.getLayoutParams();
476                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
477                    params.width = mLayout.getConversationView().getWidth();
478                }
479                mUndoBarView.setLayoutParams(params);
480                mUndoBarView.show(true, mActivity.getActivityContext(), op, mAccount,
481                        convList.getAnimatedAdapter(), null);
482                break;
483        }
484    }
485}
486