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