docs: Code sample for game controller training class.

Change-Id: I14f7046c7fba4f67838513af2ca2abbca4c3561a
diff --git a/samples/ControllerSample/AndroidManifest.xml b/samples/ControllerSample/AndroidManifest.xml
new file mode 100644
index 0000000..49b67d7
--- /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 0000000..65ebaf8
--- /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 0000000..f2fe155
--- /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 0000000..ce39f2d
--- /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 0000000..4f421f9
--- /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 0000000..38651fd
--- /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 0000000..c6f6d82
--- /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 0000000..eef1c76
--- /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 0000000..4e4a735
--- /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 0000000..541752f
--- /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 0000000..f20e015
--- /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 0000000..ba8e7d7
--- /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 0000000..4a10ca4
--- /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 0000000..6481a2a
--- /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 0000000..aaf8bae
--- /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 0000000..fabc876
--- /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 0000000..d26581e
--- /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 0000000..dcd8988
--- /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);
+    }
+
+}