• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.content.browser.input;
6 
7 import android.annotation.SuppressLint;
8 import android.annotation.TargetApi;
9 import android.content.Context;
10 import android.hardware.input.InputManager;
11 import android.hardware.input.InputManager.InputDeviceListener;
12 import android.os.Build;
13 import android.view.InputDevice;
14 import android.view.InputEvent;
15 import android.view.KeyEvent;
16 import android.view.MotionEvent;
17 
18 import org.chromium.base.CalledByNative;
19 import org.chromium.base.JNINamespace;
20 import org.chromium.base.ThreadUtils;
21 
22 /**
23  * Class to manage connected gamepad devices list.
24  *
25  * It is a Java counterpart of GamepadPlatformDataFetcherAndroid and feeds Gamepad API with input
26  * data.
27  */
28 @JNINamespace("content")
29 public class GamepadList {
30     private static final int MAX_GAMEPADS = 4;
31 
32     private final Object mLock = new Object();
33 
34     private final GamepadDevice[] mGamepadDevices = new GamepadDevice[MAX_GAMEPADS];
35     private InputManager mInputManager;
36     private int mAttachedToWindowCounter;
37     private boolean mIsGamepadAccessed;
38     private InputDeviceListener mInputDeviceListener;
39 
40     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
GamepadList()41     private GamepadList() {
42         mInputDeviceListener = new InputDeviceListener() {
43             // Override InputDeviceListener methods
44             @Override
45             public void onInputDeviceChanged(int deviceId) {
46                 onInputDeviceChangedImpl(deviceId);
47             }
48 
49             @Override
50             public void onInputDeviceRemoved(int deviceId) {
51                 onInputDeviceRemovedImpl(deviceId);
52             }
53 
54             @Override
55             public void onInputDeviceAdded(int deviceId) {
56                 onInputDeviceAddedImpl(deviceId);
57             }
58         };
59     }
60 
61     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
initializeDevices()62     private void initializeDevices() {
63         // Get list of all the attached input devices.
64         int[] deviceIds = mInputManager.getInputDeviceIds();
65         for (int i = 0; i < deviceIds.length; i++) {
66             InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]);
67             // Check for gamepad device
68             if (isGamepadDevice(inputDevice)) {
69                 // Register a new gamepad device.
70                 registerGamepad(inputDevice);
71             }
72         }
73     }
74 
75     /**
76      * Notifies the GamepadList that a {@link ContentView} is attached to a window and it should
77      * prepare itself for gamepad input. It must be called before {@link onGenericMotionEvent} and
78      * {@link dispatchKeyEvent}.
79      */
onAttachedToWindow(Context context)80     public static void onAttachedToWindow(Context context) {
81         assert ThreadUtils.runningOnUiThread();
82         if (!isGamepadSupported()) return;
83         getInstance().attachedToWindow(context);
84     }
85 
86     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
attachedToWindow(Context context)87     private void attachedToWindow(Context context) {
88         if (mAttachedToWindowCounter++ == 0) {
89             mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
90             synchronized (mLock) {
91                 initializeDevices();
92             }
93             // Register an input device listener.
94             mInputManager.registerInputDeviceListener(mInputDeviceListener, null);
95         }
96     }
97 
98     /**
99      * Notifies the GamepadList that a {@link ContentView} is detached from it's window.
100      */
101     @SuppressLint("MissingSuperCall")
onDetachedFromWindow()102     public static void onDetachedFromWindow() {
103         assert ThreadUtils.runningOnUiThread();
104         if (!isGamepadSupported()) return;
105         getInstance().detachedFromWindow();
106     }
107 
108     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
detachedFromWindow()109     private void detachedFromWindow() {
110         if (--mAttachedToWindowCounter == 0) {
111             synchronized (mLock) {
112                 for (int i = 0; i < MAX_GAMEPADS; ++i) {
113                     mGamepadDevices[i] = null;
114                 }
115             }
116             mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
117             mInputManager = null;
118         }
119     }
120 
121     // ------------------------------------------------------------
122 
onInputDeviceChangedImpl(int deviceId)123     private void onInputDeviceChangedImpl(int deviceId) {}
124 
onInputDeviceRemovedImpl(int deviceId)125     private void onInputDeviceRemovedImpl(int deviceId) {
126         synchronized (mLock) {
127             unregisterGamepad(deviceId);
128         }
129     }
130 
onInputDeviceAddedImpl(int deviceId)131     private void onInputDeviceAddedImpl(int deviceId) {
132         InputDevice inputDevice = InputDevice.getDevice(deviceId);
133         if (!isGamepadDevice(inputDevice)) return;
134         synchronized (mLock) {
135             registerGamepad(inputDevice);
136         }
137     }
138 
139     // ------------------------------------------------------------
140 
getInstance()141     private static GamepadList getInstance() {
142         assert isGamepadSupported();
143         return LazyHolder.INSTANCE;
144     }
145 
getDeviceCount()146     private int getDeviceCount() {
147         int count = 0;
148         for (int i = 0; i < MAX_GAMEPADS; i++) {
149             if (getDevice(i) != null) {
150                 count++;
151             }
152         }
153         return count;
154     }
155 
isDeviceConnected(int index)156     private boolean isDeviceConnected(int index) {
157         if (index < MAX_GAMEPADS && getDevice(index) != null) {
158             return true;
159         }
160         return false;
161     }
162 
getDeviceById(int deviceId)163     private GamepadDevice getDeviceById(int deviceId) {
164         for (int i = 0; i < MAX_GAMEPADS; i++) {
165             GamepadDevice gamepad = mGamepadDevices[i];
166             if (gamepad != null && gamepad.getId() == deviceId) {
167                 return gamepad;
168             }
169         }
170         return null;
171     }
172 
getDevice(int index)173     private GamepadDevice getDevice(int index) {
174         // Maximum 4 Gamepads can be connected at a time starting at index zero.
175         assert index >= 0 && index < MAX_GAMEPADS;
176         return mGamepadDevices[index];
177     }
178 
179     /**
180      * Handles key events from the gamepad devices.
181      * @return True if the event has been consumed.
182      */
183     public static boolean dispatchKeyEvent(KeyEvent event) {
184         if (!isGamepadSupported()) return false;
185         if (!isGamepadEvent(event)) return false;
186         return getInstance().handleKeyEvent(event);
187     }
188 
189     private boolean handleKeyEvent(KeyEvent event) {
190         synchronized (mLock) {
191             if (!mIsGamepadAccessed) return false;
192             GamepadDevice gamepad = getGamepadForEvent(event);
193             if (gamepad == null) return false;
194             return gamepad.handleKeyEvent(event);
195         }
196     }
197 
198     /**
199      * Handles motion events from the gamepad devices.
200      * @return True if the event has been consumed.
201      */
202     public static boolean onGenericMotionEvent(MotionEvent event) {
203         if (!isGamepadSupported()) return false;
204         if (!isGamepadEvent(event)) return false;
205         return getInstance().handleMotionEvent(event);
206     }
207 
208     private boolean handleMotionEvent(MotionEvent event) {
209         synchronized (mLock) {
210             if (!mIsGamepadAccessed) return false;
211             GamepadDevice gamepad = getGamepadForEvent(event);
212             if (gamepad == null) return false;
213             return gamepad.handleMotionEvent(event);
214         }
215     }
216 
217     private int getNextAvailableIndex() {
218         // When multiple gamepads are connected to a user agent, indices must be assigned on a
219         // first-come first-serve basis, starting at zero. If a gamepad is disconnected, previously
220         // assigned indices must not be reassigned to gamepads that continue to be connected.
221         // However, if a gamepad is disconnected, and subsequently the same or a different
222         // gamepad is then connected, index entries must be reused.
223 
224         for (int i = 0; i < MAX_GAMEPADS; ++i) {
225             if (getDevice(i) == null) {
226                 return i;
227             }
228         }
229         // Reached maximum gamepads limit.
230         return -1;
231     }
232 
233     private boolean registerGamepad(InputDevice inputDevice) {
234         int index = getNextAvailableIndex();
235         if (index == -1) return false; // invalid index
236 
237         GamepadDevice gamepad = new GamepadDevice(index, inputDevice);
238         mGamepadDevices[index] = gamepad;
239         return true;
240     }
241 
242     private void unregisterGamepad(int deviceId) {
243         GamepadDevice gamepadDevice = getDeviceById(deviceId);
244         if (gamepadDevice == null) return; // Not a registered device.
245         int index = gamepadDevice.getIndex();
246         mGamepadDevices[index] = null;
247     }
248 
249     private static boolean isGamepadDevice(InputDevice inputDevice) {
250         if (inputDevice == null) return false;
251         return ((inputDevice.getSources() & InputDevice.SOURCE_JOYSTICK) ==
252                 InputDevice.SOURCE_JOYSTICK);
253     }
254 
255     private GamepadDevice getGamepadForEvent(InputEvent event) {
256         return getDeviceById(event.getDeviceId());
257     }
258 
259     /**
260      * @return True if the motion event corresponds to a gamepad event.
261      */
262     public static boolean isGamepadEvent(MotionEvent event) {
263         return ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
264     }
265 
266     /**
267      * @return True if event's keycode corresponds to a gamepad key.
268      */
269     public static boolean isGamepadEvent(KeyEvent event) {
270         int keyCode = event.getKeyCode();
271         switch (keyCode) {
272         // Specific handling for dpad keys is required because
273         // KeyEvent.isGamepadButton doesn't consider dpad keys.
274             case KeyEvent.KEYCODE_DPAD_UP:
275             case KeyEvent.KEYCODE_DPAD_DOWN:
276             case KeyEvent.KEYCODE_DPAD_LEFT:
277             case KeyEvent.KEYCODE_DPAD_RIGHT:
278                 return true;
279             default:
280                 return KeyEvent.isGamepadButton(keyCode);
281         }
282     }
283 
284     private static boolean isGamepadSupported() {
285         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
286     }
287 
288     @CalledByNative
289     static void updateGamepadData(long webGamepadsPtr) {
290         if (!isGamepadSupported()) return;
291         getInstance().grabGamepadData(webGamepadsPtr);
292     }
293 
294     private void grabGamepadData(long webGamepadsPtr) {
295         synchronized (mLock) {
296             for (int i = 0; i < MAX_GAMEPADS; i++) {
297                 final GamepadDevice device = getDevice(i);
298                 if (device != null) {
299                     device.updateButtonsAndAxesMapping();
300                     nativeSetGamepadData(webGamepadsPtr, i, device.isStandardGamepad(), true,
301                             device.getName(), device.getTimestamp(), device.getAxes(),
302                             device.getButtons());
303                 } else {
304                     nativeSetGamepadData(webGamepadsPtr, i, false, false, null, 0, null, null);
305                 }
306             }
307         }
308     }
309 
310     @CalledByNative
311     static void notifyForGamepadsAccess(boolean isAccessPaused) {
312         if (!isGamepadSupported()) return;
313         getInstance().setIsGamepadAccessed(!isAccessPaused);
314     }
315 
316     private void setIsGamepadAccessed(boolean isGamepadAccessed) {
317         synchronized (mLock) {
318             mIsGamepadAccessed = isGamepadAccessed;
319             if (isGamepadAccessed) {
320                 for (int i = 0; i < MAX_GAMEPADS; i++) {
321                     GamepadDevice gamepadDevice = getDevice(i);
322                     if (gamepadDevice == null) continue;
323                     gamepadDevice.clearData();
324                 }
325             }
326         }
327     }
328 
329     private native void nativeSetGamepadData(long webGamepadsPtr, int index, boolean mapping,
330             boolean connected, String devicename, long timestamp, float[] axes, float[] buttons);
331 
332     private static class LazyHolder {
333         private static final GamepadList INSTANCE = new GamepadList();
334     }
335 
336 }
337