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