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