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