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.base.SharedMinimal.DEBUG; 20 21 import android.app.Activity; 22 import android.util.Log; 23 import android.view.ActionMode; 24 import android.view.Menu; 25 import android.view.MenuItem; 26 import android.view.View; 27 28 import androidx.annotation.IdRes; 29 import androidx.annotation.Nullable; 30 import androidx.recyclerview.selection.MutableSelection; 31 import androidx.recyclerview.selection.SelectionTracker; 32 import androidx.recyclerview.selection.SelectionTracker.SelectionObserver; 33 34 import com.android.documentsui.MenuManager.SelectionDetails; 35 import com.android.documentsui.base.EventHandler; 36 import com.android.documentsui.base.Menus; 37 import com.android.documentsui.ui.MessageBuilder; 38 39 /** 40 * A controller that listens to selection changes and manages life cycles of action modes. 41 */ 42 public class ActionModeController extends SelectionObserver<String> 43 implements ActionMode.Callback, ActionModeAddons { 44 45 private static final String TAG = "ActionModeController"; 46 47 private final Activity mActivity; 48 private final SelectionTracker<String> mSelectionMgr; 49 private final NavigationViewManager mNavigator; 50 private final MenuManager mMenuManager; 51 private final MessageBuilder mMessages; 52 53 private final ContentScope mScope = new ContentScope(); 54 private final MutableSelection<String> mSelected = new MutableSelection<>(); 55 56 private @Nullable ActionMode mActionMode; 57 private @Nullable Menu mMenu; 58 ActionModeController( Activity activity, SelectionTracker<String> selectionMgr, NavigationViewManager navigator, MenuManager menuManager, MessageBuilder messages)59 public ActionModeController( 60 Activity activity, 61 SelectionTracker<String> selectionMgr, 62 NavigationViewManager navigator, 63 MenuManager menuManager, 64 MessageBuilder messages) { 65 66 mActivity = activity; 67 mSelectionMgr = selectionMgr; 68 mNavigator = navigator; 69 mMenuManager = menuManager; 70 mMessages = messages; 71 } 72 73 @Override onSelectionChanged()74 public void onSelectionChanged() { 75 mSelectionMgr.copySelection(mSelected); 76 if (mSelected.size() > 0) { 77 if (mActionMode == null) { 78 if (DEBUG) { 79 Log.d(TAG, "Starting action mode."); 80 } 81 mActionMode = mActivity.startActionMode(this); 82 final View closeButton = 83 mActivity.findViewById(androidx.appcompat.R.id.action_mode_close_button); 84 if (closeButton != null) { 85 closeButton.setContentDescription(mActivity.getString(android.R.string.cancel)); 86 } 87 } 88 updateActionMenu(); 89 } else { 90 if (mActionMode != null) { 91 if (DEBUG) { 92 Log.d(TAG, "Finishing action mode."); 93 } 94 mActionMode.finish(); 95 } 96 } 97 98 if (mActionMode != null) { 99 assert(!mSelected.isEmpty()); 100 final String title = mMessages.getQuantityString( 101 R.plurals.elements_selected, mSelected.size()); 102 mActionMode.setTitle(title); 103 mActivity.getWindow().setTitle(title); 104 } 105 } 106 107 @Override onSelectionRestored()108 public void onSelectionRestored() { 109 onSelectionChanged(); 110 } 111 112 // Called when the user exits the action mode 113 @Override onDestroyActionMode(ActionMode mode)114 public void onDestroyActionMode(ActionMode mode) { 115 if (mActionMode == null) { 116 if (DEBUG) { 117 Log.w(TAG, "Received call to destroy action mode on alien mode object."); 118 } 119 } 120 121 assert(mActionMode.equals(mode)); 122 123 if (DEBUG) { 124 Log.d(TAG, "Handling action mode destroyed."); 125 } 126 mActionMode = null; 127 mMenu = null; 128 129 if (mSelected.size() > 0) { 130 mSelectionMgr.clearSelection(); 131 } 132 133 // Reset window title back to activity title, i.e. Root name 134 mActivity.getWindow().setTitle(mActivity.getTitle()); 135 136 // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode. 137 mScope.accessibilityImportanceSetter.setAccessibilityImportance( 138 View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar); 139 140 mNavigator.setActionModeActivated(false); 141 } 142 143 @Override onCreateActionMode(ActionMode mode, Menu menu)144 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 145 int size = mSelectionMgr.getSelection().size(); 146 mode.getMenuInflater().inflate(R.menu.action_mode_menu, menu); 147 mode.setTitle(mActivity.getResources().getQuantityString(R.plurals.selected_count, size)); 148 149 if (size > 0) { 150 mNavigator.setActionModeActivated(true); 151 152 // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to 153 // these controls when using linear navigation. 154 mScope.accessibilityImportanceSetter.setAccessibilityImportance( 155 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, 156 R.id.toolbar, 157 R.id.roots_toolbar); 158 return true; 159 } 160 161 return false; 162 } 163 164 @Override onPrepareActionMode(ActionMode mode, Menu menu)165 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 166 mMenu = menu; 167 updateActionMenu(); 168 return true; 169 } 170 updateActionMenu()171 private void updateActionMenu() { 172 assert(mMenu != null); 173 mMenuManager.updateActionMenu(mMenu, mScope.selectionDetails); 174 Menus.disableHiddenItems(mMenu); 175 } 176 177 @Override onActionItemClicked(ActionMode mode, MenuItem item)178 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 179 return mScope.menuItemClicker.accept(item); 180 } 181 setImportantForAccessibility( Activity activity, int accessibilityImportance, @IdRes int[] viewIds)182 private static void setImportantForAccessibility( 183 Activity activity, int accessibilityImportance, @IdRes int[] viewIds) { 184 for (final int id : viewIds) { 185 final View v = activity.findViewById(id); 186 if (v != null) { 187 v.setImportantForAccessibility(accessibilityImportance); 188 } 189 } 190 } 191 192 @FunctionalInterface 193 private interface AccessibilityImportanceSetter { setAccessibilityImportance(int accessibilityImportance, @IdRes int... viewIds)194 void setAccessibilityImportance(int accessibilityImportance, @IdRes int... viewIds); 195 } 196 197 @Override finishActionMode()198 public void finishActionMode() { 199 if (mActionMode != null) { 200 mActionMode.finish(); 201 mActionMode = null; 202 } else { 203 Log.w(TAG, "Tried to finish a null action mode."); 204 } 205 } 206 reset( SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker)207 public ActionModeController reset( 208 SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker) { 209 assert(mActionMode == null); 210 assert(mMenu == null); 211 212 mScope.menuItemClicker = menuItemClicker; 213 mScope.selectionDetails = selectionDetails; 214 mScope.accessibilityImportanceSetter = 215 (int accessibilityImportance, @IdRes int[] viewIds) -> { 216 setImportantForAccessibility( 217 mActivity, accessibilityImportance, viewIds); 218 }; 219 220 return this; 221 } 222 223 private static final class ContentScope { 224 private EventHandler<MenuItem> menuItemClicker; 225 private SelectionDetails selectionDetails; 226 private AccessibilityImportanceSetter accessibilityImportanceSetter; 227 } 228 } 229