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 18 package com.android.mail.ui; 19 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.net.Uri; 23 import android.os.AsyncTask; 24 import android.os.Bundle; 25 import androidx.appcompat.app.ActionBar; 26 import android.text.TextUtils; 27 import android.view.Menu; 28 import android.view.MenuItem; 29 30 import com.android.mail.R; 31 import com.android.mail.providers.Account; 32 import com.android.mail.providers.AccountObserver; 33 import com.android.mail.providers.Conversation; 34 import com.android.mail.providers.Folder; 35 import com.android.mail.providers.FolderObserver; 36 import com.android.mail.providers.UIProvider; 37 import com.android.mail.providers.UIProvider.AccountCapabilities; 38 import com.android.mail.providers.UIProvider.FolderCapabilities; 39 import com.android.mail.providers.UIProvider.FolderType; 40 import com.android.mail.utils.LogTag; 41 import com.android.mail.utils.LogUtils; 42 import com.android.mail.utils.Utils; 43 44 /** 45 * Controller to manage the various states of the {@link android.app.ActionBar}. 46 */ 47 public class ActionBarController implements ViewMode.ModeChangeListener { 48 49 private final Context mContext; 50 51 protected ActionBar mActionBar; 52 protected ControllableActivity mActivity; 53 protected ActivityController mController; 54 /** 55 * The current mode of the ActionBar and Activity 56 */ 57 private ViewMode mViewModeController; 58 59 /** 60 * The account currently being shown 61 */ 62 private Account mAccount; 63 /** 64 * The folder currently being shown 65 */ 66 private Folder mFolder; 67 68 private MenuItem mEmptyTrashItem; 69 private MenuItem mEmptySpamItem; 70 71 /** True if the current device is a tablet, false otherwise. */ 72 protected final boolean mIsOnTablet; 73 private Conversation mCurrentConversation; 74 75 public static final String LOG_TAG = LogTag.getLogTag(); 76 77 private FolderObserver mFolderObserver; 78 79 /** Updates the resolver and tells it the most recent account. */ 80 private final class UpdateProvider extends AsyncTask<Bundle, Void, Void> { 81 final Uri mAccount; 82 final ContentResolver mResolver; UpdateProvider(Uri account, ContentResolver resolver)83 public UpdateProvider(Uri account, ContentResolver resolver) { 84 mAccount = account; 85 mResolver = resolver; 86 } 87 88 @Override doInBackground(Bundle... params)89 protected Void doInBackground(Bundle... params) { 90 mResolver.call(mAccount, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT, 91 mAccount.toString(), params[0]); 92 return null; 93 } 94 } 95 96 private final AccountObserver mAccountObserver = new AccountObserver() { 97 @Override 98 public void onChanged(Account newAccount) { 99 updateAccount(newAccount); 100 } 101 }; 102 ActionBarController(Context context)103 public ActionBarController(Context context) { 104 mContext = context; 105 mIsOnTablet = Utils.useTabletUI(context.getResources()); 106 } 107 onCreateOptionsMenu(Menu menu)108 public boolean onCreateOptionsMenu(Menu menu) { 109 mEmptyTrashItem = menu.findItem(R.id.empty_trash); 110 mEmptySpamItem = menu.findItem(R.id.empty_spam); 111 112 // the menu should be displayed if the mode is known 113 return getMode() != ViewMode.UNKNOWN; 114 } 115 getOptionsMenuId()116 public int getOptionsMenuId() { 117 switch (getMode()) { 118 case ViewMode.UNKNOWN: 119 return R.menu.conversation_list_menu; 120 case ViewMode.CONVERSATION: 121 return R.menu.conversation_actions; 122 case ViewMode.CONVERSATION_LIST: 123 return R.menu.conversation_list_menu; 124 case ViewMode.SEARCH_RESULTS_LIST: 125 return R.menu.conversation_list_search_results_actions; 126 case ViewMode.SEARCH_RESULTS_CONVERSATION: 127 return R.menu.conversation_actions; 128 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION: 129 return R.menu.wait_mode_actions; 130 } 131 LogUtils.wtf(LOG_TAG, "Menu requested for unknown view mode"); 132 return R.menu.conversation_list_menu; 133 } 134 initialize(ControllableActivity activity, ActivityController callback, ActionBar actionBar)135 public void initialize(ControllableActivity activity, ActivityController callback, 136 ActionBar actionBar) { 137 mActionBar = actionBar; 138 mController = callback; 139 mActivity = activity; 140 141 mFolderObserver = new FolderObserver() { 142 @Override 143 public void onChanged(Folder newFolder) { 144 onFolderUpdated(newFolder); 145 } 146 }; 147 // Return values are purposely discarded. Initialization happens quite early, and we don't 148 // have a valid folder, or a valid list of accounts. 149 mFolderObserver.initialize(mController); 150 updateAccount(mAccountObserver.initialize(activity.getAccountController())); 151 } 152 updateAccount(Account account)153 private void updateAccount(Account account) { 154 final boolean accountChanged = mAccount == null || !mAccount.uri.equals(account.uri); 155 mAccount = account; 156 if (mAccount != null && accountChanged) { 157 final ContentResolver resolver = mActivity.getActivityContext().getContentResolver(); 158 final Bundle bundle = new Bundle(1); 159 bundle.putParcelable(UIProvider.SetCurrentAccountColumns.ACCOUNT, account); 160 final UpdateProvider updater = new UpdateProvider(mAccount.uri, resolver); 161 updater.execute(bundle); 162 setFolderAndAccount(); 163 } 164 } 165 166 /** 167 * Called by the owner of the ActionBar to change the current folder. 168 */ setFolder(Folder folder)169 public void setFolder(Folder folder) { 170 mFolder = folder; 171 setFolderAndAccount(); 172 } 173 onDestroy()174 public void onDestroy() { 175 if (mFolderObserver != null) { 176 mFolderObserver.unregisterAndDestroy(); 177 mFolderObserver = null; 178 } 179 mAccountObserver.unregisterAndDestroy(); 180 } 181 182 @Override onViewModeChanged(int newMode)183 public void onViewModeChanged(int newMode) { 184 final boolean mIsTabletLandscape = 185 mContext.getResources().getBoolean(R.bool.is_tablet_landscape); 186 187 mActivity.supportInvalidateOptionsMenu(); 188 // Check if we are either on a phone, or in Conversation mode on tablet. For these, the 189 // recent folders is enabled. 190 switch (getMode()) { 191 case ViewMode.UNKNOWN: 192 break; 193 case ViewMode.CONVERSATION_LIST: 194 showNavList(); 195 break; 196 case ViewMode.SEARCH_RESULTS_CONVERSATION: 197 mActionBar.setDisplayHomeAsUpEnabled(true); 198 setEmptyMode(); 199 break; 200 case ViewMode.CONVERSATION: 201 // If on tablet landscape, show current folder instead of emptying the action bar 202 if (mIsTabletLandscape) { 203 mActionBar.setDisplayHomeAsUpEnabled(true); 204 showNavList(); 205 break; 206 } 207 // Otherwise, fall through to default behavior, shared with Ads ViewMode. 208 case ViewMode.AD: 209 mActionBar.setDisplayHomeAsUpEnabled(true); 210 setEmptyMode(); 211 break; 212 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION: 213 // We want the user to be able to switch accounts while waiting for an account 214 // to sync. 215 showNavList(); 216 break; 217 } 218 } 219 getMode()220 protected int getMode() { 221 if (mViewModeController != null) { 222 return mViewModeController.getMode(); 223 } else { 224 return ViewMode.UNKNOWN; 225 } 226 } 227 228 /** 229 * Helper function to ensure that the menu items that are prone to variable changes and race 230 * conditions are properly set to the correct visibility 231 */ validateVolatileMenuOptionVisibility()232 public void validateVolatileMenuOptionVisibility() { 233 Utils.setMenuItemPresent(mEmptyTrashItem, mAccount != null && mFolder != null 234 && mAccount.supportsCapability(AccountCapabilities.EMPTY_TRASH) 235 && mFolder.isTrash() && mFolder.totalCount > 0 236 && (mController.getConversationListCursor() == null 237 || mController.getConversationListCursor().getCount() > 0)); 238 Utils.setMenuItemPresent(mEmptySpamItem, mAccount != null && mFolder != null 239 && mAccount.supportsCapability(AccountCapabilities.EMPTY_SPAM) 240 && mFolder.isType(FolderType.SPAM) && mFolder.totalCount > 0 241 && (mController.getConversationListCursor() == null 242 || mController.getConversationListCursor().getCount() > 0)); 243 } 244 onPrepareOptionsMenu(Menu menu)245 public void onPrepareOptionsMenu(Menu menu) { 246 menu.setQwertyMode(true); 247 // We start out with every option enabled. Based on the current view, we disable actions 248 // that are possible. 249 LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu()."); 250 251 if (mController.shouldHideMenuItems()) { 252 // Shortcut: hide all menu items if the drawer is shown 253 final int size = menu.size(); 254 255 for (int i = 0; i < size; i++) { 256 final MenuItem item = menu.getItem(i); 257 item.setVisible(false); 258 } 259 return; 260 } 261 validateVolatileMenuOptionVisibility(); 262 263 switch (getMode()) { 264 case ViewMode.CONVERSATION: 265 case ViewMode.SEARCH_RESULTS_CONVERSATION: 266 // We update the ActionBar options when we are entering conversation view because 267 // waiting for the AbstractConversationViewFragment to do it causes duplicate icons 268 // to show up during the time between the conversation is selected and the fragment 269 // is added. 270 setConversationModeOptions(menu); 271 break; 272 case ViewMode.CONVERSATION_LIST: 273 case ViewMode.SEARCH_RESULTS_LIST: 274 // The search menu item should only be visible for non-tablet devices 275 Utils.setMenuItemPresent(menu, R.id.search, 276 mAccount.supportsSearch() && !mIsOnTablet); 277 } 278 279 return; 280 } 281 282 /** 283 * Put the ActionBar in List navigation mode. 284 */ showNavList()285 private void showNavList() { 286 setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE); 287 setFolderAndAccount(); 288 } 289 setTitle(String title)290 private void setTitle(String title) { 291 if (!TextUtils.equals(title, mActionBar.getTitle())) { 292 mActionBar.setTitle(title); 293 } 294 } 295 296 /** 297 * Set the actionbar mode to empty: no title, no subtitle, no custom view. 298 */ setEmptyMode()299 protected void setEmptyMode() { 300 // Disable title/subtitle and the custom view by setting the bitmask to all off. 301 setTitleModeFlags(0); 302 } 303 304 /** 305 * Removes the back button from being shown 306 */ removeBackButton()307 public void removeBackButton() { 308 if (mActionBar == null) { 309 return; 310 } 311 // Remove the back button but continue showing an icon. 312 final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME; 313 mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME, mask); 314 mActionBar.setHomeButtonEnabled(false); 315 } 316 setBackButton()317 public void setBackButton() { 318 if (mActionBar == null) { 319 return; 320 } 321 // Show home as up, and show an icon. 322 final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME; 323 mActionBar.setDisplayOptions(mask, mask); 324 mActionBar.setHomeButtonEnabled(true); 325 } 326 327 /** 328 * Uses the current state to update the current folder {@link #mFolder} and the current 329 * account {@link #mAccount} shown in the actionbar. Also updates the actionbar subtitle to 330 * momentarily display the unread count if it has changed. 331 */ setFolderAndAccount()332 private void setFolderAndAccount() { 333 // Very little can be done if the actionbar or activity is null. 334 if (mActionBar == null || mActivity == null) { 335 return; 336 } 337 if (ViewMode.isWaitingForSync(getMode())) { 338 // Account is not synced: clear title and update the subtitle. 339 setTitle(""); 340 return; 341 } 342 // Check if we should be changing the actionbar at all, and back off if not. 343 final boolean isShowingFolder = mIsOnTablet || ViewMode.isListMode(getMode()); 344 if (!isShowingFolder) { 345 // It isn't necessary to set the title in this case, as the title view will 346 // be hidden 347 return; 348 } 349 if (mFolder == null) { 350 // Clear the action bar title. We don't want the app name to be shown while 351 // waiting for the folder query to finish 352 setTitle(""); 353 return; 354 } 355 setTitle(mFolder.name); 356 } 357 358 359 /** 360 * Notify that the folder has changed. 361 */ onFolderUpdated(Folder folder)362 public void onFolderUpdated(Folder folder) { 363 if (folder == null) { 364 return; 365 } 366 /** True if we are changing folders. */ 367 mFolder = folder; 368 setFolderAndAccount(); 369 // make sure that we re-validate the optional menu items 370 validateVolatileMenuOptionVisibility(); 371 } 372 373 /** 374 * Sets the actionbar mode: Pass it an integer which contains each of these values, perhaps 375 * OR'd together: {@link ActionBar#DISPLAY_SHOW_CUSTOM} and 376 * {@link ActionBar#DISPLAY_SHOW_TITLE}. To disable all, pass a zero. 377 * @param enabledFlags 378 */ setTitleModeFlags(int enabledFlags)379 private void setTitleModeFlags(int enabledFlags) { 380 final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM; 381 mActionBar.setDisplayOptions(enabledFlags, mask); 382 } 383 setCurrentConversation(Conversation conversation)384 public void setCurrentConversation(Conversation conversation) { 385 mCurrentConversation = conversation; 386 } 387 388 //We need to do this here instead of in the fragment setConversationModeOptions(Menu menu)389 public void setConversationModeOptions(Menu menu) { 390 if (mCurrentConversation == null) { 391 return; 392 } 393 final boolean showMarkImportant = !mCurrentConversation.isImportant(); 394 Utils.setMenuItemPresent(menu, R.id.mark_important, showMarkImportant 395 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT)); 396 Utils.setMenuItemPresent(menu, R.id.mark_not_important, !showMarkImportant 397 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT)); 398 final boolean isOutbox = mFolder.isType(FolderType.OUTBOX); 399 final boolean showDiscardOutbox = mFolder != null && isOutbox; 400 Utils.setMenuItemPresent(menu, R.id.discard_outbox, showDiscardOutbox); 401 final boolean showDelete = !isOutbox && mFolder != null && 402 mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE); 403 Utils.setMenuItemPresent(menu, R.id.delete, showDelete); 404 // We only want to show the discard drafts menu item if we are not showing the delete menu 405 // item, and the current folder is a draft folder and the account supports discarding 406 // drafts for a conversation 407 final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() && 408 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS); 409 Utils.setMenuItemPresent(menu, R.id.discard_drafts, showDiscardDrafts); 410 final boolean archiveVisible = mAccount.supportsCapability(AccountCapabilities.ARCHIVE) 411 && mFolder != null && mFolder.supportsCapability(FolderCapabilities.ARCHIVE) 412 && !mFolder.isTrash(); 413 Utils.setMenuItemPresent(menu, R.id.archive, archiveVisible); 414 Utils.setMenuItemPresent(menu, R.id.remove_folder, !archiveVisible && mFolder != null 415 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 416 && !mFolder.isProviderFolder() 417 && mAccount.supportsCapability(AccountCapabilities.ARCHIVE)); 418 Utils.setMenuItemPresent(menu, R.id.move_to, mFolder != null 419 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION)); 420 Utils.setMenuItemPresent(menu, R.id.move_to_inbox, mFolder != null 421 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX)); 422 Utils.setMenuItemPresent(menu, R.id.change_folders, mAccount.supportsCapability( 423 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)); 424 425 final MenuItem removeFolder = menu.findItem(R.id.remove_folder); 426 if (mFolder != null && removeFolder != null) { 427 removeFolder.setTitle(mActivity.getApplicationContext().getString( 428 R.string.remove_folder, mFolder.name)); 429 } 430 Utils.setMenuItemPresent(menu, R.id.report_spam, 431 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null 432 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM) 433 && !mCurrentConversation.spam); 434 Utils.setMenuItemPresent(menu, R.id.mark_not_spam, 435 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null 436 && mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM) 437 && mCurrentConversation.spam); 438 Utils.setMenuItemPresent(menu, R.id.report_phishing, 439 mAccount.supportsCapability(AccountCapabilities.REPORT_PHISHING) && mFolder != null 440 && mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING) 441 && !mCurrentConversation.phishing); 442 Utils.setMenuItemPresent(menu, R.id.mute, 443 mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null 444 && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE) 445 && !mCurrentConversation.muted); 446 } 447 setViewModeController(ViewMode viewModeController)448 public void setViewModeController(ViewMode viewModeController) { 449 mViewModeController = viewModeController; 450 mViewModeController.addListener(this); 451 } 452 getContext()453 public Context getContext() { 454 return mContext; 455 } 456 } 457