summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorquddusc <quddusc@google.com>2014-01-22 11:37:25 -0800
committerAndroid Git Automerger <android-git-automerger@android.com>2014-01-22 11:37:25 -0800
commit176d0c68d5bc41d2332f355b1c5a1324a9b926d8 (patch)
tree01861ec755d93215d709c16e7e551946b23782ff
parent9678ee604ba5bfe1810b5773b1739af5919e7249 (diff)
parenta6dfbaf03710ff5c1099196d3907ddb31d85806f (diff)
downloadandroid_development-176d0c68d5bc41d2332f355b1c5a1324a9b926d8.tar.gz
android_development-176d0c68d5bc41d2332f355b1c5a1324a9b926d8.tar.bz2
android_development-176d0c68d5bc41d2332f355b1c5a1324a9b926d8.zip
am a6dfbaf0: am ca789a98: Merge "docs: Code sample for game controller training class." into jb-mr2-docs
* commit 'a6dfbaf03710ff5c1099196d3907ddb31d85806f': docs: Code sample for game controller training class.
-rw-r--r--samples/ControllerSample/AndroidManifest.xml29
-rw-r--r--samples/ControllerSample/libs/android-support-v4.jarbin0 -> 393154 bytes
-rw-r--r--samples/ControllerSample/proguard-project.txt20
-rw-r--r--samples/ControllerSample/project.properties14
-rw-r--r--samples/ControllerSample/res/drawable-hdpi/ic_launcher.pngbin0 -> 1109 bytes
-rw-r--r--samples/ControllerSample/res/drawable-mdpi/ic_launcher.pngbin0 -> 746 bytes
-rw-r--r--samples/ControllerSample/res/drawable-xhdpi/ic_launcher.pngbin0 -> 1546 bytes
-rw-r--r--samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 2330 bytes
-rw-r--r--samples/ControllerSample/res/layout/game_controller_input.xml40
-rw-r--r--samples/ControllerSample/res/values-v11/styles.xml11
-rw-r--r--samples/ControllerSample/res/values-v14/styles.xml12
-rw-r--r--samples/ControllerSample/res/values/strings.xml11
-rw-r--r--samples/ControllerSample/res/values/styles.xml20
-rw-r--r--samples/ControllerSample/src/com/example/controllersample/GameView.java1159
-rw-r--r--samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java30
-rw-r--r--samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java140
-rw-r--r--samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java107
-rw-r--r--samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java211
18 files changed, 1804 insertions, 0 deletions
diff --git a/samples/ControllerSample/AndroidManifest.xml b/samples/ControllerSample/AndroidManifest.xml
new file mode 100644
index 000000000..49b67d792
--- /dev/null
+++ b/samples/ControllerSample/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.controllersample"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.VIBRATE" />
+
+ <uses-sdk
+ android:minSdkVersion="9"
+ android:targetSdkVersion="18" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name=".GameViewActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/samples/ControllerSample/libs/android-support-v4.jar b/samples/ControllerSample/libs/android-support-v4.jar
new file mode 100644
index 000000000..65ebaf8dc
--- /dev/null
+++ b/samples/ControllerSample/libs/android-support-v4.jar
Binary files differ
diff --git a/samples/ControllerSample/proguard-project.txt b/samples/ControllerSample/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/samples/ControllerSample/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/samples/ControllerSample/project.properties b/samples/ControllerSample/project.properties
new file mode 100644
index 000000000..ce39f2d0a
--- /dev/null
+++ b/samples/ControllerSample/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
diff --git a/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..4f421f985
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..38651fd31
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..c6f6d8216
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..eef1c7695
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/layout/game_controller_input.xml b/samples/ControllerSample/res/layout/game_controller_input.xml
new file mode 100644
index 000000000..4e4a73590
--- /dev/null
+++ b/samples/ControllerSample/res/layout/game_controller_input.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+
+<!-- Game controller input demo. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="12dip"
+ android:text="@string/game_controller_input_description" />
+
+ <com.example.controllersample.GameView
+ android:id="@+id/game"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_margin="15dip"
+ android:layout_weight="1"
+ android:background="#000000" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/samples/ControllerSample/res/values-v11/styles.xml b/samples/ControllerSample/res/values-v11/styles.xml
new file mode 100644
index 000000000..541752f6e
--- /dev/null
+++ b/samples/ControllerSample/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!--
+ Base application theme for API 11+. This theme completely replaces
+ AppBaseTheme from res/values/styles.xml on API 11+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/samples/ControllerSample/res/values-v14/styles.xml b/samples/ControllerSample/res/values-v14/styles.xml
new file mode 100644
index 000000000..f20e01501
--- /dev/null
+++ b/samples/ControllerSample/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+<resources>
+
+ <!--
+ Base application theme for API 14+. This theme completely replaces
+ AppBaseTheme from BOTH res/values/styles.xml and
+ res/values-v11/styles.xml on API 14+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/samples/ControllerSample/res/values/strings.xml b/samples/ControllerSample/res/values/strings.xml
new file mode 100644
index 000000000..ba8e7d707
--- /dev/null
+++ b/samples/ControllerSample/res/values/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Controller Sample</string>
+ <string name="game_controller_input_description">
+ This activity demonstrates how to process input events received from
+ game controllers. Please connect your game controller now and try
+ moving the joysticks or pressing buttons. If it helps, try to imagine
+ that you are a lone space cowboy in hot pursuit of the aliens who kidnapped
+ your favorite llama on their way back to Andromeda&#8230;
+ </string>
+</resources> \ No newline at end of file
diff --git a/samples/ControllerSample/res/values/styles.xml b/samples/ControllerSample/res/values/styles.xml
new file mode 100644
index 000000000..4a10ca492
--- /dev/null
+++ b/samples/ControllerSample/res/values/styles.xml
@@ -0,0 +1,20 @@
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/samples/ControllerSample/src/com/example/controllersample/GameView.java b/samples/ControllerSample/src/com/example/controllersample/GameView.java
new file mode 100644
index 000000000..6481a2a39
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/controllersample/GameView.java
@@ -0,0 +1,1159 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.controllersample;
+
+import com.example.inputmanagercompat.InputManagerCompat;
+import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.os.Build;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+/*
+ * A trivial joystick based physics game to demonstrate joystick handling. If
+ * the game controller has a vibrator, then it is used to provide feedback when
+ * a bullet is fired or the ship crashes into an obstacle. Otherwise, the system
+ * vibrator is used for that purpose.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class GameView extends View implements InputDeviceListener {
+ private static final int MAX_OBSTACLES = 12;
+
+ private static final int DPAD_STATE_LEFT = 1 << 0;
+ private static final int DPAD_STATE_RIGHT = 1 << 1;
+ private static final int DPAD_STATE_UP = 1 << 2;
+ private static final int DPAD_STATE_DOWN = 1 << 3;
+
+ private final Random mRandom;
+ /*
+ * Each ship is created as an event comes in from a new Joystick device
+ */
+ private final SparseArray<Ship> mShips;
+ private final Map<String, Integer> mDescriptorMap;
+ private final List<Bullet> mBullets;
+ private final List<Obstacle> mObstacles;
+
+ private long mLastStepTime;
+ private final InputManagerCompat mInputManager;
+
+ private final float mBaseSpeed;
+
+ private final float mShipSize;
+
+ private final float mBulletSize;
+
+ private final float mMinObstacleSize;
+ private final float mMaxObstacleSize;
+ private final float mMinObstacleSpeed;
+ private final float mMaxObstacleSpeed;
+
+ public GameView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mRandom = new Random();
+ mShips = new SparseArray<Ship>();
+ mDescriptorMap = new HashMap<String, Integer>();
+ mBullets = new ArrayList<Bullet>();
+ mObstacles = new ArrayList<Obstacle>();
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
+ mBaseSpeed = baseSize * 3;
+
+ mShipSize = baseSize * 3;
+
+ mBulletSize = baseSize;
+
+ mMinObstacleSize = baseSize * 2;
+ mMaxObstacleSize = baseSize * 12;
+ mMinObstacleSpeed = mBaseSpeed;
+ mMaxObstacleSpeed = mBaseSpeed * 3;
+
+ mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext());
+ mInputManager.registerInputDeviceListener(this, null);
+ }
+
+ // Iterate through the input devices, looking for controllers. Create a ship
+ // for every device that reports itself as a gamepad or joystick.
+ void findControllersAndAttachShips() {
+ int[] deviceIds = mInputManager.getInputDeviceIds();
+ for (int deviceId : deviceIds) {
+ InputDevice dev = mInputManager.getInputDevice(deviceId);
+ int sources = dev.getSources();
+ // if the device is a gamepad/joystick, create a ship to represent it
+ if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+ ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
+ // if the device has a gamepad or joystick
+ getShipForId(deviceId);
+ }
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ int deviceId = event.getDeviceId();
+ if (deviceId != -1) {
+ Ship currentShip = getShipForId(deviceId);
+ if (currentShip.onKeyDown(keyCode, event)) {
+ step(event.getEventTime());
+ return true;
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ int deviceId = event.getDeviceId();
+ if (deviceId != -1) {
+ Ship currentShip = getShipForId(deviceId);
+ if (currentShip.onKeyUp(keyCode, event)) {
+ step(event.getEventTime());
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ mInputManager.onGenericMotionEvent(event);
+
+ // Check that the event came from a joystick or gamepad since a generic
+ // motion event could be almost anything. API level 18 adds the useful
+ // event.isFromSource() helper function.
+ int eventSource = event.getSource();
+ if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+ ((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK))
+ && event.getAction() == MotionEvent.ACTION_MOVE) {
+ int id = event.getDeviceId();
+ if (-1 != id) {
+ Ship curShip = getShipForId(id);
+ if (curShip.onGenericMotionEvent(event)) {
+ return true;
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ // Turn on and off animations based on the window focus.
+ // Alternately, we could update the game state using the Activity
+ // onResume()
+ // and onPause() lifecycle events.
+ if (hasWindowFocus) {
+ mLastStepTime = SystemClock.uptimeMillis();
+ mInputManager.onResume();
+ } else {
+ int numShips = mShips.size();
+ for (int i = 0; i < numShips; i++) {
+ Ship currentShip = mShips.valueAt(i);
+ if (currentShip != null) {
+ currentShip.setHeading(0, 0);
+ currentShip.setVelocity(0, 0);
+ currentShip.mDPadState = 0;
+ }
+ }
+ mInputManager.onPause();
+ }
+
+ super.onWindowFocusChanged(hasWindowFocus);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Reset the game when the view changes size.
+ reset();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ // Update the animation
+ animateFrame();
+
+ // Draw the ships.
+ int numShips = mShips.size();
+ for (int i = 0; i < numShips; i++) {
+ Ship currentShip = mShips.valueAt(i);
+ if (currentShip != null) {
+ currentShip.draw(canvas);
+ }
+ }
+
+ // Draw bullets.
+ int numBullets = mBullets.size();
+ for (int i = 0; i < numBullets; i++) {
+ final Bullet bullet = mBullets.get(i);
+ bullet.draw(canvas);
+ }
+
+ // Draw obstacles.
+ int numObstacles = mObstacles.size();
+ for (int i = 0; i < numObstacles; i++) {
+ final Obstacle obstacle = mObstacles.get(i);
+ obstacle.draw(canvas);
+ }
+ }
+
+ /**
+ * Uses the device descriptor to try to assign the same color to the same
+ * joystick. If there are two joysticks of the same type connected over USB,
+ * or the API is < API level 16, it will be unable to distinguish the two
+ * devices.
+ *
+ * @param shipID
+ * @return
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private Ship getShipForId(int shipID) {
+ Ship currentShip = mShips.get(shipID);
+ if (null == currentShip) {
+
+ // do we know something about this ship already?
+ InputDevice dev = InputDevice.getDevice(shipID);
+ String deviceString = null;
+ Integer shipColor = null;
+ if (null != dev) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ deviceString = dev.getDescriptor();
+ } else {
+ deviceString = dev.getName();
+ }
+ shipColor = mDescriptorMap.get(deviceString);
+ }
+
+ if (null != shipColor) {
+ int color = shipColor;
+ int numShips = mShips.size();
+ // do we already have a ship with this color?
+ for (int i = 0; i < numShips; i++) {
+ if (mShips.valueAt(i).getColor() == color) {
+ shipColor = null;
+ // we won't store this value either --- if the first
+ // controller gets disconnected/connected, it will get
+ // the same color.
+ deviceString = null;
+ }
+ }
+ }
+ if (null != shipColor) {
+ currentShip = new Ship(shipColor);
+ if (null != deviceString) {
+ mDescriptorMap.remove(deviceString);
+ }
+ } else {
+ currentShip = new Ship(getNextShipColor());
+ }
+ mShips.append(shipID, currentShip);
+ currentShip.setInputDevice(dev);
+
+ if (null != deviceString) {
+ mDescriptorMap.put(deviceString, currentShip.getColor());
+ }
+ }
+ return currentShip;
+ }
+
+ /**
+ * Remove the ship from the array of active ships by ID.
+ *
+ * @param shipID
+ */
+ private void removeShipForID(int shipID) {
+ mShips.remove(shipID);
+ }
+
+ private void reset() {
+ mShips.clear();
+ mBullets.clear();
+ mObstacles.clear();
+ findControllersAndAttachShips();
+ }
+
+ private void animateFrame() {
+ long currentStepTime = SystemClock.uptimeMillis();
+ step(currentStepTime);
+ invalidate();
+ }
+
+ private void step(long currentStepTime) {
+ float tau = (currentStepTime - mLastStepTime) * 0.001f;
+ mLastStepTime = currentStepTime;
+
+ // Move the ships
+ int numShips = mShips.size();
+ for (int i = 0; i < numShips; i++) {
+ Ship currentShip = mShips.valueAt(i);
+ if (currentShip != null) {
+ currentShip.accelerate(tau);
+ if (!currentShip.step(tau)) {
+ currentShip.reincarnate();
+ }
+ }
+ }
+
+ // Move the bullets.
+ int numBullets = mBullets.size();
+ for (int i = 0; i < numBullets; i++) {
+ final Bullet bullet = mBullets.get(i);
+ if (!bullet.step(tau)) {
+ mBullets.remove(i);
+ i -= 1;
+ numBullets -= 1;
+ }
+ }
+
+ // Move obstacles.
+ int numObstacles = mObstacles.size();
+ for (int i = 0; i < numObstacles; i++) {
+ final Obstacle obstacle = mObstacles.get(i);
+ if (!obstacle.step(tau)) {
+ mObstacles.remove(i);
+ i -= 1;
+ numObstacles -= 1;
+ }
+ }
+
+ // Check for collisions between bullets and obstacles.
+ for (int i = 0; i < numBullets; i++) {
+ final Bullet bullet = mBullets.get(i);
+ for (int j = 0; j < numObstacles; j++) {
+ final Obstacle obstacle = mObstacles.get(j);
+ if (bullet.collidesWith(obstacle)) {
+ bullet.destroy();
+ obstacle.destroy();
+ break;
+ }
+ }
+ }
+
+ // Check for collisions between the ship and obstacles --- this could
+ // get slow
+ for (int i = 0; i < numObstacles; i++) {
+ final Obstacle obstacle = mObstacles.get(i);
+ for (int j = 0; j < numShips; j++) {
+ Ship currentShip = mShips.valueAt(j);
+ if (currentShip != null) {
+ if (currentShip.collidesWith(obstacle)) {
+ currentShip.destroy();
+ obstacle.destroy();
+ break;
+ }
+ }
+ }
+ }
+
+ // Spawn more obstacles offscreen when needed.
+ // Avoid putting them right on top of the ship.
+ int tries = MAX_OBSTACLES - mObstacles.size() + 10;
+ final float minDistance = mShipSize * 4;
+ while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) {
+ float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
+ + mMinObstacleSize;
+ float positionX, positionY;
+ int edge = mRandom.nextInt(4);
+ switch (edge) {
+ case 0:
+ positionX = -size;
+ positionY = mRandom.nextInt(getHeight());
+ break;
+ case 1:
+ positionX = getWidth() + size;
+ positionY = mRandom.nextInt(getHeight());
+ break;
+ case 2:
+ positionX = mRandom.nextInt(getWidth());
+ positionY = -size;
+ break;
+ default:
+ positionX = mRandom.nextInt(getWidth());
+ positionY = getHeight() + size;
+ break;
+ }
+ boolean positionSafe = true;
+
+ // If the obstacle is too close to any ships, we don't want to
+ // spawn it.
+ for (int i = 0; i < numShips; i++) {
+ Ship currentShip = mShips.valueAt(i);
+ if (currentShip != null) {
+ if (currentShip.distanceTo(positionX, positionY) < minDistance) {
+ // try to spawn again
+ positionSafe = false;
+ break;
+ }
+ }
+ }
+
+ // if the position is safe, add the obstacle and reset the retry
+ // counter
+ if (positionSafe) {
+ tries = MAX_OBSTACLES - mObstacles.size() + 10;
+ // we can add the obstacle now since it isn't close to any ships
+ float direction = mRandom.nextFloat() * (float) Math.PI * 2;
+ float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed)
+ + mMinObstacleSpeed;
+ float velocityX = (float) Math.cos(direction) * speed;
+ float velocityY = (float) Math.sin(direction) * speed;
+
+ Obstacle obstacle = new Obstacle();
+ obstacle.setPosition(positionX, positionY);
+ obstacle.setSize(size);
+ obstacle.setVelocity(velocityX, velocityY);
+ mObstacles.add(obstacle);
+ }
+ }
+ }
+
+ private static float pythag(float x, float y) {
+ return (float) Math.sqrt(x * x + y * y);
+ }
+
+ private static int blend(float alpha, int from, int to) {
+ return from + (int) ((to - from) * alpha);
+ }
+
+ private static void setPaintARGBBlend(Paint paint, float alpha,
+ int a1, int r1, int g1, int b1,
+ int a2, int r2, int g2, int b2) {
+ paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2),
+ blend(alpha, g1, g2), blend(alpha, b1, b2));
+ }
+
+ private static float getCenteredAxis(MotionEvent event, InputDevice device,
+ int axis, int historyPos) {
+ final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
+ if (range != null) {
+ final float flat = range.getFlat();
+ final float value = historyPos < 0 ? event.getAxisValue(axis)
+ : event.getHistoricalAxisValue(axis, historyPos);
+
+ // Ignore axis values that are within the 'flat' region of the
+ // joystick axis center.
+ // A joystick at rest does not always report an absolute position of
+ // (0,0).
+ if (Math.abs(value) > flat) {
+ return value;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire
+ * key.
+ *
+ * @param keyCode
+ * @return true of it's a fire key.
+ */
+ private static boolean isFireKey(int keyCode) {
+ return KeyEvent.isGamepadButton(keyCode)
+ || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
+ || keyCode == KeyEvent.KEYCODE_SPACE;
+ }
+
+ private abstract class Sprite {
+ protected float mPositionX;
+ protected float mPositionY;
+ protected float mVelocityX;
+ protected float mVelocityY;
+ protected float mSize;
+ protected boolean mDestroyed;
+ protected float mDestroyAnimProgress;
+
+ public void setPosition(float x, float y) {
+ mPositionX = x;
+ mPositionY = y;
+ }
+
+ public void setVelocity(float x, float y) {
+ mVelocityX = x;
+ mVelocityY = y;
+ }
+
+ public void setSize(float size) {
+ mSize = size;
+ }
+
+ public float distanceTo(float x, float y) {
+ return pythag(mPositionX - x, mPositionY - y);
+ }
+
+ public float distanceTo(Sprite other) {
+ return distanceTo(other.mPositionX, other.mPositionY);
+ }
+
+ public boolean collidesWith(Sprite other) {
+ // Really bad collision detection.
+ return !mDestroyed && !other.mDestroyed
+ && distanceTo(other) <= Math.max(mSize, other.mSize)
+ + Math.min(mSize, other.mSize) * 0.5f;
+ }
+
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * Moves the sprite based on the elapsed time defined by tau.
+ *
+ * @param tau the elapsed time in seconds since the last step
+ * @return false if the sprite is to be removed from the display
+ */
+ public boolean step(float tau) {
+ mPositionX += mVelocityX * tau;
+ mPositionY += mVelocityY * tau;
+
+ if (mDestroyed) {
+ mDestroyAnimProgress += tau / getDestroyAnimDuration();
+ if (mDestroyAnimProgress >= getDestroyAnimCycles()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Draws the sprite.
+ *
+ * @param canvas the Canvas upon which to draw the sprite.
+ */
+ public abstract void draw(Canvas canvas);
+
+ /**
+ * Returns the duration of the destruction animation of the sprite in
+ * seconds.
+ *
+ * @return the float duration in seconds of the destruction animation
+ */
+ public abstract float getDestroyAnimDuration();
+
+ /**
+ * Returns the number of cycles to play the destruction animation. A
+ * destruction animation has a duration and a number of cycles to play
+ * it for, so we can have an extended death sequence when a ship or
+ * object is destroyed.
+ *
+ * @return the float number of cycles to play the destruction animation
+ */
+ public abstract float getDestroyAnimCycles();
+
+ protected boolean isOutsidePlayfield() {
+ final int width = GameView.this.getWidth();
+ final int height = GameView.this.getHeight();
+ return mPositionX < 0 || mPositionX >= width
+ || mPositionY < 0 || mPositionY >= height;
+ }
+
+ protected void wrapAtPlayfieldBoundary() {
+ final int width = GameView.this.getWidth();
+ final int height = GameView.this.getHeight();
+ while (mPositionX <= -mSize) {
+ mPositionX += width + mSize * 2;
+ }
+ while (mPositionX >= width + mSize) {
+ mPositionX -= width + mSize * 2;
+ }
+ while (mPositionY <= -mSize) {
+ mPositionY += height + mSize * 2;
+ }
+ while (mPositionY >= height + mSize) {
+ mPositionY -= height + mSize * 2;
+ }
+ }
+
+ public void destroy() {
+ mDestroyed = true;
+ step(0);
+ }
+ }
+
+ private static int sShipColor = 0;
+
+ /**
+ * Returns the next ship color in the sequence. Very simple. Does not in any
+ * way guarantee that there are not multiple ships with the same color on
+ * the screen.
+ *
+ * @return an int containing the index of the next ship color
+ */
+ private static int getNextShipColor() {
+ int color = sShipColor & 0x07;
+ if (0 == color) {
+ color++;
+ sShipColor++;
+ }
+ sShipColor++;
+ return color;
+ }
+
+ /*
+ * Static constants associated with Ship inner class
+ */
+ private static final long[] sDestructionVibratePattern = new long[] {
+ 0, 20, 20, 40, 40, 80, 40, 300
+ };
+
+ private class Ship extends Sprite {
+ private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3;
+ private static final float TO_DEGREES = (float) (180.0 / Math.PI);
+
+ private final float mMaxShipThrust = mBaseSpeed * 0.25f;
+ private final float mMaxSpeed = mBaseSpeed * 12;
+
+ // The ship actually determines the speed of the bullet, not the bullet
+ // itself
+ private final float mBulletSpeed = mBaseSpeed * 12;
+
+ private final Paint mPaint;
+ private final Path mPath;
+ private final int mR, mG, mB;
+ private final int mColor;
+
+ // The current device that is controlling the ship
+ private InputDevice mInputDevice;
+
+ private float mHeadingX;
+ private float mHeadingY;
+ private float mHeadingAngle;
+ private float mHeadingMagnitude;
+
+ private int mDPadState;
+
+ /**
+ * The colorIndex is used to create the color based on the lower three
+ * bits of the value in the current implementation.
+ *
+ * @param colorIndex
+ */
+ public Ship(int colorIndex) {
+ mPaint = new Paint();
+ mPaint.setStyle(Style.FILL);
+
+ setPosition(getWidth() * 0.5f, getHeight() * 0.5f);
+ setVelocity(0, 0);
+ setSize(mShipSize);
+
+ mPath = new Path();
+ mPath.moveTo(0, 0);
+ mPath.lineTo((float) Math.cos(-CORNER_ANGLE) * mSize,
+ (float) Math.sin(-CORNER_ANGLE) * mSize);
+ mPath.lineTo(mSize, 0);
+ mPath.lineTo((float) Math.cos(CORNER_ANGLE) * mSize,
+ (float) Math.sin(CORNER_ANGLE) * mSize);
+ mPath.lineTo(0, 0);
+
+ mR = (colorIndex & 0x01) == 0 ? 63 : 255;
+ mG = (colorIndex & 0x02) == 0 ? 63 : 255;
+ mB = (colorIndex & 0x04) == 0 ? 63 : 255;
+
+ mColor = colorIndex;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+
+ // Handle keys going up.
+ boolean handled = false;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ setHeadingX(0);
+ mDPadState &= ~DPAD_STATE_LEFT;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ setHeadingX(0);
+ mDPadState &= ~DPAD_STATE_RIGHT;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ setHeadingY(0);
+ mDPadState &= ~DPAD_STATE_UP;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ setHeadingY(0);
+ mDPadState &= ~DPAD_STATE_DOWN;
+ handled = true;
+ break;
+ default:
+ if (isFireKey(keyCode)) {
+ handled = true;
+ }
+ break;
+ }
+ return handled;
+ }
+
+ /*
+ * Firing is a unique case where a ship creates a bullet. A bullet needs
+ * to be created with a position near the ship that is firing with a
+ * velocity that is based upon the speed of the ship.
+ */
+ private void fire() {
+ if (!isDestroyed()) {
+ Bullet bullet = new Bullet();
+ bullet.setPosition(getBulletInitialX(), getBulletInitialY());
+ bullet.setVelocity(getBulletVelocityX(),
+ getBulletVelocityY());
+ mBullets.add(bullet);
+ vibrateController(20);
+ }
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Handle DPad keys and fire button on initial down but not on
+ // auto-repeat.
+ boolean handled = false;
+ if (event.getRepeatCount() == 0) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ setHeadingX(-1);
+ mDPadState |= DPAD_STATE_LEFT;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ setHeadingX(1);
+ mDPadState |= DPAD_STATE_RIGHT;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ setHeadingY(-1);
+ mDPadState |= DPAD_STATE_UP;
+ handled = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ setHeadingY(1);
+ mDPadState |= DPAD_STATE_DOWN;
+ handled = true;
+ break;
+ default:
+ if (isFireKey(keyCode)) {
+ fire();
+ handled = true;
+ }
+ break;
+ }
+ }
+ return handled;
+ }
+
+ /**
+ * Gets the vibrator from the controller if it is present. Note that it
+ * would be easy to get the system vibrator here if the controller one
+ * is not present, but we don't choose to do it in this case.
+ *
+ * @return the Vibrator for the controller, or null if it is not
+ * present. or the API level cannot support it
+ */
+ @SuppressLint("NewApi")
+ private final Vibrator getVibrator() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
+ null != mInputDevice) {
+ return mInputDevice.getVibrator();
+ }
+ return null;
+ }
+
+ private void vibrateController(int time) {
+ Vibrator vibrator = getVibrator();
+ if (null != vibrator) {
+ vibrator.vibrate(time);
+ }
+ }
+
+ private void vibrateController(long[] pattern, int repeat) {
+ Vibrator vibrator = getVibrator();
+ if (null != vibrator) {
+ vibrator.vibrate(pattern, repeat);
+ }
+ }
+
+ /**
+ * The ship directly handles joystick input.
+ *
+ * @param event
+ * @param historyPos
+ */
+ private void processJoystickInput(MotionEvent event, int historyPos) {
+ // Get joystick position.
+ // Many game pads with two joysticks report the position of the
+ // second
+ // joystick
+ // using the Z and RZ axes so we also handle those.
+ // In a real game, we would allow the user to configure the axes
+ // manually.
+ if (null == mInputDevice) {
+ mInputDevice = event.getDevice();
+ }
+ float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos);
+ if (x == 0) {
+ x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
+ }
+ if (x == 0) {
+ x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos);
+ }
+
+ float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos);
+ if (y == 0) {
+ y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
+ }
+ if (y == 0) {
+ y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos);
+ }
+
+ // Set the ship heading.
+ setHeading(x, y);
+ GameView.this.step(historyPos < 0 ? event.getEventTime() : event
+ .getHistoricalEventTime(historyPos));
+ }
+
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (0 == mDPadState) {
+ // Process all historical movement samples in the batch.
+ final int historySize = event.getHistorySize();
+ for (int i = 0; i < historySize; i++) {
+ processJoystickInput(event, i);
+ }
+
+ // Process the current movement sample in the batch.
+ processJoystickInput(event, -1);
+ }
+ return true;
+ }
+
+ /**
+ * Set the game controller to be used to control the ship.
+ *
+ * @param dev the input device that will be controlling the ship
+ */
+ public void setInputDevice(InputDevice dev) {
+ mInputDevice = dev;
+ }
+
+ /**
+ * Sets the X component of the joystick heading value, defined by the
+ * platform as being from -1.0 (left) to 1.0 (right). This function is
+ * generally used to change the heading in response to a button-style
+ * DPAD event.
+ *
+ * @param x the float x component of the joystick heading value
+ */
+ public void setHeadingX(float x) {
+ mHeadingX = x;
+ updateHeading();
+ }
+
+ /**
+ * Sets the Y component of the joystick heading value, defined by the
+ * platform as being from -1.0 (top) to 1.0 (bottom). This function is
+ * generally used to change the heading in response to a button-style
+ * DPAD event.
+ *
+ * @param y the float y component of the joystick heading value
+ */
+ public void setHeadingY(float y) {
+ mHeadingY = y;
+ updateHeading();
+ }
+
+ /**
+ * Sets the heading as floating point values returned by a joystick.
+ * These values are normalized by the Android platform to be from -1.0
+ * (left, top) to 1.0 (right, bottom)
+ *
+ * @param x the float x component of the joystick heading value
+ * @param y the float y component of the joystick heading value
+ */
+ public void setHeading(float x, float y) {
+ mHeadingX = x;
+ mHeadingY = y;
+ updateHeading();
+ }
+
+ /**
+ * Converts the heading values from joystick devices to the polar
+ * representation of the heading angle if the magnitude of the heading
+ * is significant (> 0.1f).
+ */
+ private void updateHeading() {
+ mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
+ if (mHeadingMagnitude > 0.1f) {
+ mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
+ }
+ }
+
+ /**
+ * Bring our ship back to life, stopping the destroy animation.
+ */
+ public void reincarnate() {
+ mDestroyed = false;
+ mDestroyAnimProgress = 0.0f;
+ }
+
+ private float polarX(float radius) {
+ return (float) Math.cos(mHeadingAngle) * radius;
+ }
+
+ private float polarY(float radius) {
+ return (float) Math.sin(mHeadingAngle) * radius;
+ }
+
+ /**
+ * Gets the initial x coordinate for the bullet.
+ *
+ * @return the x coordinate of the bullet adjusted for the position and
+ * direction of the ship
+ */
+ public float getBulletInitialX() {
+ return mPositionX + polarX(mSize);
+ }
+
+ /**
+ * Gets the initial y coordinate for the bullet.
+ *
+ * @return the y coordinate of the bullet adjusted for the position and
+ * direction of the ship
+ */
+ public float getBulletInitialY() {
+ return mPositionY + polarY(mSize);
+ }
+
+ /**
+ * Returns the bullet speed Y component.
+ *
+ * @return adjusted Y component bullet speed for the velocity and
+ * direction of the ship
+ */
+ public float getBulletVelocityY() {
+ return mVelocityY + polarY(mBulletSpeed);
+ }
+
+ /**
+ * Returns the bullet speed X component
+ *
+ * @return adjusted X component bullet speed for the velocity and
+ * direction of the ship
+ */
+ public float getBulletVelocityX() {
+ return mVelocityX + polarX(mBulletSpeed);
+ }
+
+ /**
+ * Uses the heading magnitude and direction to change the acceleration
+ * of the ship. In theory, this should be scaled according to the
+ * elapsed time.
+ *
+ * @param tau the elapsed time in seconds between the last step
+ */
+ public void accelerate(float tau) {
+ final float thrust = mHeadingMagnitude * mMaxShipThrust;
+ mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4;
+ mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4;
+
+ final float speed = pythag(mVelocityX, mVelocityY);
+ if (speed > mMaxSpeed) {
+ final float scale = mMaxSpeed / speed;
+ mVelocityX = mVelocityX * scale * scale;
+ mVelocityY = mVelocityY * scale * scale;
+ }
+ }
+
+ @Override
+ public boolean step(float tau) {
+ if (!super.step(tau)) {
+ return false;
+ }
+ wrapAtPlayfieldBoundary();
+ return true;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress),
+ 255, mR, mG, mB,
+ 0, 255, 0, 0);
+
+ canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.translate(mPositionX, mPositionY);
+ canvas.rotate(mHeadingAngle * TO_DEGREES);
+ canvas.drawPath(mPath, mPaint);
+ canvas.restore();
+ }
+
+ @Override
+ public float getDestroyAnimDuration() {
+ return 1.0f;
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ vibrateController(sDestructionVibratePattern, -1);
+ }
+
+ @Override
+ public float getDestroyAnimCycles() {
+ return 5.0f;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+ }
+
+ private static final Paint mBulletPaint;
+ static {
+ mBulletPaint = new Paint();
+ mBulletPaint.setStyle(Style.FILL);
+ }
+
+ private class Bullet extends Sprite {
+
+ public Bullet() {
+ setSize(mBulletSize);
+ }
+
+ @Override
+ public boolean step(float tau) {
+ if (!super.step(tau)) {
+ return false;
+ }
+ return !isOutsidePlayfield();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress,
+ 255, 255, 255, 0,
+ 0, 255, 255, 255);
+ canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint);
+ }
+
+ @Override
+ public float getDestroyAnimDuration() {
+ return 0.125f;
+ }
+
+ @Override
+ public float getDestroyAnimCycles() {
+ return 1.0f;
+ }
+
+ }
+
+ private static final Paint mObstaclePaint;
+ static {
+ mObstaclePaint = new Paint();
+ mObstaclePaint.setARGB(255, 127, 127, 255);
+ mObstaclePaint.setStyle(Style.FILL);
+ }
+
+ private class Obstacle extends Sprite {
+
+ @Override
+ public boolean step(float tau) {
+ if (!super.step(tau)) {
+ return false;
+ }
+ wrapAtPlayfieldBoundary();
+ return true;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress,
+ 255, 127, 127, 255,
+ 0, 255, 0, 0);
+ canvas.drawCircle(mPositionX, mPositionY,
+ mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint);
+ }
+
+ @Override
+ public float getDestroyAnimDuration() {
+ return 0.25f;
+ }
+
+ @Override
+ public float getDestroyAnimCycles() {
+ return 1.0f;
+ }
+ }
+
+ /*
+ * When an input device is added, we add a ship based upon the device.
+ * @see
+ * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+ * #onInputDeviceAdded(int)
+ */
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ getShipForId(deviceId);
+ }
+
+ /*
+ * This is an unusual case. Input devices don't typically change, but they
+ * certainly can --- for example a device may have different modes. We use
+ * this to make sure that the ship has an up-to-date InputDevice.
+ * @see
+ * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+ * #onInputDeviceChanged(int)
+ */
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ Ship ship = getShipForId(deviceId);
+ ship.setInputDevice(InputDevice.getDevice(deviceId));
+ }
+
+ /*
+ * Remove any ship associated with the ID.
+ * @see
+ * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+ * #onInputDeviceRemoved(int)
+ */
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ removeShipForID(deviceId);
+ }
+}
diff --git a/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java b/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java
new file mode 100644
index 000000000..aaf8baee6
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.controllersample;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class GameViewActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.setContentView(R.layout.game_controller_input);
+ }
+
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java
new file mode 100644
index 000000000..fabc87614
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+public interface InputManagerCompat {
+ /**
+ * Gets information about the input device with the specified id.
+ *
+ * @param id The device id
+ * @return The input device or null if not found
+ */
+ public InputDevice getInputDevice(int id);
+
+ /**
+ * Gets the ids of all input devices in the system.
+ *
+ * @return The input device ids.
+ */
+ public int[] getInputDeviceIds();
+
+ /**
+ * Registers an input device listener to receive notifications about when
+ * input devices are added, removed or changed.
+ *
+ * @param listener The listener to register.
+ * @param handler The handler on which the listener should be invoked, or
+ * null if the listener should be invoked on the calling thread's
+ * looper.
+ */
+ public void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener,
+ Handler handler);
+
+ /**
+ * Unregisters an input device listener.
+ *
+ * @param listener The listener to unregister.
+ */
+ public void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener);
+
+ /*
+ * The following three calls are to simulate V16 behavior on pre-Jellybean
+ * devices. If you don't call them, your callback will never be called
+ * pre-API 16.
+ */
+
+ /**
+ * Pass the motion events to the InputManagerCompat. This is used to
+ * optimize for polling for controllers. If you do not pass these events in,
+ * polling will cause regular object creation.
+ *
+ * @param event the motion event from the app
+ */
+ public void onGenericMotionEvent(MotionEvent event);
+
+ /**
+ * Tell the V9 input manager that it should stop polling for disconnected
+ * devices. You can call this during onPause in your activity, although you
+ * might want to call it whenever your game is not active (or whenever you
+ * don't care about being notified of new input devices)
+ */
+ public void onPause();
+
+ /**
+ * Tell the V9 input manager that it should start polling for disconnected
+ * devices. You can call this during onResume in your activity, although you
+ * might want to call it less often (only when the gameplay is actually
+ * active)
+ */
+ public void onResume();
+
+ public interface InputDeviceListener {
+ /**
+ * Called whenever the input manager detects that a device has been
+ * added. This will only be called in the V9 version when a motion event
+ * is detected.
+ *
+ * @param deviceId The id of the input device that was added.
+ */
+ void onInputDeviceAdded(int deviceId);
+
+ /**
+ * Called whenever the properties of an input device have changed since
+ * they were last queried. This will not be called for the V9 version of
+ * the API.
+ *
+ * @param deviceId The id of the input device that changed.
+ */
+ void onInputDeviceChanged(int deviceId);
+
+ /**
+ * Called whenever the input manager detects that a device has been
+ * removed. For the V9 version, this can take some time depending on the
+ * poll rate.
+ *
+ * @param deviceId The id of the input device that was removed.
+ */
+ void onInputDeviceRemoved(int deviceId);
+ }
+
+ /**
+ * Use this to construct a compatible InputManager.
+ */
+ public static class Factory {
+
+ /**
+ * Constructs and returns a compatible InputManger
+ *
+ * @param context the Context that will be used to get the system
+ * service from
+ * @return a compatible implementation of InputManager
+ */
+ public static InputManagerCompat getInputManager(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return new InputManagerV16(context);
+ } else {
+ return new InputManagerV9();
+ }
+ }
+ }
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java
new file mode 100644
index 000000000..d26581e6c
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Build;
+import android.os.Handler;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class InputManagerV16 implements InputManagerCompat {
+
+ private final InputManager mInputManager;
+ private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners;
+
+ public InputManagerV16(Context context) {
+ mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+ mListeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>();
+ }
+
+ @Override
+ public InputDevice getInputDevice(int id) {
+ return mInputManager.getInputDevice(id);
+ }
+
+ @Override
+ public int[] getInputDeviceIds() {
+ return mInputManager.getInputDeviceIds();
+ }
+
+ static class V16InputDeviceListener implements InputManager.InputDeviceListener {
+ final InputManagerCompat.InputDeviceListener mIDL;
+
+ public V16InputDeviceListener(InputDeviceListener idl) {
+ mIDL = idl;
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ mIDL.onInputDeviceAdded(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ mIDL.onInputDeviceChanged(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ mIDL.onInputDeviceRemoved(deviceId);
+ }
+
+ }
+
+ @Override
+ public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
+ V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener);
+ mInputManager.registerInputDeviceListener(v16Listener, handler);
+ mListeners.put(listener, v16Listener);
+ }
+
+ @Override
+ public void unregisterInputDeviceListener(InputDeviceListener listener) {
+ V16InputDeviceListener curListener = mListeners.remove(listener);
+ if (null != curListener)
+ {
+ mInputManager.unregisterInputDeviceListener(curListener);
+ }
+
+ }
+
+ @Override
+ public void onGenericMotionEvent(MotionEvent event) {
+ // unused in V16
+ }
+
+ @Override
+ public void onPause() {
+ // unused in V16
+ }
+
+ @Override
+ public void onResume() {
+ // unused in V16
+ }
+
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java
new file mode 100644
index 000000000..dcd89880d
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+
+public class InputManagerV9 implements InputManagerCompat {
+ private static final String LOG_TAG = "InputManagerV9";
+ private static final int MESSAGE_TEST_FOR_DISCONNECT = 101;
+ private static final long CHECK_ELAPSED_TIME = 3000L;
+
+ private static final int ON_DEVICE_ADDED = 0;
+ private static final int ON_DEVICE_CHANGED = 1;
+ private static final int ON_DEVICE_REMOVED = 2;
+
+ private final SparseArray<long[]> mDevices;
+ private final Map<InputDeviceListener, Handler> mListeners;
+ private final Handler mDefaultHandler;
+
+ private static class PollingMessageHandler extends Handler {
+ private final WeakReference<InputManagerV9> mInputManager;
+
+ PollingMessageHandler(InputManagerV9 im) {
+ mInputManager = new WeakReference<InputManagerV9>(im);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ switch (msg.what) {
+ case MESSAGE_TEST_FOR_DISCONNECT:
+ InputManagerV9 imv = mInputManager.get();
+ if (null != imv) {
+ long time = SystemClock.elapsedRealtime();
+ int size = imv.mDevices.size();
+ for (int i = 0; i < size; i++) {
+ long[] lastContact = imv.mDevices.valueAt(i);
+ if (null != lastContact) {
+ if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
+ // check to see if the device has been
+ // disconnected
+ int id = imv.mDevices.keyAt(i);
+ if (null == InputDevice.getDevice(id)) {
+ // disconnected!
+ imv.notifyListeners(ON_DEVICE_REMOVED, id);
+ imv.mDevices.remove(id);
+ } else {
+ lastContact[0] = time;
+ }
+ }
+ }
+ }
+ sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
+ CHECK_ELAPSED_TIME);
+ }
+ break;
+ }
+ }
+
+ }
+
+ public InputManagerV9() {
+ mDevices = new SparseArray<long[]>();
+ mListeners = new HashMap<InputDeviceListener, Handler>();
+ mDefaultHandler = new PollingMessageHandler(this);
+ // as a side-effect, populates our collection of watched
+ // input devices
+ getInputDeviceIds();
+ }
+
+ @Override
+ public InputDevice getInputDevice(int id) {
+ return InputDevice.getDevice(id);
+ }
+
+ @Override
+ public int[] getInputDeviceIds() {
+ // add any hitherto unknown devices to our
+ // collection of watched input devices
+ int[] activeDevices = InputDevice.getDeviceIds();
+ long time = SystemClock.elapsedRealtime();
+ for ( int id : activeDevices ) {
+ long[] lastContact = mDevices.get(id);
+ if ( null == lastContact ) {
+ // we have a new device
+ mDevices.put(id, new long[] { time });
+ }
+ }
+ return activeDevices;
+ }
+
+ @Override
+ public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
+ mListeners.remove(listener);
+ if (handler == null) {
+ handler = mDefaultHandler;
+ }
+ mListeners.put(listener, handler);
+ }
+
+ @Override
+ public void unregisterInputDeviceListener(InputDeviceListener listener) {
+ mListeners.remove(listener);
+ }
+
+ private void notifyListeners(int why, int deviceId) {
+ // the state of some device has changed
+ if (!mListeners.isEmpty()) {
+ // yes... this will cause an object to get created... hopefully
+ // it won't happen very often
+ for (InputDeviceListener listener : mListeners.keySet()) {
+ Handler handler = mListeners.get(listener);
+ DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener);
+ handler.post(odc);
+ }
+ }
+ }
+
+ private static class DeviceEvent implements Runnable {
+ private int mMessageType;
+ private int mId;
+ private InputDeviceListener mListener;
+ private static Queue<DeviceEvent> sEventQueue = new ArrayDeque<DeviceEvent>();
+
+ private DeviceEvent() {
+ }
+
+ static DeviceEvent getDeviceEvent(int messageType, int id,
+ InputDeviceListener listener) {
+ DeviceEvent curChanged = sEventQueue.poll();
+ if (null == curChanged) {
+ curChanged = new DeviceEvent();
+ }
+ curChanged.mMessageType = messageType;
+ curChanged.mId = id;
+ curChanged.mListener = listener;
+ return curChanged;
+ }
+
+ @Override
+ public void run() {
+ switch (mMessageType) {
+ case ON_DEVICE_ADDED:
+ mListener.onInputDeviceAdded(mId);
+ break;
+ case ON_DEVICE_CHANGED:
+ mListener.onInputDeviceChanged(mId);
+ break;
+ case ON_DEVICE_REMOVED:
+ mListener.onInputDeviceRemoved(mId);
+ break;
+ default:
+ Log.e(LOG_TAG, "Unknown Message Type");
+ break;
+ }
+ // dump this runnable back in the queue
+ sEventQueue.offer(this);
+ }
+ }
+
+ @Override
+ public void onGenericMotionEvent(MotionEvent event) {
+ // detect new devices
+ int id = event.getDeviceId();
+ long[] timeArray = mDevices.get(id);
+ if (null == timeArray) {
+ notifyListeners(ON_DEVICE_ADDED, id);
+ timeArray = new long[1];
+ mDevices.put(id, timeArray);
+ }
+ long time = SystemClock.elapsedRealtime();
+ timeArray[0] = time;
+ }
+
+ @Override
+ public void onPause() {
+ mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT);
+ }
+
+ @Override
+ public void onResume() {
+ mDefaultHandler.sendEmptyMessage(MESSAGE_TEST_FOR_DISCONNECT);
+ }
+
+}