TwoPaneController.java revision e5f4dc0661dad02e2cb39ffa62ff2157147ac387
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.FragmentTransaction;
36import android.database.Cursor;
37import android.os.Bundle;
38import android.view.MenuItem;
39
40/**
41 * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
42 * limited.
43 */
44
45// Called OnePaneActivityController in Gmail.
46public final class TwoPaneController extends AbstractActivityController {
47    private boolean mJumpToFirstConversation;
48    private TwoPaneLayout mLayout;
49    private final ActionCompleteListener mDeleteListener = new TwoPaneDestructiveActionListener(
50            R.id.delete);
51    private final ActionCompleteListener mArchiveListener = new TwoPaneDestructiveActionListener(
52            R.id.archive);
53    private final ActionCompleteListener mMuteListener = new TwoPaneDestructiveActionListener(
54            R.id.mute);
55    private final ActionCompleteListener mSpamListener = new TwoPaneDestructiveActionListener(
56            R.id.report_spam);
57    private final TwoPaneDestructiveActionListener mFolderChangeListener =
58            new TwoPaneDestructiveActionListener(R.id.change_folder);
59
60    /**
61     * @param activity
62     * @param viewMode
63     */
64    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
65        super(activity, viewMode);
66    }
67
68    /**
69     * Display the conversation list fragment.
70     * @param show
71     */
72    private void initializeConversationListFragment(boolean show) {
73        if (show) {
74            if (mConvListContext != null && mConvListContext.isSearchResult()) {
75                mViewMode.enterSearchResultsListMode();
76            } else {
77                mViewMode.enterConversationListMode();
78            }
79        }
80        renderConversationList();
81    }
82
83    /**
84     * Render the conversation list in the correct pane.
85     */
86    private void renderConversationList() {
87        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
88        // Use cross fading animation.
89        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
90        Fragment conversationListFragment = ConversationListFragment
91                .newInstance(mConvListContext);
92        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment);
93        fragmentTransaction.commitAllowingStateLoss();
94    }
95
96    /**
97     * Render the folder list in the correct pane.
98     */
99    private void renderFolderList() {
100        FolderListFragment folderListFragment = FolderListFragment.newInstance(this,
101                mAccount.folderListUri);
102        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
103        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
104        fragmentTransaction.replace(R.id.folders_pane, folderListFragment);
105        fragmentTransaction.commitAllowingStateLoss();
106        // Since we are showing the folder list, we are at the start of the view
107        // stack.
108        resetActionBarIcon();
109        attachFolderList(folderListFragment);
110        if (getCurrentListContext() != null) {
111            folderListFragment.selectFolder(getCurrentListContext().folder);
112        }
113    }
114
115    @Override
116    protected boolean isConversationListVisible() {
117        // TODO(viki): Auto-generated method stub
118        return false;
119    }
120
121    @Override
122    public void showConversationList(ConversationListContext context) {
123        initializeConversationListFragment(true);
124    }
125
126    @Override
127    public void showFolderList() {
128        // On two-pane layouts, showing the folder list takes you to the top level of the
129        // application, which is the same as pressing the Up button
130        onUpPressed();
131    }
132
133    @Override
134    public boolean onCreate(Bundle savedState) {
135        mActivity.setContentView(R.layout.two_pane_activity);
136        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
137        if (mLayout == null) {
138            LogUtils.d(LOG_TAG, "mLayout is null!");
139        }
140        mLayout.initializeLayout(mActivity.getApplicationContext());
141
142        // The tablet layout needs to refer to mode changes.
143        mViewMode.addListener(mLayout);
144        // The activity controller needs to listen to layout changes.
145        mLayout.setListener(this);
146        final boolean isParentInitialized = super.onCreate(savedState);
147        return isParentInitialized;
148    }
149
150    @Override
151    public void onAccountChanged(Account account) {
152        super.onAccountChanged(account);
153        renderFolderList();
154    }
155
156    @Override
157    public void onFolderChanged(Folder folder) {
158        super.onFolderChanged(folder);
159        if (mFolderListFragment != null) {
160            mFolderListFragment.selectFolder(folder);
161        }
162    }
163
164    @Override
165    public void onViewModeChanged(int newMode) {
166        super.onViewModeChanged(newMode);
167        if (newMode != ViewMode.CONVERSATION) {
168            // Clear this flag if the user jumps out of conversation mode
169            // before a load completes.
170            mJumpToFirstConversation = false;
171        }
172        resetActionBarIcon();
173    }
174
175    @Override
176    public void resetActionBarIcon() {
177        if (mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
178            mActionBarView.removeBackButton();
179        } else {
180            mActionBarView.setBackButton();
181        }
182    }
183
184    @Override
185    public void showConversation(Conversation conversation) {
186        super.showConversation(conversation);
187        int mode = mViewMode.getMode();
188        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
189            mViewMode.enterSearchResultsConversationMode();
190            unhideConversationList();
191        } else {
192            mViewMode.enterConversationMode();
193        }
194        Fragment convFragment = ConversationViewFragment.newInstance(mAccount, conversation);
195        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
196        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
197        fragmentTransaction.replace(R.id.conversation_pane, convFragment);
198        fragmentTransaction.commitAllowingStateLoss();
199    }
200
201    /**
202     * Show the conversation list if it can be shown in the current orientation.
203     * @return true if the conversation list was shown
204     */
205    private boolean unhideConversationList() {
206        // Find if the conversation list can be shown
207        int mode = mViewMode.getMode();
208        final boolean isConversationListShowable = (mode == ViewMode.CONVERSATION
209                && mLayout.isConversationListCollapsible()
210                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION));
211        if (isConversationListShowable) {
212            return mLayout.uncollapseList();
213        }
214        return false;
215    }
216
217    /**
218     * Up works as follows:
219     * 1) If the user is in a conversation and:
220     *  a) the conversation list is hidden (portrait mode), shows the conv list and
221     *  stays in conversation view mode.
222     *  b) the conversation list is shown, goes back to conversation list mode.
223     * 2) If the user is in search results, up exits search.
224     * mode and returns the user to whatever view they were in when they began search.
225     * 3) If the user is in conversation list mode, there is no up.
226     */
227    @Override
228    public boolean onUpPressed() {
229        int mode = mViewMode.getMode();
230        if (mode == ViewMode.CONVERSATION) {
231            if (!mLayout.isConversationListVisible()) {
232                unhideConversationList();
233            } else {
234                mActivity.onBackPressed();
235            }
236        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
237            if (!mLayout.isConversationListVisible()) {
238                unhideConversationList();
239            } else {
240                mActivity.finish();
241            }
242        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
243            mActivity.finish();
244        }
245        return true;
246    }
247
248    @Override
249    public boolean onBackPressed() {
250        popView(false);
251        return true;
252    }
253
254    /**
255     * Pops the "view stack" to the last screen the user was viewing.
256     *
257     * @param preventClose Whether to prevent closing the app if the stack is empty.
258     */
259    protected void popView(boolean preventClose) {
260        // If the user is in search query entry mode, or the user is viewing search results, exit
261        // the mode.
262        int mode = mViewMode.getMode();
263        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
264            mActivity.finish();
265        } else if (mViewMode.getMode() == ViewMode.CONVERSATION) {
266            // Go to conversation list.
267            mViewMode.enterConversationListMode();
268        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
269            mViewMode.enterSearchResultsListMode();
270        } else {
271            // There is nothing else to pop off the stack.
272            if (!preventClose) {
273                mActivity.finish();
274            }
275        }
276    }
277
278    @Override
279    public boolean shouldShowFirstConversation() {
280        return mConvListContext != null && mConvListContext.isSearchResult();
281    }
282
283    @Override
284    public boolean onOptionsItemSelected(MenuItem item) {
285        boolean handled = true;
286        final int id = item.getItemId();
287        switch (id) {
288            case R.id.y_button: {
289                final Settings settings = mActivity.getSettings();
290                final boolean showDialog = (settings != null && settings.confirmArchive);
291                confirmAndDelete(showDialog, R.plurals.confirm_archive_conversation,
292                        mArchiveListener);
293                break;
294            }
295            case R.id.delete: {
296                final Settings settings = mActivity.getSettings();
297                final boolean showDialog = (settings != null && settings.confirmDelete);
298                confirmAndDelete(showDialog, R.plurals.confirm_delete_conversation,
299                        mDeleteListener);
300                break;
301            }
302            case R.id.change_folders:
303                new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
304                        Collections.singletonList(mCurrentConversation)).show();
305                break;
306            case R.id.inside_conversation_unread:
307                updateCurrentConversation(ConversationColumns.READ, false);
308                break;
309            case R.id.mark_important:
310                updateCurrentConversation(ConversationColumns.PRIORITY,
311                        UIProvider.ConversationPriority.HIGH);
312                break;
313            case R.id.mark_not_important:
314                updateCurrentConversation(ConversationColumns.PRIORITY,
315                        UIProvider.ConversationPriority.LOW);
316                break;
317            case R.id.mute:
318                mConversationListFragment.requestDelete(mMuteListener);
319                break;
320            case R.id.report_spam:
321                mConversationListFragment.requestDelete(mSpamListener);
322                break;
323            default:
324                handled = false;
325                break;
326        }
327        return handled || super.onOptionsItemSelected(item);
328    }
329
330    /**
331     * An object that performs an action on the conversation database. This is an
332     * ActionCompleteListener since this is called <b>after</a> the conversation list has animated
333     * the conversation away. Once the animation is completed, the {@link #onActionComplete()}
334     * method is called which performs the correct data operation.
335     */
336    private class TwoPaneDestructiveActionListener extends DestructiveActionListener {
337        public TwoPaneDestructiveActionListener(int action) {
338            super(action);
339        }
340
341        @Override
342        public void onActionComplete() {
343            final ArrayList<Conversation> single = new ArrayList<Conversation>();
344            single.add(mCurrentConversation);
345            int next = -1;
346            int pref = getAutoAdvanceSetting(mActivity);
347            Cursor c = mConversationListFragment.getConversationListCursor();
348            int updatedPosition = -1;
349            int position = mCurrentConversation.position;
350            if (c != null) {
351                switch (pref) {
352                    case AutoAdvance.NEWER:
353                        if (position - 1 >= 0) {
354                            // This conversation was deleted, so to get to the previous
355                            // conversation, show what is now in its position - 1.
356                            next = position - 1;
357                            // The position is correct, since no items before this have
358                            // been deleted.
359                            updatedPosition = position - 1;
360                        }
361                        break;
362                    case AutoAdvance.OLDER:
363                        if (position + 1 < c.getCount()) {
364                            // This conversation was deleted, so to get to the next
365                            // conversation, show what is now in position + 1.
366                            next = position + 1;
367                            // Since this conversation was deleted, update the conversation
368                            // we are showing to have the position this conversation was in.
369                            updatedPosition = position;
370                        }
371                        break;
372                }
373            }
374            mConversationListFragment.onActionComplete();
375            mConversationListFragment.onUndoAvailable(new UndoOperation(1, mAction));
376            if (next != -1) {
377                mConversationListFragment.viewConversation(next);
378                mCurrentConversation.position = updatedPosition;
379            } else {
380                onBackPressed();
381            }
382            performConversationAction(single);
383            mConversationListFragment.requestListRefresh();
384        }
385    }
386
387    protected void requestDelete(final ActionCompleteListener listener) {
388        mConversationListFragment.requestDelete(listener);
389    }
390
391    @Override
392    protected DestructiveActionListener getFolderDestructiveActionListener() {
393        return mFolderChangeListener;
394    }
395}
396