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