Add a sample to demonstrate game controller usage.

Change-Id: I86a91916a39f3a211b06016b163f8d4a6d4a5a3b
diff --git a/samples/ApiDemos/AndroidManifest.xml b/samples/ApiDemos/AndroidManifest.xml
index 657d0dc..b834c5c 100644
--- a/samples/ApiDemos/AndroidManifest.xml
+++ b/samples/ApiDemos/AndroidManifest.xml
@@ -1961,6 +1961,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".view.GameControllerInput" android:label="Views/Game Controller Input">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <!-- ************************************* -->
         <!--           GRAPHICS SAMPLES            -->
         <!-- ************************************* -->
diff --git a/samples/ApiDemos/res/layout/game_controller_input.xml b/samples/ApiDemos/res/layout/game_controller_input.xml
new file mode 100644
index 0000000..951c69b
--- /dev/null
+++ b/samples/ApiDemos/res/layout/game_controller_input.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/game_controller_input_description"
+        android:padding="12dip" />
+
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:padding="12dip">
+        <ListView
+            android:id="@+id/summary"
+            android:layout_width="0dip"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:padding="3dip">
+        </ListView>
+
+        <com.example.android.apis.view.GameView
+            android:id="@+id/game"
+            android:layout_width="0dip"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:background="#000000"
+            android:padding="3dip" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/samples/ApiDemos/res/layout/game_controller_input_heading.xml b/samples/ApiDemos/res/layout/game_controller_input_heading.xml
new file mode 100644
index 0000000..297d378
--- /dev/null
+++ b/samples/ApiDemos/res/layout/game_controller_input_heading.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:padding="6dip"
+    android:textAppearance="?android:attr/textAppearanceMedium" />
diff --git a/samples/ApiDemos/res/layout/game_controller_input_text_column.xml b/samples/ApiDemos/res/layout/game_controller_input_text_column.xml
new file mode 100644
index 0000000..991f4a7
--- /dev/null
+++ b/samples/ApiDemos/res/layout/game_controller_input_text_column.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="6dip">
+    <TextView
+        android:id="@+id/label"
+        android:gravity="left"
+        android:layout_width="0dip"
+        android:layout_height="match_parent"
+        android:layout_weight="1" />
+    <TextView
+        android:id="@+id/content"
+        android:gravity="left"
+        android:layout_width="0dip"
+        android:layout_height="match_parent"
+        android:layout_weight="1" />
+</LinearLayout>
diff --git a/samples/ApiDemos/res/values/strings.xml b/samples/ApiDemos/res/values/strings.xml
index 7fe666a..66e17b3 100644
--- a/samples/ApiDemos/res/values/strings.xml
+++ b/samples/ApiDemos/res/values/strings.xml
@@ -773,6 +773,20 @@
         dot will append the drag\'s textual conversion to the EditText.
     </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...
+    </string>
+    <string name="game_controller_input_heading_device">Input Device</string>
+    <string name="game_controller_input_heading_axes">Axes</string>
+    <string name="game_controller_input_heading_keys">Keys and Buttons</string>
+    <string name="game_controller_input_label_device_name">Name</string>
+    <string name="game_controller_input_key_pressed">Pressed</string>
+    <string name="game_controller_input_key_released">Released</string>
+
     <!-- ============================== -->
     <!--  GoogleLogin examples strings  -->
     <!-- ============================== -->
diff --git a/samples/ApiDemos/src/com/example/android/apis/view/GameControllerInput.java b/samples/ApiDemos/src/com/example/android/apis/view/GameControllerInput.java
new file mode 100644
index 0000000..8c0db32
--- /dev/null
+++ b/samples/ApiDemos/src/com/example/android/apis/view/GameControllerInput.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2011 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.android.apis.view;
+
+import com.example.android.apis.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * Demonstrates how to process input events received from game controllers.
+ *
+ * This activity displays button states and joystick positions.
+ * Also writes detailed information about relevant input events to the log.
+ *
+ * The game controller is also uses to control a very simple game.  See {@link GameView}
+ * for the game itself.
+ */
+public class GameControllerInput extends Activity {
+    private static final String TAG = "GameControllerInput";
+
+    private SparseArray<InputDeviceState> mInputDeviceStates;
+    private GameView mGame;
+    private ListView mSummaryList;
+    private SummaryAdapter mSummaryAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mInputDeviceStates = new SparseArray<InputDeviceState>();
+        mSummaryAdapter = new SummaryAdapter(this, getResources());
+
+        setContentView(R.layout.game_controller_input);
+
+        mGame = (GameView) findViewById(R.id.game);
+
+        mSummaryList = (ListView) findViewById(R.id.summary);
+        mSummaryList.setAdapter(mSummaryAdapter);
+        mSummaryList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                mSummaryAdapter.onItemClick(position);
+            }
+        });
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+
+        mGame.requestFocus();
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Update device state for visualization and logging.
+        InputDeviceState state = getInputDeviceState(event);
+        if (state != null) {
+            switch (event.getAction()) {
+                case KeyEvent.ACTION_DOWN:
+                    if (state.onKeyDown(event)) {
+                        mSummaryAdapter.show(state);
+                    }
+                    break;
+                case KeyEvent.ACTION_UP:
+                    if (state.onKeyUp(event)) {
+                        mSummaryAdapter.show(state);
+                    }
+                    break;
+            }
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent event) {
+        // Check that the event came from a joystick since a generic motion event
+        // could be almost anything.
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0
+                && event.getAction() == MotionEvent.ACTION_MOVE) {
+            // Update device state for visualization and logging.
+            InputDeviceState state = getInputDeviceState(event);
+            if (state != null && state.onJoystickMotion(event)) {
+                mSummaryAdapter.show(state);
+            }
+        }
+        return super.dispatchGenericMotionEvent(event);
+    }
+
+    private InputDeviceState getInputDeviceState(InputEvent event) {
+        final int deviceId = event.getDeviceId();
+        InputDeviceState state = mInputDeviceStates.get(deviceId);
+        if (state == null) {
+            final InputDevice device = event.getDevice();
+            if (device == null) {
+                return null;
+            }
+            state = new InputDeviceState(device);
+            mInputDeviceStates.put(deviceId, state);
+
+            Log.i(TAG, device.toString());
+        }
+        return state;
+    }
+
+    /**
+     * Tracks the state of joystick axes and game controller buttons for a particular
+     * input device for diagnostic purposes.
+     */
+    private static class InputDeviceState {
+        private final InputDevice mDevice;
+        private final int[] mAxes;
+        private final float[] mAxisValues;
+        private final SparseIntArray mKeys;
+
+        public InputDeviceState(InputDevice device) {
+            mDevice = device;
+            mAxes = device.getMotionAxes();
+            mAxisValues = new float[mAxes.length];
+            mKeys = new SparseIntArray();
+        }
+
+        public InputDevice getDevice() {
+            return mDevice;
+        }
+
+        public int getAxisCount() {
+            return mAxes.length;
+        }
+
+        public int getAxis(int axisIndex) {
+            return mAxes[axisIndex];
+        }
+
+        public float getAxisValue(int axisIndex) {
+            return mAxisValues[axisIndex];
+        }
+
+        public int getKeyCount() {
+            return mKeys.size();
+        }
+
+        public int getKeyCode(int keyIndex) {
+            return mKeys.keyAt(keyIndex);
+        }
+
+        public boolean isKeyPressed(int keyIndex) {
+            return mKeys.valueAt(keyIndex) != 0;
+        }
+
+        public boolean onKeyDown(KeyEvent event) {
+            final int keyCode = event.getKeyCode();
+            if (isGameKey(keyCode)) {
+                if (event.getRepeatCount() == 0) {
+                    final String symbolicName = KeyEvent.keyCodeToString(keyCode);
+                    mKeys.put(keyCode, 1);
+                    Log.i(TAG, mDevice.getName() + " - Key Down: " + symbolicName);
+                }
+                return true;
+            }
+            return false;
+        }
+
+        public boolean onKeyUp(KeyEvent event) {
+            final int keyCode = event.getKeyCode();
+            if (isGameKey(keyCode)) {
+                int index = mKeys.indexOfKey(keyCode);
+                if (index >= 0) {
+                    final String symbolicName = KeyEvent.keyCodeToString(keyCode);
+                    mKeys.put(keyCode, 0);
+                    Log.i(TAG, mDevice.getName() + " - Key Up: " + symbolicName);
+                }
+                return true;
+            }
+            return false;
+        }
+
+        public boolean onJoystickMotion(MotionEvent event) {
+            StringBuilder message = new StringBuilder();
+            message.append(mDevice.getName()).append(" - Joystick Motion:\n");
+
+            final int historySize = event.getHistorySize();
+            for (int i = 0; i < mAxes.length; i++) {
+                final int axis = mAxes[i];
+                final float value = event.getAxisValue(axis);
+                mAxisValues[i] = value;
+                message.append("  ").append(MotionEvent.axisToString(axis)).append(": ");
+
+                // Append all historical values in the batch.
+                for (int historyPos = 0; historyPos < historySize; historyPos++) {
+                    message.append(event.getHistoricalAxisValue(axis, historyPos));
+                    message.append(", ");
+                }
+
+                // Append the current value.
+                message.append(value);
+                message.append("\n");
+            }
+            Log.i(TAG, message.toString());
+            return true;
+        }
+
+        // Check whether this is a key we care about.
+        // In a real game, we would probably let the user configure which keys to use
+        // instead of hardcoding the keys like this.
+        private static boolean isGameKey(int keyCode) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_DPAD_UP:
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                case KeyEvent.KEYCODE_DPAD_CENTER:
+                case KeyEvent.KEYCODE_SPACE:
+                    return true;
+                default:
+                    return KeyEvent.isGamepadButton(keyCode);
+            }
+        }
+    }
+
+    /**
+     * A list adapter that displays a summary of the device state.
+     */
+    private static class SummaryAdapter extends BaseAdapter {
+        private static final int BASE_ID_HEADING = 1 << 10;
+        private static final int BASE_ID_DEVICE_ITEM = 2 << 10;
+        private static final int BASE_ID_AXIS_ITEM = 3 << 10;
+        private static final int BASE_ID_KEY_ITEM = 4 << 10;
+
+        private final Context mContext;
+        private final Resources mResources;
+
+        private final SparseArray<Item> mDataItems = new SparseArray<Item>();
+        private final ArrayList<Item> mVisibleItems = new ArrayList<Item>();
+
+        private final Heading mDeviceHeading;
+        private final TextColumn mDeviceNameTextColumn;
+
+        private final Heading mAxesHeading;
+        private final Heading mKeysHeading;
+
+        private InputDeviceState mState;
+
+        public SummaryAdapter(Context context, Resources resources) {
+            mContext = context;
+            mResources = resources;
+
+            mDeviceHeading = new Heading(BASE_ID_HEADING | 0,
+                    mResources.getString(R.string.game_controller_input_heading_device));
+            mDeviceNameTextColumn = new TextColumn(BASE_ID_DEVICE_ITEM | 0,
+                    mResources.getString(R.string.game_controller_input_label_device_name));
+
+            mAxesHeading = new Heading(BASE_ID_HEADING | 1,
+                    mResources.getString(R.string.game_controller_input_heading_axes));
+            mKeysHeading = new Heading(BASE_ID_HEADING | 2,
+                    mResources.getString(R.string.game_controller_input_heading_keys));
+        }
+
+        public void onItemClick(int position) {
+            if (mState != null) {
+                Toast toast = Toast.makeText(
+                        mContext, mState.getDevice().toString(), Toast.LENGTH_LONG);
+                toast.show();
+            }
+        }
+
+        public void show(InputDeviceState state) {
+            mState = state;
+            mVisibleItems.clear();
+
+            // Populate device information.
+            mVisibleItems.add(mDeviceHeading);
+            mDeviceNameTextColumn.setContent(state.getDevice().getName());
+            mVisibleItems.add(mDeviceNameTextColumn);
+
+            // Populate axes.
+            mVisibleItems.add(mAxesHeading);
+            final int axisCount = state.getAxisCount();
+            for (int i = 0; i < axisCount; i++) {
+                final int axis = state.getAxis(i);
+                final int id = BASE_ID_AXIS_ITEM | axis;
+                TextColumn column = (TextColumn) mDataItems.get(id);
+                if (column == null) {
+                    column = new TextColumn(id, MotionEvent.axisToString(axis));
+                    mDataItems.put(id, column);
+                }
+                column.setContent(Float.toString(state.getAxisValue(i)));
+                mVisibleItems.add(column);
+            }
+
+            // Populate keys.
+            mVisibleItems.add(mKeysHeading);
+            final int keyCount = state.getKeyCount();
+            for (int i = 0; i < keyCount; i++) {
+                final int keyCode = state.getKeyCode(i);
+                final int id = BASE_ID_KEY_ITEM | keyCode;
+                TextColumn column = (TextColumn) mDataItems.get(id);
+                if (column == null) {
+                    column = new TextColumn(id, KeyEvent.keyCodeToString(keyCode));
+                    mDataItems.put(id, column);
+                }
+                column.setContent(mResources.getString(state.isKeyPressed(i)
+                        ? R.string.game_controller_input_key_pressed
+                        : R.string.game_controller_input_key_released));
+                mVisibleItems.add(column);
+            }
+
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public int getCount() {
+            return mVisibleItems.size();
+        }
+
+        @Override
+        public Item getItem(int position) {
+            return mVisibleItems.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return getItem(position).getItemId();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return getItem(position).getView(convertView, parent);
+        }
+
+        private static abstract class Item {
+            private final int mItemId;
+            private final int mLayoutResourceId;
+            private View mView;
+
+            public Item(int itemId, int layoutResourceId) {
+                mItemId = itemId;
+                mLayoutResourceId = layoutResourceId;
+            }
+
+            public long getItemId() {
+                return mItemId;
+            }
+
+            public View getView(View convertView, ViewGroup parent) {
+                if (mView == null) {
+                    LayoutInflater inflater = (LayoutInflater)
+                            parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+                    mView = inflater.inflate(mLayoutResourceId, parent, false);
+                    initView(mView);
+                }
+                updateView(mView);
+                return mView;
+            }
+
+            protected void initView(View view) {
+            }
+
+            protected void updateView(View view) {
+            }
+        }
+
+        private static class Heading extends Item {
+            private final String mLabel;
+
+            public Heading(int itemId, String label) {
+                super(itemId, R.layout.game_controller_input_heading);
+                mLabel = label;
+            }
+
+            @Override
+            public void initView(View view) {
+                TextView textView = (TextView) view;
+                textView.setText(mLabel);
+            }
+        }
+
+        private static class TextColumn extends Item {
+            private final String mLabel;
+
+            private String mContent;
+            private TextView mContentView;
+
+            public TextColumn(int itemId, String label) {
+                super(itemId, R.layout.game_controller_input_text_column);
+                mLabel = label;
+            }
+
+            public void setContent(String content) {
+                mContent = content;
+            }
+
+            @Override
+            public void initView(View view) {
+                TextView textView = (TextView) view.findViewById(R.id.label);
+                textView.setText(mLabel);
+
+                mContentView = (TextView) view.findViewById(R.id.content);
+            }
+
+            @Override
+            public void updateView(View view) {
+                mContentView.setText(mContent);
+            }
+        }
+    }
+}
diff --git a/samples/ApiDemos/src/com/example/android/apis/view/GameView.java b/samples/ApiDemos/src/com/example/android/apis/view/GameView.java
new file mode 100644
index 0000000..2bdec34
--- /dev/null
+++ b/samples/ApiDemos/src/com/example/android/apis/view/GameView.java
@@ -0,0 +1,747 @@
+/*
+ * Copyright (C) 2011 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.android.apis.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Paint.Style;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * A trivial joystick based physics game to demonstrate joystick handling.
+ *
+ * @see GameControllerInput
+ */
+public class GameView extends View {
+    private final long ANIMATION_TIME_STEP = 1000 / 60;
+    private final int MAX_OBSTACLES = 12;
+
+    private final Random mRandom;
+    private Ship mShip;
+    private final List<Bullet> mBullets;
+    private final List<Obstacle> mObstacles;
+
+    private long mLastStepTime;
+    private InputDevice mLastInputDevice;
+
+    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 int mDPadState;
+
+    private float mShipSize;
+    private float mMaxShipThrust;
+    private float mMaxShipSpeed;
+
+    private float mBulletSize;
+    private float mBulletSpeed;
+
+    private float mMinObstacleSize;
+    private float mMaxObstacleSize;
+    private float mMinObstacleSpeed;
+    private float mMaxObstacleSpeed;
+
+    private final Runnable mAnimationRunnable = new Runnable() {
+        public void run() {
+            animate();
+        }
+    };
+
+    public GameView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mRandom = new Random();
+        mBullets = new ArrayList<Bullet>();
+        mObstacles = new ArrayList<Obstacle>();
+
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+
+        float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
+        float baseSpeed = baseSize * 3;
+
+        mShipSize = baseSize * 3;
+        mMaxShipThrust = baseSpeed * 0.25f;
+        mMaxShipSpeed = baseSpeed * 12;
+
+        mBulletSize = baseSize;
+        mBulletSpeed = baseSpeed * 12;
+
+        mMinObstacleSize = baseSize * 2;
+        mMaxObstacleSize = baseSize * 12;
+        mMinObstacleSpeed = baseSpeed;
+        mMaxObstacleSpeed = baseSpeed * 3;
+    }
+
+    @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
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        ensureInitialized();
+
+        // 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:
+                    mShip.setHeadingX(-1);
+                    mDPadState |= DPAD_STATE_LEFT;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    mShip.setHeadingX(1);
+                    mDPadState |= DPAD_STATE_RIGHT;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    mShip.setHeadingY(-1);
+                    mDPadState |= DPAD_STATE_UP;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    mShip.setHeadingY(1);
+                    mDPadState |= DPAD_STATE_DOWN;
+                    handled = true;
+                    break;
+                default:
+                    if (isFireKey(keyCode)) {
+                        fire();
+                        handled = true;
+                    }
+                    break;
+            }
+        }
+        if (handled) {
+            step(event.getEventTime());
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        ensureInitialized();
+
+        // Handle keys going up.
+        boolean handled = false;
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                mShip.setHeadingX(0);
+                mDPadState &= ~DPAD_STATE_LEFT;
+                handled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                mShip.setHeadingX(0);
+                mDPadState &= ~DPAD_STATE_RIGHT;
+                handled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                mShip.setHeadingY(0);
+                mDPadState &= ~DPAD_STATE_UP;
+                handled = true;
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                mShip.setHeadingY(0);
+                mDPadState &= ~DPAD_STATE_DOWN;
+                handled = true;
+                break;
+            default:
+                if (isFireKey(keyCode)) {
+                    handled = true;
+                }
+                break;
+        }
+        if (handled) {
+            step(event.getEventTime());
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    private static boolean isFireKey(int keyCode) {
+        return KeyEvent.isGamepadButton(keyCode)
+                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
+                || keyCode == KeyEvent.KEYCODE_SPACE;
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        ensureInitialized();
+
+        // Check that the event came from a joystick since a generic motion event
+        // could be almost anything.
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0
+                && event.getAction() == MotionEvent.ACTION_MOVE) {
+            // Cache the most recently obtained device information.
+            // The device information may change over time but it can be
+            // somewhat expensive to query.
+            if (mLastInputDevice == null || mLastInputDevice.getId() != event.getDeviceId()) {
+                mLastInputDevice = event.getDevice();
+                // It's possible for the device id to be invalid.
+                // In that case, getDevice() will return null.
+                if (mLastInputDevice == null) {
+                    return false;
+                }
+            }
+
+            // Ignore joystick while the DPad is pressed to avoid conflicting motions.
+            if (mDPadState != 0) {
+                return true;
+            }
+
+            // 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;
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    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.
+        float x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_X, historyPos);
+        if (x == 0) {
+            x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
+        }
+        if (x == 0) {
+            x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Z, historyPos);
+        }
+
+        float y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Y, historyPos);
+        if (y == 0) {
+            y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
+        }
+        if (y == 0) {
+            y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_RZ, historyPos);
+        }
+
+        // Set the ship heading.
+        mShip.setHeading(x, y);
+        step(historyPos < 0 ? event.getEventTime() : event.getHistoricalEventTime(historyPos));
+    }
+
+    private static float getCenteredAxis(MotionEvent event, InputDevice device,
+            int axis, int historyPos) {
+        final InputDevice.MotionRange range = device.getMotionRange(axis);
+        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;
+    }
+
+    @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) {
+            getHandler().postDelayed(mAnimationRunnable, ANIMATION_TIME_STEP);
+            mLastStepTime = SystemClock.uptimeMillis();
+        } else {
+            getHandler().removeCallbacks(mAnimationRunnable);
+
+            mDPadState = 0;
+            if (mShip != null) {
+                mShip.setHeading(0, 0);
+                mShip.setVelocity(0, 0);
+            }
+        }
+
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    private void fire() {
+        if (mShip != null && !mShip.isDestroyed()) {
+            Bullet bullet = new Bullet();
+            bullet.setPosition(mShip.getBulletInitialX(), mShip.getBulletInitialY());
+            bullet.setVelocity(mShip.getBulletVelocityX(mBulletSpeed),
+                    mShip.getBulletVelocityY(mBulletSpeed));
+            mBullets.add(bullet);
+        }
+    }
+
+    private void ensureInitialized() {
+        if (mShip == null) {
+            reset();
+        }
+    }
+
+    private void reset() {
+        mShip = new Ship();
+        mBullets.clear();
+        mObstacles.clear();
+    }
+
+    void animate() {
+        long currentStepTime = SystemClock.uptimeMillis();
+        step(currentStepTime);
+
+        Handler handler = getHandler();
+        if (handler != null) {
+            handler.postAtTime(mAnimationRunnable, currentStepTime + ANIMATION_TIME_STEP);
+            invalidate();
+        }
+    }
+
+    private void step(long currentStepTime) {
+        float tau = (currentStepTime - mLastStepTime) * 0.001f;
+        mLastStepTime = currentStepTime;
+
+        ensureInitialized();
+
+        // Move the ship.
+        mShip.accelerate(tau, mMaxShipThrust, mMaxShipSpeed);
+        if (!mShip.step(tau)) {
+            reset();
+        }
+
+        // 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.
+        for (int i = 0; i < numObstacles; i++) {
+            final Obstacle obstacle = mObstacles.get(i);
+            if (mShip.collidesWith(obstacle)) {
+                mShip.destroy();
+                obstacle.destroy();
+                break;
+            }
+        }
+
+        // Spawn more obstacles offscreen when needed.
+        // Avoid putting them right on top of the ship.
+        OuterLoop: while (mObstacles.size() < MAX_OBSTACLES) {
+            final float minDistance = mShipSize * 4;
+            float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
+                    + mMinObstacleSize;
+            float positionX, positionY;
+            int tries = 0;
+            do {
+                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;
+                }
+                if (++tries > 10) {
+                    break OuterLoop;
+                }
+            } while (mShip.distanceTo(positionX, positionY) < minDistance);
+
+            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);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Draw the ship.
+        if (mShip != null) {
+            mShip.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);
+        }
+    }
+
+    static float pythag(float x, float y) {
+        return (float) Math.sqrt(x * x + y * y);
+    }
+
+    static int blend(float alpha, int from, int to) {
+        return from + (int) ((to - from) * alpha);
+    }
+
+    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 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;
+        }
+
+        public boolean step(float tau) {
+            mPositionX += mVelocityX * tau;
+            mPositionY += mVelocityY * tau;
+
+            if (mDestroyed) {
+                mDestroyAnimProgress += tau / getDestroyAnimDuration();
+                if (mDestroyAnimProgress >= 1.0f) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public abstract void draw(Canvas canvas);
+
+        public abstract float getDestroyAnimDuration();
+
+        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 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 float mHeadingX;
+        private float mHeadingY;
+        private float mHeadingAngle;
+        private float mHeadingMagnitude;
+        private final Paint mPaint;
+        private final Path mPath;
+
+
+        public Ship() {
+            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);
+        }
+
+        public void setHeadingX(float x) {
+            mHeadingX = x;
+            updateHeading();
+        }
+
+        public void setHeadingY(float y) {
+            mHeadingY = y;
+            updateHeading();
+        }
+
+        public void setHeading(float x, float y) {
+            mHeadingX = x;
+            mHeadingY = y;
+            updateHeading();
+        }
+
+        private void updateHeading() {
+            mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
+            if (mHeadingMagnitude > 0.1f) {
+                mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
+            }
+        }
+
+        private float polarX(float radius) {
+            return (float) Math.cos(mHeadingAngle) * radius;
+        }
+
+        private float polarY(float radius) {
+            return (float) Math.sin(mHeadingAngle) * radius;
+        }
+
+        public float getBulletInitialX() {
+            return mPositionX + polarX(mSize);
+        }
+
+        public float getBulletInitialY() {
+            return mPositionY + polarY(mSize);
+        }
+
+        public float getBulletVelocityX(float relativeSpeed) {
+            return mVelocityX + polarX(relativeSpeed);
+        }
+
+        public float getBulletVelocityY(float relativeSpeed) {
+            return mVelocityY + polarY(relativeSpeed);
+        }
+
+        public void accelerate(float tau, float maxThrust, float maxSpeed) {
+            final float thrust = mHeadingMagnitude * maxThrust;
+            mVelocityX += polarX(thrust);
+            mVelocityY += polarY(thrust);
+
+            final float speed = pythag(mVelocityX, mVelocityY);
+            if (speed > maxSpeed) {
+                final float scale = maxSpeed / speed;
+                mVelocityX = mVelocityX * scale;
+                mVelocityY = mVelocityY * scale;
+            }
+        }
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            wrapAtPlayfieldBoundary();
+            return true;
+        }
+
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mPaint, mDestroyAnimProgress,
+                    255, 63, 255, 63,
+                    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;
+        }
+    }
+
+    private class Bullet extends Sprite {
+        private final Paint mPaint;
+
+        public Bullet() {
+            mPaint = new Paint();
+            mPaint.setStyle(Style.FILL);
+
+            setSize(mBulletSize);
+        }
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            return !isOutsidePlayfield();
+        }
+
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mPaint, mDestroyAnimProgress,
+                    255, 255, 255, 0,
+                    0, 255, 255, 255);
+            canvas.drawCircle(mPositionX, mPositionY, mSize, mPaint);
+        }
+
+        @Override
+        public float getDestroyAnimDuration() {
+            return 0.125f;
+        }
+    }
+
+    private class Obstacle extends Sprite {
+        private final Paint mPaint;
+
+        public Obstacle() {
+            mPaint = new Paint();
+            mPaint.setARGB(255, 127, 127, 255);
+            mPaint.setStyle(Style.FILL);
+        }
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            wrapAtPlayfieldBoundary();
+            return true;
+        }
+
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mPaint, mDestroyAnimProgress,
+                    255, 127, 127, 255,
+                    0, 255, 0, 0);
+            canvas.drawCircle(mPositionX, mPositionY,
+                    mSize * (1.0f - mDestroyAnimProgress), mPaint);
+        }
+
+        @Override
+        public float getDestroyAnimDuration() {
+            return 0.25f;
+        }
+    }
+}
\ No newline at end of file