1 /* 2 * Copyright (C) 2014 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.example.android.visualgamecontroller; 18 19 import android.annotation.TargetApi; 20 import android.app.Activity; 21 import android.content.Context; 22 import android.hardware.input.InputManager; 23 import android.hardware.input.InputManager.InputDeviceListener; 24 import android.os.Build; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.InputDevice; 29 import android.view.KeyEvent; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.WindowManager; 33 34 import com.example.android.visualgamecontroller.util.SystemUiHider; 35 36 import java.util.ArrayList; 37 38 /** 39 * An example full-screen activity that shows and hides the system UI (i.e. 40 * status bar and navigation/system bar) with user interaction. 41 * 42 * @see SystemUiHider 43 */ 44 public class FullscreenActivity extends Activity implements InputDeviceListener { 45 private static final String TAG = "FullscreenActivity"; 46 47 /** 48 * Whether or not the system UI should be auto-hidden after 49 * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds. 50 */ 51 private static final boolean AUTO_HIDE = true; 52 53 /** 54 * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after 55 * user interaction before hiding the system UI. 56 */ 57 private static final int AUTO_HIDE_DELAY_MILLIS = 3000; 58 59 /** 60 * If set, will toggle the system UI visibility upon interaction. Otherwise, 61 * will show the system UI visibility upon interaction. 62 */ 63 private static final boolean TOGGLE_ON_CLICK = true; 64 65 /** 66 * The flags to pass to {@link SystemUiHider#getInstance}. 67 */ 68 private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION; 69 70 /** 71 * The instance of the {@link SystemUiHider} for this activity. 72 */ 73 private SystemUiHider mSystemUiHider; 74 75 private ControllerView mControllerView; 76 77 public enum ButtonMapping { 78 BUTTON_A(KeyEvent.KEYCODE_BUTTON_A), 79 BUTTON_B(KeyEvent.KEYCODE_BUTTON_B), 80 BUTTON_X(KeyEvent.KEYCODE_BUTTON_X), 81 BUTTON_Y(KeyEvent.KEYCODE_BUTTON_Y), 82 BUTTON_L1(KeyEvent.KEYCODE_BUTTON_L1), 83 BUTTON_R1(KeyEvent.KEYCODE_BUTTON_R1), 84 BUTTON_L2(KeyEvent.KEYCODE_BUTTON_L2), 85 BUTTON_R2(KeyEvent.KEYCODE_BUTTON_R2), 86 BUTTON_SELECT(KeyEvent.KEYCODE_BUTTON_SELECT), 87 BUTTON_START(KeyEvent.KEYCODE_BUTTON_START), 88 BUTTON_THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL), 89 BUTTON_THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR), 90 BACK(KeyEvent.KEYCODE_BACK), 91 POWER(KeyEvent.KEYCODE_BUTTON_MODE); 92 93 private final int mKeyCode; 94 ButtonMapping(int keyCode)95 ButtonMapping(int keyCode) { 96 mKeyCode = keyCode; 97 } 98 getKeycode()99 private int getKeycode() { 100 return mKeyCode; 101 } 102 } 103 104 public enum AxesMapping { 105 AXIS_X(MotionEvent.AXIS_X), 106 AXIS_Y(MotionEvent.AXIS_Y), 107 AXIS_Z(MotionEvent.AXIS_Z), 108 AXIS_RZ(MotionEvent.AXIS_RZ), 109 AXIS_HAT_X(MotionEvent.AXIS_HAT_X), 110 AXIS_HAT_Y(MotionEvent.AXIS_HAT_Y), 111 AXIS_LTRIGGER(MotionEvent.AXIS_LTRIGGER), 112 AXIS_RTRIGGER(MotionEvent.AXIS_RTRIGGER), 113 AXIS_BRAKE(MotionEvent.AXIS_BRAKE), 114 AXIS_GAS(MotionEvent.AXIS_GAS); 115 116 private final int mMotionEvent; 117 AxesMapping(int motionEvent)118 AxesMapping(int motionEvent) { 119 mMotionEvent = motionEvent; 120 } 121 getMotionEvent()122 private int getMotionEvent() { 123 return mMotionEvent; 124 } 125 } 126 127 private int[] mButtons = new int[ButtonMapping.values().length]; 128 private float[] mAxes = new float[AxesMapping.values().length]; 129 private InputManager mInputManager; 130 private ArrayList<Integer> mConnectedDevices = new ArrayList<Integer>(); 131 private int mCurrentDeviceId = -1; 132 133 @Override onCreate(Bundle savedInstanceState)134 protected void onCreate(Bundle savedInstanceState) { 135 super.onCreate(savedInstanceState); 136 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 137 setContentView(R.layout.activity_fullscreen); 138 139 final View controlsView = findViewById(R.id.fullscreen_content_controls); 140 final View contentView = findViewById(R.id.fullscreen_content); 141 142 mControllerView = (ControllerView) findViewById(R.id.controller); 143 for (int i = 0; i < mButtons.length; i++) { 144 mButtons[i] = 0; 145 } 146 for (int i = 0; i < mAxes.length; i++) { 147 mAxes[i] = 0.0f; 148 } 149 mControllerView.setButtonsAxes(mButtons, mAxes); 150 151 // Set up an instance of SystemUiHider to control the system UI for 152 // this activity. 153 mSystemUiHider = SystemUiHider.getInstance(this, contentView, HIDER_FLAGS); 154 mSystemUiHider.setup(); 155 mSystemUiHider 156 .setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() { 157 // Cached values. 158 int mControlsHeight; 159 int mShortAnimTime; 160 161 @Override 162 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 163 public void onVisibilityChange(boolean visible) { 164 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 165 // If the ViewPropertyAnimator API is available 166 // (Honeycomb MR2 and later), use it to animate the 167 // in-layout UI controls at the bottom of the 168 // screen. 169 if (mControlsHeight == 0) { 170 mControlsHeight = controlsView.getHeight(); 171 } 172 if (mShortAnimTime == 0) { 173 mShortAnimTime = getResources().getInteger( 174 android.R.integer.config_shortAnimTime); 175 } 176 controlsView.animate() 177 .translationY(visible ? 0 : mControlsHeight) 178 .setDuration(mShortAnimTime); 179 } else { 180 // If the ViewPropertyAnimator APIs aren't 181 // available, simply show or hide the in-layout UI 182 // controls. 183 controlsView.setVisibility(visible ? View.VISIBLE : View.GONE); 184 } 185 186 if (visible && AUTO_HIDE) { 187 // Schedule a hide(). 188 delayedHide(AUTO_HIDE_DELAY_MILLIS); 189 } 190 } 191 }); 192 193 // Set up the user interaction to manually show or hide the system UI. 194 contentView.setOnClickListener(new View.OnClickListener() { 195 @Override 196 public void onClick(View view) { 197 if (TOGGLE_ON_CLICK) { 198 mSystemUiHider.toggle(); 199 } else { 200 mSystemUiHider.show(); 201 } 202 } 203 }); 204 205 mInputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); 206 checkGameControllers(); 207 } 208 209 /** 210 * Check for any game controllers that are connected already. 211 */ checkGameControllers()212 private void checkGameControllers() { 213 Log.d(TAG, "checkGameControllers"); 214 int[] deviceIds = mInputManager.getInputDeviceIds(); 215 for (int deviceId : deviceIds) { 216 InputDevice dev = InputDevice.getDevice(deviceId); 217 int sources = dev.getSources(); 218 219 // Verify that the device has gamepad buttons, control sticks, or 220 // both. 221 if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) 222 || ((sources & InputDevice.SOURCE_JOYSTICK) 223 == InputDevice.SOURCE_JOYSTICK)) { 224 // This device is a game controller. Store its device ID. 225 if (!mConnectedDevices.contains(deviceId)) { 226 mConnectedDevices.add(deviceId); 227 if (mCurrentDeviceId == -1) { 228 mCurrentDeviceId = deviceId; 229 mControllerView.setCurrentControllerNumber(dev.getControllerNumber()); 230 mControllerView.invalidate(); 231 } 232 } 233 } 234 } 235 } 236 237 @Override onPostCreate(Bundle savedInstanceState)238 protected void onPostCreate(Bundle savedInstanceState) { 239 super.onPostCreate(savedInstanceState); 240 241 // Trigger the initial hide() shortly after the activity has been 242 // created, to briefly hint to the user that UI controls 243 // are available. 244 delayedHide(100); 245 } 246 247 @Override onResume()248 protected void onResume() { 249 super.onResume(); 250 mInputManager.registerInputDeviceListener(this, null); 251 } 252 253 @Override onPause()254 protected void onPause() { 255 super.onPause(); 256 mInputManager.unregisterInputDeviceListener(this); 257 } 258 259 /** 260 * Touch listener to use for in-layout UI controls to delay hiding the 261 * system UI. This is to prevent the jarring behavior of controls going away 262 * while interacting with activity UI. 263 */ 264 View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() { 265 @Override 266 public boolean onTouch(View view, MotionEvent motionEvent) { 267 if (AUTO_HIDE) { 268 delayedHide(AUTO_HIDE_DELAY_MILLIS); 269 } 270 return false; 271 } 272 }; 273 274 Handler mHideHandler = new Handler(); 275 Runnable mHideRunnable = new Runnable() { 276 @Override 277 public void run() { 278 mSystemUiHider.hide(); 279 } 280 }; 281 282 /** 283 * Schedules a call to hide() in [delay] milliseconds, canceling any 284 * previously scheduled calls. 285 */ delayedHide(int delayMillis)286 private void delayedHide(int delayMillis) { 287 mHideHandler.removeCallbacks(mHideRunnable); 288 mHideHandler.postDelayed(mHideRunnable, delayMillis); 289 } 290 291 /* 292 * (non-Javadoc) 293 * @see android.app.Activity#onGenericMotionEvent(android.view.MotionEvent) 294 */ 295 @Override onGenericMotionEvent(final MotionEvent ev)296 public boolean onGenericMotionEvent(final MotionEvent ev) { 297 // Log.d(TAG, "onGenericMotionEvent: " + ev); 298 InputDevice device = ev.getDevice(); 299 // Only care about game controllers. 300 if (device != null && device.getId() == mCurrentDeviceId) { 301 if (isGamepad(device)) { 302 for (AxesMapping axesMapping : AxesMapping.values()) { 303 mAxes[axesMapping.ordinal()] = getCenteredAxis(ev, device, 304 axesMapping.getMotionEvent()); 305 } 306 mControllerView.invalidate(); 307 return true; 308 } 309 } 310 return super.onGenericMotionEvent(ev); 311 } 312 313 /** 314 * Get centered position for axis input by considering flat area. 315 * 316 * @param event 317 * @param device 318 * @param axis 319 * @return 320 */ getCenteredAxis(MotionEvent event, InputDevice device, int axis)321 private float getCenteredAxis(MotionEvent event, InputDevice device, int axis) { 322 InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); 323 324 // A joystick at rest does not always report an absolute position of 325 // (0,0). Use the getFlat() method to determine the range of values 326 // bounding the joystick axis center. 327 if (range != null) { 328 float flat = range.getFlat(); 329 float value = event.getAxisValue(axis); 330 331 // Ignore axis values that are within the 'flat' region of the 332 // joystick axis center. 333 if (Math.abs(value) > flat) { 334 return value; 335 } 336 } 337 return 0; 338 } 339 340 /* 341 * (non-Javadoc) 342 * @see android.support.v4.app.FragmentActivity#onKeyDown(int, 343 * android.view.KeyEvent) 344 */ 345 @Override onKeyDown(final int keyCode, KeyEvent ev)346 public boolean onKeyDown(final int keyCode, KeyEvent ev) { 347 // Log.d(TAG, "onKeyDown: " + ev); 348 InputDevice device = ev.getDevice(); 349 // Only care about game controllers. 350 if (device != null && device.getId() == mCurrentDeviceId) { 351 if (isGamepad(device)) { 352 int index = getButtonMappingIndex(keyCode); 353 if (index >= 0) { 354 mButtons[index] = 1; 355 mControllerView.invalidate(); 356 } 357 return true; 358 } 359 } 360 return super.onKeyDown(keyCode, ev); 361 } 362 363 /* 364 * (non-Javadoc) 365 * @see android.app.Activity#onKeyUp(int, android.view.KeyEvent) 366 */ 367 @Override onKeyUp(final int keyCode, KeyEvent ev)368 public boolean onKeyUp(final int keyCode, KeyEvent ev) { 369 // Log.d(TAG, "onKeyUp: " + ev); 370 InputDevice device = ev.getDevice(); 371 // Only care about game controllers. 372 if (device != null && device.getId() == mCurrentDeviceId) { 373 if (isGamepad(device)) { 374 int index = getButtonMappingIndex(keyCode); 375 if (index >= 0) { 376 mButtons[index] = 0; 377 mControllerView.invalidate(); 378 } 379 return true; 380 } 381 } 382 return super.onKeyUp(keyCode, ev); 383 } 384 385 /** 386 * Utility method to determine if input device is a gamepad. 387 * 388 * @param device 389 * @return 390 */ isGamepad(InputDevice device)391 private boolean isGamepad(InputDevice device) { 392 if ((device.getSources() & 393 InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD 394 || (device.getSources() & 395 InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) { 396 return true; 397 } 398 return false; 399 } 400 401 /** 402 * Get the array index for the key code. 403 * 404 * @param keyCode 405 * @return 406 */ getButtonMappingIndex(int keyCode)407 private int getButtonMappingIndex(int keyCode) { 408 for (ButtonMapping buttonMapping : ButtonMapping.values()) { 409 if (buttonMapping.getKeycode() == keyCode) { 410 return buttonMapping.ordinal(); 411 } 412 } 413 return -1; 414 } 415 416 /* 417 * (non-Javadoc) 418 * @see 419 * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceAdded 420 * (int) 421 */ 422 @Override onInputDeviceAdded(int deviceId)423 public void onInputDeviceAdded(int deviceId) { 424 Log.d(TAG, "onInputDeviceAdded: " + deviceId); 425 if (!mConnectedDevices.contains(deviceId)) { 426 mConnectedDevices.add(new Integer(deviceId)); 427 } 428 if (mCurrentDeviceId == -1) { 429 mCurrentDeviceId = deviceId; 430 InputDevice dev = InputDevice.getDevice(mCurrentDeviceId); 431 if (dev != null) { 432 mControllerView.setCurrentControllerNumber(dev.getControllerNumber()); 433 mControllerView.invalidate(); 434 } 435 } 436 } 437 438 /* 439 * (non-Javadoc) 440 * @see 441 * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceRemoved 442 * (int) 443 */ 444 @Override onInputDeviceRemoved(int deviceId)445 public void onInputDeviceRemoved(int deviceId) { 446 Log.d(TAG, "onInputDeviceRemoved: " + deviceId); 447 mConnectedDevices.remove(new Integer(deviceId)); 448 if (mCurrentDeviceId == deviceId) { 449 mCurrentDeviceId = -1; 450 } 451 if (mConnectedDevices.size() == 0) { 452 mControllerView.setCurrentControllerNumber(-1); 453 mControllerView.invalidate(); 454 } else { 455 mCurrentDeviceId = mConnectedDevices.get(0); 456 InputDevice dev = InputDevice.getDevice(mCurrentDeviceId); 457 if (dev != null) { 458 mControllerView.setCurrentControllerNumber(dev.getControllerNumber()); 459 mControllerView.invalidate(); 460 } 461 } 462 } 463 464 /* 465 * (non-Javadoc) 466 * @see 467 * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceChanged 468 * (int) 469 */ 470 @Override onInputDeviceChanged(int deviceId)471 public void onInputDeviceChanged(int deviceId) { 472 Log.d(TAG, "onInputDeviceChanged: " + deviceId); 473 mControllerView.invalidate(); 474 } 475 476 } 477