1 /*
2  * Copyright (C) 2020 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.car.rotaryplayground;
18 
19 import android.util.Log;
20 import android.view.KeyEvent;
21 import android.view.MotionEvent;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import androidx.annotation.Nullable;
26 import androidx.core.util.Preconditions;
27 
28 /**
29  * A {@link View.OnKeyListener} and {@link View.OnGenericMotionListener} that adds a
30  * "Direct Manipulation" mode to any {@link View} that uses it.
31  * <p>
32  * Direct Manipulation mode in the Rotary context is a mode in which the user can use the
33  * Rotary controls to manipulate and change the UI elements they are interacting with rather
34  * than navigate through the entire UI.
35  * <p>
36  * Treats {@link KeyEvent#KEYCODE_DPAD_CENTER} as the signal to enter Direct Manipulation
37  * mode, and {@link KeyEvent#KEYCODE_BACK} as the signal to exit, and keeps track of which
38  * mode the {@link View} using it is currently in.
39  * <p>
40  * When in Direct Manipulation mode, it delegates to {@code mDirectionalDelegate}
41  * for handling nudge behavior and {@code mMotionDelegate} for rotation behavior. Generally
42  * it is expected that in Direct Manipulation mode, nudges are used for navigation and
43  * rotation is used for "manipulating" the value of the selected {@link View}.
44  * <p>
45  * To reduce boilerplate, this class provides "no op" nudge and rotation behavior if
46  * no {@link View.OnKeyListener} or {@link View.OnGenericMotionListener} are provided as
47  * delegates for tackling the relevant events.
48  * <p>
49  * Allows {@link View}s that are within a {@link ViewGroup} to provide a link to their
50  * ancestor {@link ViewGroup} from which Direct Manipulation mode was first enabled. That way
51  * when the user finally exits Direct Manipulation mode, both objects are restored to their
52  * original state.
53  */
54 public class DirectManipulationHandler implements View.OnKeyListener,
55         View.OnGenericMotionListener {
56 
57     private final DirectManipulationState mDirectManipulationMode;
58     private final View.OnKeyListener mNudgeDelegate;
59     private final View.OnGenericMotionListener mRotationDelegate;
60 
61     /**
62      * A builder for {@link DirectManipulationHandler}.
63      */
64     public static class Builder {
65         private final DirectManipulationState mDmState;
66         private View.OnKeyListener mNudgeDelegate;
67         private View.OnGenericMotionListener mRotationDelegate;
68 
Builder(DirectManipulationState dmState)69         public Builder(DirectManipulationState dmState) {
70             Preconditions.checkNotNull(dmState);
71             this.mDmState = dmState;
72         }
73 
setNudgeHandler(View.OnKeyListener directionalDelegate)74         public Builder setNudgeHandler(View.OnKeyListener directionalDelegate) {
75             Preconditions.checkNotNull(directionalDelegate);
76             this.mNudgeDelegate = directionalDelegate;
77             return this;
78         }
79 
setRotationHandler(View.OnGenericMotionListener motionDelegate)80         public Builder setRotationHandler(View.OnGenericMotionListener motionDelegate) {
81             Preconditions.checkNotNull(motionDelegate);
82             this.mRotationDelegate = motionDelegate;
83             return this;
84         }
85 
build()86         public DirectManipulationHandler build() {
87             if (mNudgeDelegate == null && mRotationDelegate == null) {
88                 throw new IllegalStateException("At least one delegate must be provided.");
89             }
90             return new DirectManipulationHandler(mDmState, mNudgeDelegate, mRotationDelegate);
91         }
92     }
93 
DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, @Nullable View.OnGenericMotionListener rotationDelegate)94     private DirectManipulationHandler(DirectManipulationState dmState,
95             @Nullable View.OnKeyListener nudgeDelegate,
96             @Nullable View.OnGenericMotionListener rotationDelegate) {
97         Preconditions.checkNotNull(dmState);
98         mDirectManipulationMode = dmState;
99         mNudgeDelegate = nudgeDelegate;
100         mRotationDelegate = rotationDelegate;
101     }
102 
103     @Override
onKey(View view, int keyCode, KeyEvent keyEvent)104     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
105         boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
106         Log.d(L.TAG, "View: " + view + " is handling " + keyCode
107                 + " and action " + keyEvent.getAction()
108                 + " direct manipulation mode is "
109                 + (mDirectManipulationMode.isActive() ? "active" : "inactive"));
110 
111         switch (keyCode) {
112             case KeyEvent.KEYCODE_DPAD_CENTER:
113                 // If not yet in Direct Manipulation mode, switch to that mode.
114 
115                 if (!mDirectManipulationMode.isActive() && isActionUp) {
116                     mDirectManipulationMode.enable(view);
117                 }
118                 return true;
119             case KeyEvent.KEYCODE_BACK:
120                 // If in Direct Manipulation mode, exit, and clean up state.
121                 if (mDirectManipulationMode.isActive() && isActionUp) {
122                     mDirectManipulationMode.disable();
123                 }
124                 return true;
125             default:
126                 // This handler is only responsible for behavior during Direct Manipulation
127                 // mode. When the mode is disabled, ignore events.
128                 if (!mDirectManipulationMode.isActive()) {
129                     return false;
130                 }
131                 // If no delegate present, silently consume the events.
132                 if (mNudgeDelegate == null) {
133                     return true;
134                 }
135                 return mNudgeDelegate.onKey(view, keyCode, keyEvent);
136         }
137     }
138 
139     @Override
onGenericMotion(View v, MotionEvent event)140     public boolean onGenericMotion(View v, MotionEvent event) {
141         // This handler is only responsible for behavior during Direct Manipulation
142         // mode. When the mode is disabled, ignore events.
143         if (!mDirectManipulationMode.isActive()) {
144             return false;
145         }
146         // If no delegate present, silently consume the events.
147         if (mRotationDelegate == null) {
148             return true;
149         }
150         return mRotationDelegate.onGenericMotion(v, event);
151     }
152 }
153