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