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