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