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