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     /**
58      * Sets the provided {@link DirectManipulationHandler} to the key listener and motion
59      * listener of the provided view.
60      */
setDirectManipulationHandler(@ullable View view, DirectManipulationHandler handler)61     public static void setDirectManipulationHandler(@Nullable View view,
62             DirectManipulationHandler handler) {
63         if (view == null) {
64             return;
65         }
66         view.setOnKeyListener(handler);
67         view.setOnGenericMotionListener(handler);
68     }
69 
70     /**
71      * A builder for {@link DirectManipulationHandler}.
72      */
73     public static class Builder {
74         private final DirectManipulationState mDmState;
75         private View.OnKeyListener mNudgeDelegate;
76         private View.OnGenericMotionListener mRotationDelegate;
77         private View.OnKeyListener mBackDelegate;
78 
Builder(DirectManipulationState dmState)79         public Builder(DirectManipulationState dmState) {
80             Preconditions.checkNotNull(dmState);
81             this.mDmState = dmState;
82         }
83 
setNudgeHandler(View.OnKeyListener nudgeDelegate)84         public Builder setNudgeHandler(View.OnKeyListener nudgeDelegate) {
85             Preconditions.checkNotNull(nudgeDelegate);
86             this.mNudgeDelegate = nudgeDelegate;
87             return this;
88         }
89 
setBackHandler(View.OnKeyListener backDelegate)90         public Builder setBackHandler(View.OnKeyListener backDelegate) {
91             Preconditions.checkNotNull(backDelegate);
92             this.mBackDelegate = backDelegate;
93             return this;
94         }
95 
setRotationHandler(View.OnGenericMotionListener rotationDelegate)96         public Builder setRotationHandler(View.OnGenericMotionListener rotationDelegate) {
97             Preconditions.checkNotNull(rotationDelegate);
98             this.mRotationDelegate = rotationDelegate;
99             return this;
100         }
101 
build()102         public DirectManipulationHandler build() {
103             if (mNudgeDelegate == null && mRotationDelegate == null) {
104                 throw new IllegalStateException("Nudge and/or rotation delegate must be provided.");
105             }
106             return new DirectManipulationHandler(mDmState, mNudgeDelegate, mBackDelegate,
107                     mRotationDelegate);
108         }
109     }
110 
111     private final DirectManipulationState mDirectManipulationMode;
112     private final View.OnKeyListener mNudgeDelegate;
113     private final View.OnKeyListener mBackDelegate;
114     private final View.OnGenericMotionListener mRotationDelegate;
115 
DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, @Nullable View.OnKeyListener backDelegate, @Nullable View.OnGenericMotionListener rotationDelegate)116     private DirectManipulationHandler(DirectManipulationState dmState,
117             @Nullable View.OnKeyListener nudgeDelegate,
118             @Nullable View.OnKeyListener backDelegate,
119             @Nullable View.OnGenericMotionListener rotationDelegate) {
120         mDirectManipulationMode = dmState;
121         mNudgeDelegate = nudgeDelegate;
122         mBackDelegate = backDelegate;
123         mRotationDelegate = rotationDelegate;
124     }
125 
126     @Override
onKey(View view, int keyCode, KeyEvent keyEvent)127     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
128         boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
129         Log.d(L.TAG, "View: " + view + " is handling " + KeyEvent.keyCodeToString(keyCode)
130                 + " and action " + keyEvent.getAction()
131                 + " direct manipulation mode is "
132                 + (mDirectManipulationMode.isActive() ? "active" : "inactive"));
133 
134         switch (keyCode) {
135             case KeyEvent.KEYCODE_DPAD_CENTER:
136                 // If not yet in Direct Manipulation mode, switch to that mode.
137 
138                 if (!mDirectManipulationMode.isActive() && isActionUp) {
139                     mDirectManipulationMode.enable(view);
140                 }
141                 return true;
142             case KeyEvent.KEYCODE_BACK:
143                 // If in Direct Manipulation mode, exit, and clean up state.
144                 if (mDirectManipulationMode.isActive() && isActionUp) {
145                     mDirectManipulationMode.disable();
146                 }
147                 // If no delegate is present, silently consume the events.
148                 if (mBackDelegate == null) {
149                     return true;
150                 }
151 
152                 return mBackDelegate.onKey(view, keyCode, keyEvent);
153             case KeyEvent.KEYCODE_DPAD_UP:
154             case KeyEvent.KEYCODE_DPAD_DOWN:
155             case KeyEvent.KEYCODE_DPAD_LEFT:
156             case KeyEvent.KEYCODE_DPAD_RIGHT:
157                 // This handler is only responsible for nudging behavior during Direct Manipulation
158                 // mode. When the mode is disabled, ignore events.
159                 if (!mDirectManipulationMode.isActive()) {
160                     return false;
161                 }
162                 // If no delegate is present, silently consume the events.
163                 if (mNudgeDelegate == null) {
164                     return true;
165                 }
166                 return mNudgeDelegate.onKey(view, keyCode, keyEvent);
167             default:
168                 // Ignore all other key events.
169                 return false;
170         }
171     }
172 
173     @Override
onGenericMotion(View v, MotionEvent event)174     public boolean onGenericMotion(View v, MotionEvent event) {
175         // This handler is only responsible for behavior during Direct Manipulation
176         // mode. When the mode is disabled, ignore events.
177         if (!mDirectManipulationMode.isActive()) {
178             return false;
179         }
180         // If no delegate is present, silently consume the events.
181         if (mRotationDelegate == null) {
182             return true;
183         }
184         return mRotationDelegate.onGenericMotion(v, event);
185     }
186 }
187