1 /*
2  * Copyright (C) 2017 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 package com.android.documentsui.dirlist;
17 
18 import android.view.KeyEvent;
19 
20 import androidx.recyclerview.selection.SelectionTracker;
21 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
22 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
23 
24 import com.android.documentsui.base.Events;
25 
26 import javax.annotation.Nullable;
27 
28 // TODO(b/69058726): Migrate to RecyclerView-Selection
29 /**
30  * Class that handles keyboard events on RecyclerView items. The input handler
31  * must be attached directly to a RecyclerView item since, unlike DOM, events
32  * don't appear bubble up.
33  */
34 public final class KeyInputHandler extends KeyboardEventListener<DocumentItemDetails> {
35 
36     private final SelectionTracker<String> mSelectionHelper;
37     private final SelectionPredicate<String> mSelectionPredicate;
38     private final Callbacks<DocumentItemDetails> mCallbacks;
39 
KeyInputHandler( SelectionTracker<String> selectionHelper, SelectionPredicate<String> selectionPredicate, Callbacks<DocumentItemDetails> callbacks)40     public KeyInputHandler(
41             SelectionTracker<String> selectionHelper,
42             SelectionPredicate<String> selectionPredicate,
43             Callbacks<DocumentItemDetails> callbacks) {
44 
45         mSelectionHelper = selectionHelper;
46         mSelectionPredicate = selectionPredicate;
47         mCallbacks = callbacks;
48     }
49 
50     @Override
onKey(@ullable DocumentItemDetails details, int keyCode, KeyEvent event)51     public boolean onKey(@Nullable DocumentItemDetails details, int keyCode, KeyEvent event) {
52         // Only handle key-down events. This is simpler, consistent with most other UIs, and
53         // enables the handling of repeated key events from holding down a key.
54         if (event.getAction() != KeyEvent.ACTION_DOWN) {
55             return false;
56         }
57 
58         // Ignore tab key events.  Those should be handled by the top-level key handler.
59         if (keyCode == KeyEvent.KEYCODE_TAB) {
60             return false;
61         }
62 
63         // Ignore events sent to Addon Holders.
64         if (details != null) {
65             int itemType = details.getItemViewType();
66             if (itemType == DocumentsAdapter.ITEM_TYPE_HEADER_MESSAGE
67                     || itemType == DocumentsAdapter.ITEM_TYPE_INFLATED_MESSAGE
68                     || itemType == DocumentsAdapter.ITEM_TYPE_SECTION_BREAK) {
69                 return false;
70             }
71         }
72 
73         if (mCallbacks.onFocusItem(details, keyCode, event)) {
74             // Handle range selection adjustments. Extending the selection will adjust the
75             // bounds of the in-progress range selection. Each time an unshifted navigation
76             // event is received, the range selection is restarted.
77             if (shouldExtendSelection(details, event)) {
78                 if (!mSelectionHelper.isRangeActive()) {
79                     // Start a range selection if one isn't active
80                     mSelectionHelper.startRange(details.getPosition());
81                 }
82                 mSelectionHelper.extendRange(details.getPosition());
83             } else {
84                 mSelectionHelper.endRange();
85                 mSelectionHelper.clearSelection();
86             }
87             return true;
88         }
89 
90         // we don't yet have a mechanism to handle opening/previewing multiple documents at once
91         if (mSelectionHelper.getSelection().size() > 1) {
92             return false;
93         }
94 
95         return mCallbacks.onItemActivated(details, event);
96     }
97 
shouldExtendSelection(DocumentItemDetails item, KeyEvent event)98     private boolean shouldExtendSelection(DocumentItemDetails item, KeyEvent event) {
99         if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
100             return false;
101         }
102 
103         return mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true);
104     }
105 
106     public static abstract class Callbacks<T extends ItemDetails<?>> {
isInteractiveItem(T item, KeyEvent e)107         public abstract boolean isInteractiveItem(T item, KeyEvent e);
onItemActivated(T item, KeyEvent e)108         public abstract boolean onItemActivated(T item, KeyEvent e);
onFocusItem(T details, int keyCode, KeyEvent event)109         public abstract boolean onFocusItem(T details, int keyCode, KeyEvent event);
110     }
111 }
112