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