1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.documentsui; 18 19 import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; 20 import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled; 21 22 import android.view.KeyboardShortcutGroup; 23 import android.view.Menu; 24 import android.view.MenuInflater; 25 import android.view.MenuItem; 26 import android.view.View; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.VisibleForTesting; 30 import androidx.fragment.app.Fragment; 31 32 import com.android.documentsui.archives.ArchivesProvider; 33 import com.android.documentsui.base.DocumentInfo; 34 import com.android.documentsui.base.Menus; 35 import com.android.documentsui.base.RootInfo; 36 import com.android.documentsui.base.State; 37 import com.android.documentsui.dirlist.DirectoryFragment; 38 import com.android.documentsui.queries.SearchViewManager; 39 import com.android.documentsui.sidebar.RootsFragment; 40 41 import java.util.List; 42 import java.util.function.IntFunction; 43 import java.util.function.IntSupplier; 44 45 public abstract class MenuManager { 46 private final static String TAG = "MenuManager"; 47 48 protected final SearchViewManager mSearchManager; 49 protected final State mState; 50 protected final DirectoryDetails mDirDetails; 51 protected final IntSupplier mFilesCountSupplier; 52 53 protected Menu mOptionMenu; 54 MenuManager( SearchViewManager searchManager, State displayState, DirectoryDetails dirDetails, IntSupplier filesCountSupplier)55 public MenuManager( 56 SearchViewManager searchManager, 57 State displayState, 58 DirectoryDetails dirDetails, 59 IntSupplier filesCountSupplier) { 60 mSearchManager = searchManager; 61 mState = displayState; 62 mDirDetails = dirDetails; 63 mFilesCountSupplier = filesCountSupplier; 64 } 65 66 /** @see ActionModeController */ updateActionMenu(Menu menu, SelectionDetails selection)67 public void updateActionMenu(Menu menu, SelectionDetails selection) { 68 updateOpenWith(menu.findItem(R.id.action_menu_open_with), selection); 69 updateDelete(menu.findItem(R.id.action_menu_delete), selection); 70 updateShare(menu.findItem(R.id.action_menu_share), selection); 71 updateRename(menu.findItem(R.id.action_menu_rename), selection); 72 updateSelect(menu.findItem(R.id.action_menu_select), selection); 73 updateSelectAll(menu.findItem(R.id.action_menu_select_all), selection); 74 updateDeselectAll(menu.findItem(R.id.action_menu_deselect_all), selection); 75 updateMoveTo(menu.findItem(R.id.action_menu_move_to), selection); 76 updateCopyTo(menu.findItem(R.id.action_menu_copy_to), selection); 77 updateCompress(menu.findItem(R.id.action_menu_compress), selection); 78 updateExtractTo(menu.findItem(R.id.action_menu_extract_to), selection); 79 updateInspect(menu.findItem(R.id.action_menu_inspect), selection); 80 updateViewInOwner(menu.findItem(R.id.action_menu_view_in_owner), selection); 81 updateSort(menu.findItem(R.id.action_menu_sort)); 82 83 Menus.disableHiddenItems(menu); 84 } 85 86 /** @see BaseActivity#onPrepareOptionsMenu */ updateOptionMenu(Menu menu)87 public void updateOptionMenu(Menu menu) { 88 mOptionMenu = menu; 89 updateOptionMenu(); 90 } 91 updateOptionMenu()92 public void updateOptionMenu() { 93 if (mOptionMenu == null) { 94 return; 95 } 96 updateCreateDir(mOptionMenu.findItem(R.id.option_menu_create_dir)); 97 if (isZipNgFlagEnabled()) { 98 updateExtractAll(mOptionMenu.findItem(R.id.option_menu_extract_all)); 99 } 100 updateSettings(mOptionMenu.findItem(R.id.option_menu_settings)); 101 updateSelectAll(mOptionMenu.findItem(R.id.option_menu_select_all)); 102 updateNewWindow(mOptionMenu.findItem(R.id.option_menu_new_window)); 103 updateDebug(mOptionMenu.findItem(R.id.option_menu_debug)); 104 updateInspect(mOptionMenu.findItem(R.id.option_menu_inspect)); 105 updateSort(mOptionMenu.findItem(R.id.option_menu_sort)); 106 updateLauncher(mOptionMenu.findItem(R.id.option_menu_launcher)); 107 updateShowHiddenFiles(mOptionMenu.findItem(R.id.option_menu_show_hidden_files)); 108 109 if (isUseMaterial3FlagEnabled()) { 110 updateModePicker(mOptionMenu.findItem(R.id.sub_menu_grid), 111 mOptionMenu.findItem(R.id.sub_menu_list)); 112 } 113 114 Menus.disableHiddenItems(mOptionMenu); 115 mSearchManager.updateMenu(); 116 } 117 updateSubMenu(Menu menu)118 public void updateSubMenu(Menu menu) { 119 // Remove the subMenu when material3 is launched b/379776735. 120 if (isUseMaterial3FlagEnabled()) { 121 menu = mOptionMenu; 122 if (menu == null) { 123 return; 124 } 125 } 126 updateModePicker(menu.findItem(R.id.sub_menu_grid), menu.findItem(R.id.sub_menu_list)); 127 128 } 129 updateModel(Model model)130 public void updateModel(Model model) {} 131 132 /** 133 * Called when we needs {@link MenuManager} to ask Android to show context menu for us. 134 * {@link MenuManager} can choose to defeat this request. 135 * 136 * {@link #inflateContextMenuForDocs} and {@link #inflateContextMenuForContainer} are called 137 * afterwards when Android asks us to provide the content of context menus, so they're not 138 * correct locations to suppress context menus. 139 */ showContextMenu(Fragment f, View v, float x, float y)140 public void showContextMenu(Fragment f, View v, float x, float y) { 141 // Pickers don't have any context menu at this moment. 142 } 143 144 /** 145 * Called when container context menu needs to be inflated. 146 * 147 * @param menu context menu from activity or fragment 148 * @param inflater the MenuInflater 149 * @param selectionDetails selection of files 150 */ inflateContextMenuForContainer( Menu menu, MenuInflater inflater, SelectionDetails selectionDetails)151 public void inflateContextMenuForContainer( 152 Menu menu, MenuInflater inflater, SelectionDetails selectionDetails) { 153 throw new UnsupportedOperationException("Pickers don't allow context menu."); 154 } 155 inflateContextMenuForDocs( Menu menu, MenuInflater inflater, SelectionDetails selectionDetails)156 public void inflateContextMenuForDocs( 157 Menu menu, MenuInflater inflater, SelectionDetails selectionDetails) { 158 throw new UnsupportedOperationException("Pickers don't allow context menu."); 159 } 160 161 /** 162 * Called when user tries to generate a context menu anchored to a file when the selection 163 * doesn't contain any folder. 164 * 165 * @see DirectoryFragment#onCreateContextMenu 166 * 167 * @param selectionDetails 168 * containsFiles may return false because this may be called when user right clicks on an 169 * unselectable item in pickers 170 */ 171 @VisibleForTesting updateContextMenuForFiles(Menu menu, SelectionDetails selectionDetails)172 public void updateContextMenuForFiles(Menu menu, SelectionDetails selectionDetails) { 173 assert selectionDetails != null; 174 175 MenuItem share = menu.findItem(R.id.dir_menu_share); 176 MenuItem open = menu.findItem(R.id.dir_menu_open); 177 MenuItem openWith = menu.findItem(R.id.dir_menu_open_with); 178 MenuItem rename = menu.findItem(R.id.dir_menu_rename); 179 MenuItem viewInOwner = menu.findItem(R.id.dir_menu_view_in_owner); 180 181 updateShare(share, selectionDetails); 182 updateOpenInContextMenu(open, selectionDetails); 183 updateOpenWith(openWith, selectionDetails); 184 updateRename(rename, selectionDetails); 185 updateViewInOwner(viewInOwner, selectionDetails); 186 187 if (isZipNgFlagEnabled()) { 188 updateExtractHere(menu.findItem(R.id.dir_menu_extract_here), selectionDetails); 189 updateBrowse(menu.findItem(R.id.dir_menu_browse), selectionDetails); 190 } 191 192 updateContextMenu(menu, selectionDetails); 193 } 194 195 /** 196 * Called when user tries to generate a context menu anchored to a folder when the selection 197 * doesn't contain any file. 198 * 199 * @see DirectoryFragment#onCreateContextMenu 200 * 201 * @param selectionDetails 202 * containDirectories may return false because this may be called when user right clicks on 203 * an unselectable item in pickers 204 */ 205 @VisibleForTesting updateContextMenuForDirs(Menu menu, SelectionDetails selectionDetails)206 public void updateContextMenuForDirs(Menu menu, SelectionDetails selectionDetails) { 207 assert selectionDetails != null; 208 209 MenuItem openInNewWindow = menu.findItem(R.id.dir_menu_open_in_new_window); 210 MenuItem rename = menu.findItem(R.id.dir_menu_rename); 211 MenuItem pasteInto = menu.findItem(R.id.dir_menu_paste_into_folder); 212 213 updateOpenInNewWindow(openInNewWindow, selectionDetails); 214 updateRename(rename, selectionDetails); 215 updatePasteInto(pasteInto, selectionDetails); 216 217 updateContextMenu(menu, selectionDetails); 218 } 219 220 /** 221 * @see DirectoryFragment#onCreateContextMenu 222 * 223 * Update shared context menu items of both files and folders context menus. 224 */ 225 @VisibleForTesting updateContextMenu(Menu menu, SelectionDetails selectionDetails)226 public void updateContextMenu(Menu menu, SelectionDetails selectionDetails) { 227 assert selectionDetails != null; 228 229 MenuItem cut = menu.findItem(R.id.dir_menu_cut_to_clipboard); 230 MenuItem copy = menu.findItem(R.id.dir_menu_copy_to_clipboard); 231 MenuItem delete = menu.findItem(R.id.dir_menu_delete); 232 MenuItem inspect = menu.findItem(R.id.dir_menu_inspect); 233 234 final boolean canCopy = 235 selectionDetails.size() > 0 && !selectionDetails.containsPartialFiles(); 236 final boolean canDelete = selectionDetails.canDelete(); 237 Menus.setEnabledAndVisible(cut, canCopy && canDelete); 238 Menus.setEnabledAndVisible(copy, canCopy); 239 Menus.setEnabledAndVisible(delete, canDelete); 240 241 Menus.setEnabledAndVisible(inspect, selectionDetails.size() == 1); 242 243 updateCompress(menu.findItem(R.id.dir_menu_compress), selectionDetails); 244 } 245 246 /** 247 * @see DirectoryFragment#onCreateContextMenu 248 * 249 * Called when user tries to generate a context menu anchored to an empty pane. 250 */ 251 @VisibleForTesting updateContextMenuForContainer(Menu menu, SelectionDetails selectionDetails)252 public void updateContextMenuForContainer(Menu menu, SelectionDetails selectionDetails) { 253 MenuItem paste = menu.findItem(R.id.dir_menu_paste_from_clipboard); 254 MenuItem selectAll = menu.findItem(R.id.dir_menu_select_all); 255 MenuItem deselectAll = menu.findItem(R.id.dir_menu_deselect_all); 256 MenuItem createDir = menu.findItem(R.id.dir_menu_create_dir); 257 MenuItem inspect = menu.findItem(R.id.dir_menu_inspect); 258 259 Menus.setEnabledAndVisible(paste, 260 mDirDetails.hasItemsToPaste() && mDirDetails.canCreateDoc()); 261 updateSelectAll(selectAll, selectionDetails); 262 updateDeselectAll(deselectAll, selectionDetails); 263 updateCreateDir(createDir); 264 updateInspect(inspect); 265 } 266 267 /** 268 * @see RootsFragment#onCreateContextMenu 269 */ updateRootContextMenu(Menu menu, RootInfo root, DocumentInfo docInfo)270 public void updateRootContextMenu(Menu menu, RootInfo root, DocumentInfo docInfo) { 271 MenuItem eject = menu.findItem(R.id.root_menu_eject_root); 272 MenuItem pasteInto = menu.findItem(R.id.root_menu_paste_into_folder); 273 MenuItem openInNewWindow = menu.findItem(R.id.root_menu_open_in_new_window); 274 MenuItem settings = menu.findItem(R.id.root_menu_settings); 275 276 updateEject(eject, root); 277 updatePasteInto(pasteInto, root, docInfo); 278 updateOpenInNewWindow(openInNewWindow, root); 279 updateSettings(settings, root); 280 } 281 updateKeyboardShortcutsMenu( List<KeyboardShortcutGroup> data, IntFunction<String> stringSupplier)282 public abstract void updateKeyboardShortcutsMenu( 283 List<KeyboardShortcutGroup> data, IntFunction<String> stringSupplier); 284 285 /** 286 * Called on option menu creation to instantiate the job progress item if applicable. 287 * 288 * @param menu The option menu created. 289 */ instantiateJobProgress(Menu menu)290 public void instantiateJobProgress(Menu menu) { 291 // This icon is not shown in the picker. 292 } 293 updateModePicker(MenuItem grid, MenuItem list)294 protected void updateModePicker(MenuItem grid, MenuItem list) { 295 // The order of enabling disabling menu item in wrong order removed accessibility focus. 296 if (mState.derivedMode != State.MODE_LIST) { 297 Menus.setEnabledAndVisible(list, mState.derivedMode != State.MODE_LIST); 298 Menus.setEnabledAndVisible(grid, mState.derivedMode != State.MODE_GRID); 299 } else { 300 Menus.setEnabledAndVisible(grid, mState.derivedMode != State.MODE_GRID); 301 Menus.setEnabledAndVisible(list, mState.derivedMode != State.MODE_LIST); 302 } 303 } 304 updateShowHiddenFiles(MenuItem showHidden)305 protected void updateShowHiddenFiles(MenuItem showHidden) { 306 Menus.setEnabledAndVisible(showHidden, true); 307 showHidden.setTitle(mState.showHiddenFiles 308 ? R.string.menu_hide_hidden_files 309 : R.string.menu_show_hidden_files); 310 } 311 updateSort(MenuItem sort)312 protected void updateSort(MenuItem sort) { 313 Menus.setEnabledAndVisible(sort, true); 314 } 315 updateDebug(MenuItem debug)316 protected void updateDebug(MenuItem debug) { 317 Menus.setEnabledAndVisible(debug, mState.debugMode); 318 } 319 updateSettings(MenuItem settings)320 protected void updateSettings(MenuItem settings) { 321 Menus.setEnabledAndVisible(settings, false); 322 } 323 updateSettings(MenuItem settings, RootInfo root)324 protected void updateSettings(MenuItem settings, RootInfo root) { 325 Menus.setEnabledAndVisible(settings, false); 326 } 327 updateEject(MenuItem eject, RootInfo root)328 protected void updateEject(MenuItem eject, RootInfo root) { 329 Menus.setEnabledAndVisible(eject, false); 330 } 331 updateNewWindow(MenuItem newWindow)332 protected void updateNewWindow(MenuItem newWindow) { 333 Menus.setEnabledAndVisible(newWindow, false); 334 } 335 updateSelect(MenuItem select, SelectionDetails selectionDetails)336 protected void updateSelect(MenuItem select, SelectionDetails selectionDetails) { 337 Menus.setEnabledAndVisible(select, false); 338 } 339 updateOpenWith(MenuItem openWith, SelectionDetails selectionDetails)340 protected void updateOpenWith(MenuItem openWith, SelectionDetails selectionDetails) { 341 Menus.setEnabledAndVisible(openWith, false); 342 } 343 updateOpenInNewWindow( MenuItem openInNewWindow, SelectionDetails selectionDetails)344 protected void updateOpenInNewWindow( 345 MenuItem openInNewWindow, SelectionDetails selectionDetails) { 346 Menus.setEnabledAndVisible(openInNewWindow, false); 347 } 348 updateOpenInNewWindow( MenuItem openInNewWindow, RootInfo root)349 protected void updateOpenInNewWindow( 350 MenuItem openInNewWindow, RootInfo root) { 351 Menus.setEnabledAndVisible(openInNewWindow, false); 352 } 353 updateShare(MenuItem share, SelectionDetails selectionDetails)354 protected void updateShare(MenuItem share, SelectionDetails selectionDetails) { 355 Menus.setEnabledAndVisible(share, false); 356 } 357 updateDelete(MenuItem delete, SelectionDetails selectionDetails)358 protected void updateDelete(MenuItem delete, SelectionDetails selectionDetails) { 359 Menus.setEnabledAndVisible(delete, false); 360 } 361 updateRename(MenuItem rename, SelectionDetails selectionDetails)362 protected void updateRename(MenuItem rename, SelectionDetails selectionDetails) { 363 Menus.setEnabledAndVisible(rename, false); 364 } 365 366 /** 367 * This method is called for standard activity option menu as opposed 368 * to when there is a selection. 369 */ updateInspect(MenuItem inspector)370 protected void updateInspect(MenuItem inspector) { 371 Menus.setEnabledAndVisible(inspector, false); 372 } 373 374 /** 375 * This method is called for action mode, when a selection exists. 376 */ updateInspect(MenuItem inspect, SelectionDetails selectionDetails)377 protected void updateInspect(MenuItem inspect, SelectionDetails selectionDetails) { 378 Menus.setEnabledAndVisible(inspect, false); 379 } 380 updateViewInOwner(MenuItem view, SelectionDetails selectionDetails)381 protected void updateViewInOwner(MenuItem view, SelectionDetails selectionDetails) { 382 Menus.setEnabledAndVisible(view, false); 383 } 384 updateMoveTo(MenuItem moveTo, SelectionDetails selectionDetails)385 protected void updateMoveTo(MenuItem moveTo, SelectionDetails selectionDetails) { 386 Menus.setEnabledAndVisible(moveTo, false); 387 } 388 updateCopyTo(MenuItem copyTo, SelectionDetails selectionDetails)389 protected void updateCopyTo(MenuItem copyTo, SelectionDetails selectionDetails) { 390 Menus.setEnabledAndVisible(copyTo, false); 391 } 392 updateCompress(MenuItem compress, SelectionDetails selectionDetails)393 protected void updateCompress(MenuItem compress, SelectionDetails selectionDetails) { 394 Menus.setEnabledAndVisible(compress, false); 395 } 396 updateExtractTo(MenuItem extractTo, SelectionDetails selectionDetails)397 protected void updateExtractTo(MenuItem extractTo, SelectionDetails selectionDetails) { 398 Menus.setEnabledAndVisible(extractTo, false); 399 } 400 updateExtractHere(@onNull MenuItem it, @NonNull SelectionDetails selection)401 protected void updateExtractHere(@NonNull MenuItem it, @NonNull SelectionDetails selection) { 402 Menus.setEnabledAndVisible(it, false); 403 } 404 updateBrowse(@onNull MenuItem it, @NonNull SelectionDetails selection)405 protected void updateBrowse(@NonNull MenuItem it, @NonNull SelectionDetails selection) { 406 Menus.setEnabledAndVisible(it, false); 407 } 408 updatePasteInto(MenuItem pasteInto, SelectionDetails selectionDetails)409 protected void updatePasteInto(MenuItem pasteInto, SelectionDetails selectionDetails) { 410 Menus.setEnabledAndVisible(pasteInto, false); 411 } 412 updatePasteInto(MenuItem pasteInto, RootInfo root, DocumentInfo docInfo)413 protected void updatePasteInto(MenuItem pasteInto, RootInfo root, DocumentInfo docInfo) { 414 Menus.setEnabledAndVisible(pasteInto, false); 415 } 416 updateOpenInContextMenu(MenuItem open, SelectionDetails selectionDetails)417 protected void updateOpenInContextMenu(MenuItem open, SelectionDetails selectionDetails) { 418 Menus.setEnabledAndVisible(open, false); 419 } 420 updateLauncher(MenuItem launcher)421 protected void updateLauncher(MenuItem launcher) { 422 Menus.setEnabledAndVisible(launcher, false); 423 } 424 updateExtractAll(MenuItem it)425 protected void updateExtractAll(MenuItem it) { 426 Menus.setEnabledAndVisible(it, false); 427 } 428 updateSelectAll(MenuItem selectAll)429 protected abstract void updateSelectAll(MenuItem selectAll); 430 updateSelectAll(MenuItem selectAll, SelectionDetails selectionDetails)431 protected abstract void updateSelectAll(MenuItem selectAll, SelectionDetails selectionDetails); 432 updateDeselectAll( MenuItem deselectAll, SelectionDetails selectionDetails)433 protected abstract void updateDeselectAll( 434 MenuItem deselectAll, SelectionDetails selectionDetails); 435 updateCreateDir(MenuItem createDir)436 protected abstract void updateCreateDir(MenuItem createDir); 437 438 /** 439 * Access to meta data about the selection. 440 */ 441 public interface SelectionDetails { 442 /** Gets the total number of items (files and directories) in the selection. */ size()443 int size(); 444 445 /** Returns whether the selection contains at least a directory. */ containsDirectories()446 boolean containsDirectories(); 447 448 /** Returns whether the selection contains at least a file. */ containsFiles()449 boolean containsFiles(); 450 451 /** 452 * Returns whether the selection contains at least a file that has not been fully downloaded 453 * yet. 454 */ containsPartialFiles()455 boolean containsPartialFiles(); 456 457 /** Returns whether the selection contains at least a file located in a mounted archive. */ containsFilesInArchive()458 boolean containsFilesInArchive(); 459 460 /** 461 * Returns whether the selection contains exactly one file which is also a supported archive 462 * type. 463 */ isArchive()464 boolean isArchive(); 465 466 // TODO: Update these to express characteristics instead of answering concrete questions, 467 // since the answer to those questions is (or can be) activity specific. canDelete()468 boolean canDelete(); 469 canRename()470 boolean canRename(); 471 canPasteInto()472 boolean canPasteInto(); 473 canExtract()474 boolean canExtract(); 475 canOpen()476 boolean canOpen(); 477 canViewInOwner()478 boolean canViewInOwner(); 479 } 480 481 public static class DirectoryDetails { 482 private final BaseActivity mActivity; 483 DirectoryDetails(BaseActivity activity)484 public DirectoryDetails(BaseActivity activity) { 485 mActivity = activity; 486 } 487 hasRootSettings()488 public boolean hasRootSettings() { 489 return mActivity.getCurrentRoot().hasSettings(); 490 } 491 hasItemsToPaste()492 public boolean hasItemsToPaste() { 493 return false; 494 } 495 canCreateDoc()496 public boolean canCreateDoc() { 497 return isInRecents() ? false : mActivity.getCurrentDirectory().isCreateSupported(); 498 } 499 isInRecents()500 public boolean isInRecents() { 501 return mActivity.isInRecents(); 502 } 503 504 /** Is the current directory showing the contents of an archive? */ isInArchive()505 public boolean isInArchive() { 506 final DocumentInfo dir = mActivity.getCurrentDirectory(); 507 return dir != null && ArchivesProvider.AUTHORITY.equals(dir.authority); 508 } 509 canCreateDirectory()510 public boolean canCreateDirectory() { 511 return mActivity.canCreateDirectory(); 512 } 513 canInspectDirectory()514 public boolean canInspectDirectory() { 515 return mActivity.canInspectDirectory() && !isInRecents(); 516 } 517 } 518 } 519