GamepadList.java revision 34680572440d7894ef8dafce81d8039ed80726a2
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
5package org.chromium.content.browser.input;
6
7import android.annotation.SuppressLint;
8import android.annotation.TargetApi;
9import android.content.Context;
10import android.hardware.input.InputManager;
11import android.hardware.input.InputManager.InputDeviceListener;
12import android.os.Build;
13import android.view.InputDevice;
14import android.view.InputEvent;
15import android.view.KeyEvent;
16import android.view.MotionEvent;
17
18import org.chromium.base.CalledByNative;
19import org.chromium.base.JNINamespace;
20import 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")
29public 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)
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)
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     */
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)
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")
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)
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
123    private void onInputDeviceChangedImpl(int deviceId) {}
124
125    private void onInputDeviceRemovedImpl(int deviceId) {
126        synchronized (mLock) {
127            unregisterGamepad(deviceId);
128        }
129    }
130
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
141    private static GamepadList getInstance() {
142        assert isGamepadSupported();
143        return LazyHolder.INSTANCE;
144    }
145
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
156    private boolean isDeviceConnected(int index) {
157        if (index < MAX_GAMEPADS && getDevice(index) != null) {
158            return true;
159        }
160        return false;
161    }
162
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
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