Browse Source

support gamepad remapping on android

hondres 9 years ago
parent
commit
e7c920fdba

+ 14 - 0
main/input_default.cpp

@@ -164,6 +164,12 @@ void InputDefault::joy_connection_changed(int p_idx, bool p_connected, String p_
 				//printf("found mapping\n");
 			};
 		};
+#ifdef ANDROID_ENABLED
+		//Use a default mapping for Android, as we recieve events using indices of a SDL_GAMECONTROLLER.
+		//So we need to map those to our own joystick layout
+		if (mapping == -1)
+			mapping = 0;
+#endif
 		js.mapping = mapping;
 	};
 	joy_names[p_idx] = js;
@@ -513,6 +519,10 @@ InputDefault::InputDefault() {
 	hat_map_default[HAT_LEFT].index = JOY_DPAD_LEFT;
 	hat_map_default[HAT_LEFT].value = 0;
 
+#ifdef ANDROID_ENABLED
+	//add the default mapping first, in case someone actually sets the env variable on their phone
+	parse_mapping("Default Android Gamepad,Default Controller,leftx:a0,lefty:a1,dpdown:h0.4,rightstick:b8,rightshoulder:b10,rightx:a2,start:b6,righty:a3,dpleft:h0.8,lefttrigger:a4,x:b2,dpup:h0.1,back:b4,leftstick:b7,leftshoulder:b9,y:b3,a:b0,dpright:h0.2,righttrigger:a5,b:b1,");
+#endif
 	String env_mapping = OS::get_singleton()->get_environment("SDL_GAMECONTROLLERCONFIG");
 	if (env_mapping != "") {
 
@@ -865,6 +875,10 @@ String InputDefault::get_joy_guid(int p_device) const {
 
 //platforms that use the remapping system can override and call to these ones
 bool InputDefault::is_joy_mapped(int p_device) {
+#ifdef ANDROID_ENABLED
+	if (joy_names[p_device].mapping == 0)
+		return false;
+#endif
 	return joy_names[p_device].mapping != -1 ? true : false;
 }
 

+ 2 - 2
main/input_default.h

@@ -36,7 +36,7 @@ class InputDefault : public Input {
 	struct Joystick {
 		StringName name;
 		StringName uid;
-		bool last_buttons[JOY_BUTTON_MAX + 2]; //html5 needs support for 18 buttons to map some devices correctly
+		bool last_buttons[JOY_BUTTON_MAX + 19]; //apparently SDL specifies 35 possible buttons on android
 		float last_axis[JOY_AXIS_MAX];
 		float filter;
 		int last_hat;
@@ -50,7 +50,7 @@ class InputDefault : public Input {
 				last_axis[i] = 0.0f;
 
 			}
-			for (int i = 0; i < JOY_BUTTON_MAX + 2; i++) {
+			for (int i = 0; i < JOY_BUTTON_MAX + 19; i++) {
 
 				last_buttons[i] = false;
 			}

+ 2 - 0
platform/android/java/src/org/godotengine/godot/GodotLib.java

@@ -54,6 +54,8 @@ public class GodotLib {
 	 public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed);
 	 public static native void joybutton(int p_device, int p_but, boolean p_pressed);
 	 public static native void joyaxis(int p_device, int p_axis, float p_value);
+	 public static native void joyhat(int p_device, int p_hat_x, int p_hat_y);
+	 public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name);
      public static native void focusin();
      public static native void focusout();
      public static native void audio();

+ 106 - 131
platform/android/java/src/org/godotengine/godot/GodotView.java

@@ -36,14 +36,21 @@ import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.content.ContextWrapper;
 import android.view.InputDevice;
+import android.hardware.input.InputManager;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import javax.microedition.khronos.egl.EGL10;
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.egl.EGLContext;
 import javax.microedition.khronos.egl.EGLDisplay;
 import javax.microedition.khronos.opengles.GL10;
 
+import org.godotengine.godot.input.InputManagerCompat;
+import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener;
 /**
  * A simple GLSurfaceView sub-class that demonstrate how to perform
  * OpenGL ES 2.0 rendering into a GL Surface. Note the following important
@@ -62,7 +69,7 @@ import javax.microedition.khronos.opengles.GL10;
  *   that matches it exactly (with regards to red/green/blue/alpha channels
  *   bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
  */
-public class GodotView extends GLSurfaceView {
+public class GodotView extends GLSurfaceView implements InputDeviceListener {
 
 	private static String TAG = "GodotView";
 	private static final boolean DEBUG = false;
@@ -75,6 +82,8 @@ public class GodotView extends GLSurfaceView {
 
 	private Godot activity;
 
+
+	private InputManagerCompat mInputManager;
 	public GodotView(Context context,GodotIO p_io,boolean p_use_gl2, boolean p_use_32_bits, Godot p_activity) {
 		super(context);
 		ctx=context;
@@ -88,7 +97,8 @@ public class GodotView extends GLSurfaceView {
 			//will only work on SDK 11+!!
 			setPreserveEGLContextOnPause(true);
 		}
-
+		mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext());
+		mInputManager.registerInputDeviceListener(this, null);
 		init(false, 16, 0);
     }
 
@@ -119,50 +129,112 @@ public class GodotView extends GLSurfaceView {
 				button = 3;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_L1:
-				button = 4;
+				button = 9;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_L2:
-				button = 6;
+				button = 15;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_R1:
-				button = 5;
+				button = 10;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_R2:
-				button = 7;
+				button = 16;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_SELECT:
-				button = 10;
+				button = 4;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_START:
-				button = 11;
+				button = 6;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_THUMBL:
-				button = 8;
+				button = 7;
 				break;
 			case KeyEvent.KEYCODE_BUTTON_THUMBR:
-				button = 9;
+				button = 8;
 				break;
 			case KeyEvent.KEYCODE_DPAD_UP:
-				button = 12;
+				button = 11;
 				break;
 			case KeyEvent.KEYCODE_DPAD_DOWN:
-				button = 13;
+				button = 12;
 				break;
 			case KeyEvent.KEYCODE_DPAD_LEFT:
-				button = 14;
+				button = 13;
 				break;
 			case KeyEvent.KEYCODE_DPAD_RIGHT:
-				button = 15;
+				button = 14;
+				break;
+			case KeyEvent.KEYCODE_BUTTON_C:
+				button = 17;
+				break;
+			case KeyEvent.KEYCODE_BUTTON_Z:
+				button = 18;
 				break;
 
 			default:
-				button = keyCode - KeyEvent.KEYCODE_BUTTON_1;
+				button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20;
 				break;
 		};
-
 		return button;
 	};
 
+	private static class joystick {
+		public int device_id;
+		public String name;
+		public ArrayList<InputDevice.MotionRange> axes;
+		public ArrayList<InputDevice.MotionRange> hats;
+	}
+
+	private static class RangeComparator implements Comparator<InputDevice.MotionRange> {
+		@Override
+		public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
+			return arg0.getAxis() - arg1.getAxis();
+		}
+	}
+
+	ArrayList<joystick> joy_devices = new ArrayList<joystick>();
+
+	private int find_joy_device(int device_id) {
+		for (int i=0; i<joy_devices.size(); i++) {
+			if (joy_devices.get(i).device_id == device_id) {
+					return i;
+			}
+		}
+		onInputDeviceAdded(device_id);
+		return joy_devices.size() - 1;
+	}
+
+	@Override public void onInputDeviceAdded(int deviceId) {
+		joystick joy = new joystick();
+		joy.device_id = deviceId;
+		int id = joy_devices.size();
+		InputDevice device = mInputManager.getInputDevice(deviceId);
+		joy.name = device.getName();
+		joy.axes = new ArrayList<InputDevice.MotionRange>();
+		joy.hats = new ArrayList<InputDevice.MotionRange>();
+		List<InputDevice.MotionRange> ranges = device.getMotionRanges();
+		Collections.sort(ranges, new RangeComparator());
+		for (InputDevice.MotionRange range : ranges) {
+			if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
+				joy.hats.add(range);
+			}
+			else {
+				joy.axes.add(range);
+			}
+		}
+		joy_devices.add(joy);
+		GodotLib.joyconnectionchanged(id, true, joy.name);
+  }
+
+	@Override public void onInputDeviceRemoved(int deviceId) {
+		int id = find_joy_device(deviceId);
+		joy_devices.remove(id);
+		GodotLib.joyconnectionchanged(id, false, "");
+	}
+
+	@Override public void onInputDeviceChanged(int deviceId) {
+
+	}
 	@Override public boolean onKeyUp(int keyCode, KeyEvent event) {
 
 		if (keyCode == KeyEvent.KEYCODE_BACK) {
@@ -177,7 +249,7 @@ public class GodotView extends GLSurfaceView {
 		if ((source & InputDevice.SOURCE_JOYSTICK) != 0 || (source & InputDevice.SOURCE_DPAD) != 0 || (source & InputDevice.SOURCE_GAMEPAD) != 0) {
 
 			int button = get_godot_button(keyCode);
-			int device = event.getDeviceId();
+			int device = find_joy_device(event.getDeviceId());
 
 			GodotLib.joybutton(device, button, false);
 			return true;
@@ -209,7 +281,8 @@ public class GodotView extends GLSurfaceView {
 			if (event.getRepeatCount() > 0) // ignore key echo
 				return true;
 			int button = get_godot_button(keyCode);
-			int device = event.getDeviceId();
+			int device = find_joy_device(event.getDeviceId());
+
 			//Log.e(TAG, String.format("joy button down! button %x, %d, device %d", keyCode, button, device));
 
 			GodotLib.joybutton(device, button, true);
@@ -221,125 +294,27 @@ public class GodotView extends GLSurfaceView {
 		return super.onKeyDown(keyCode, event);
 	}
 
-	public float axis_value(MotionEvent p_event, InputDevice p_device, int p_axis, int p_pos) {
-
-		final InputDevice.MotionRange range = p_device.getMotionRange(p_axis, p_event.getSource());
-		if (range == null)
-			return 0;
-
-		//Log.e(TAG, String.format("axis ranges %f, %f, %f", range.getRange(), range.getMin(), range.getMax()));
-
-		final float flat = range.getFlat();
-		final float value =
-			p_pos < 0 ? p_event.getAxisValue(p_axis):
-			p_event.getHistoricalAxisValue(p_axis, p_pos);
-
-		final float absval = Math.abs(value);
-		if (absval <= flat) {
-			return 0;
-		};
-
-		final float ret = (value - range.getMin()) / range.getRange() * 2 - 1.0f;
-
-		return ret;
-	};
-
-	float[] last_axis_values = { 0, 0, 0, 0, -1, -1 };
-	boolean[] last_axis_buttons = { false, false, false, false, false, false }; // dpad up down left right, ltrigger, rtrigger
-
-	public void process_axis_state(MotionEvent p_event, int p_pos) {
-
-		int device_id = p_event.getDeviceId();
-		InputDevice device = p_event.getDevice();
-		float val;
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_X, p_pos);
-		if (val != last_axis_values[0]) {
-			last_axis_values[0] = val;
-			//Log.e(TAG, String.format("axis moved! axis %d, value %f", 0, val));
-			GodotLib.joyaxis(device_id, 0, val);
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_Y, p_pos);
-		if (val != last_axis_values[1]) {
-			last_axis_values[1] = val;
-			//Log.e(TAG, String.format("axis moved! axis %d, value %f", 1, val));
-			GodotLib.joyaxis(device_id, 1, val);
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_Z, p_pos);
-		if (val != last_axis_values[2]) {
-			last_axis_values[2] = val;
-			//Log.e(TAG, String.format("axis moved! axis %d, value %f", 2, val));
-			GodotLib.joyaxis(device_id, 2, val);
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_RZ, p_pos);
-		if (val != last_axis_values[3]) {
-			last_axis_values[3] = val;
-			//Log.e(TAG, String.format("axis moved! axis %d, value %f", 3, val));
-			GodotLib.joyaxis(device_id, 3, val);
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_LTRIGGER, p_pos);
-		if (val != last_axis_values[4]) {
-			last_axis_values[4] = val;
-			if ((val != 0) != (last_axis_buttons[4])) {
-				last_axis_buttons[4] = (val != 0);
-				GodotLib.joybutton(device_id, 6, (val != 0));
-			};
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_RTRIGGER, p_pos);
-		if (val != last_axis_values[5]) {
-			last_axis_values[5] = val;
-			if ((val != 0) != (last_axis_buttons[5])) {
-				last_axis_buttons[5] = (val != 0);
-				GodotLib.joybutton(device_id, 7, (val != 0));
-			};
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_HAT_Y, p_pos);
-
-		if (last_axis_buttons[0] != (val > 0)) {
-			last_axis_buttons[0] = val > 0;
-			GodotLib.joybutton(device_id, 12, val > 0);
-		};
-		if (last_axis_buttons[1] != (val < 0)) {
-			last_axis_buttons[1] = val < 0;
-			GodotLib.joybutton(device_id, 13, val > 0);
-		};
-
-		val = axis_value(p_event, device, MotionEvent.AXIS_HAT_X, p_pos);
-		if (last_axis_buttons[2] != (val < 0)) {
-			last_axis_buttons[2] = val < 0;
-			GodotLib.joybutton(device_id, 14, val < 0);
-		};
-		if (last_axis_buttons[3] != (val > 0)) {
-			last_axis_buttons[3] = val > 0;
-			GodotLib.joybutton(device_id, 15, val > 0);
-		};
-	};
-
 	@Override public boolean onGenericMotionEvent(MotionEvent event) {
 
 		if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) {
 
-			// Process all historical movement samples in the batch
-			final int historySize = event.getHistorySize();
+			int device_id = find_joy_device(event.getDeviceId());
+			joystick joy = joy_devices.get(device_id);
 
-			// Process the movements starting from the
-			// earliest historical position in the batch
-			for (int i = 0; i < historySize; i++) {
-				// Process the event at historical position i
-				process_axis_state(event, i);
+			for (int i = 0; i < joy.axes.size(); i++) {
+				InputDevice.MotionRange range = joy.axes.get(i);
+				float value = (event.getAxisValue(range.getAxis()) - range.getMin() ) / range.getRange() * 2.0f - 1.0f;
+				//Log.e(TAG, String.format("axis event: %d, value %f", i, value));
+				GodotLib.joyaxis(device_id, i, value);
 			}
 
-			// Process the current movement sample in the batch (position -1)
-			process_axis_state(event, -1);
+			for (int i = 0; i < joy.hats.size(); i+=2) {
+				int hatX = Math.round(event.getAxisValue(joy.hats.get(i).getAxis()));
+				int hatY = Math.round(event.getAxisValue(joy.hats.get(i+1).getAxis()));
+				//Log.e(TAG, String.format("HAT EVENT %d, %d", hatX, hatY));
+				GodotLib.joyhat(device_id, hatX, hatY);
+			}
 			return true;
-
-
 		};
 
 		return super.onGenericMotionEvent(event);
@@ -413,12 +388,12 @@ public class GodotView extends GLSurfaceView {
     	/* Fallback if 32bit View is not supported*/
 	private static class FallbackConfigChooser extends ConfigChooser {
 		private ConfigChooser fallback;
-		
+
 		public FallbackConfigChooser(int r, int g, int b, int a, int depth, int stencil, ConfigChooser fallback) {
 			super(r, g, b, a, depth, stencil);
 			this.fallback = fallback;
 		}
-      
+
       		@Override
 		public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {
 			EGLConfig ec = super.chooseConfig(egl, display, configs);

+ 140 - 0
platform/android/java/src/org/godotengine/godot/input/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 org.godotengine.godot.input;
+
+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();
+            }
+        }
+    }
+}

+ 107 - 0
platform/android/java/src/org/godotengine/godot/input/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 org.godotengine.godot.input;
+
+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
+    }
+
+}

+ 211 - 0
platform/android/java/src/org/godotengine/godot/input/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 org.godotengine.godot.input;
+
+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);
+    }
+
+}

+ 47 - 28
platform/android/java_glue.cpp

@@ -41,6 +41,7 @@
 #include "core/os/keyboard.h"
 #include "java_class_wrapper.h"
 #include "android/asset_manager_jni.h"
+#include "main/input_default.h"
 
 static JavaClassWrapper *java_class_wrapper=NULL;
 static OS_Android *os_android=NULL;
@@ -639,6 +640,7 @@ struct JAndroidPointerEvent {
 
 static List<JAndroidPointerEvent> pointer_events;
 static List<InputEvent> key_events;
+static List<OS_Android::JoystickEvent> joy_events;
 static bool initialized=false;
 static Mutex *input_mutex=NULL;
 static Mutex *suspend_mutex=NULL;
@@ -1067,6 +1069,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv * env, jo
 		key_events.pop_front();
 	};
 
+	while (joy_events.size()) {
+
+		OS_Android::JoystickEvent event = joy_events.front()->get();
+		os_android->process_joy_event(event);
+
+		joy_events.pop_front();
+	}
+
 	if (quit_request) {
 
 		os_android->main_loop_request_quit();
@@ -1380,48 +1390,57 @@ static unsigned int android_get_keysym(unsigned int p_code) {
 	return KEY_UNKNOWN;
 }
 
-static int find_device(int p_device) {
-
-	for (int i=0; i<joy_device_ids.size(); i++) {
-
-		if (joy_device_ids[i] == p_device) {
-			//print_line("found device at "+String::num(i));
-			return i;
-		};
-	};
-
-	//print_line("adding a device at" + String::num(joy_device_ids.size()));
-	joy_device_ids.push_back(p_device);
-
-	return joy_device_ids.size() - 1;
-};
-
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv * env, jobject obj, jint p_device, jint p_button, jboolean p_pressed) {
 
-	InputEvent ievent;
-	ievent.type = InputEvent::JOYSTICK_BUTTON;
-	ievent.device = find_device(p_device);
-	ievent.joy_button.button_index = p_button;
-	ievent.joy_button.pressed = p_pressed;
+	OS_Android::JoystickEvent jevent;
+	jevent.device = p_device;
+	jevent.type = OS_Android::JOY_EVENT_BUTTON;
+	jevent.index = p_button;
+	jevent.pressed = p_pressed;
 
 	input_mutex->lock();
-	key_events.push_back(ievent);
+	joy_events.push_back(jevent);
 	input_mutex->unlock();
 };
 
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv * env, jobject obj, jint p_device, jint p_axis, jfloat p_value) {
 
-	InputEvent ievent;
-	ievent.type = InputEvent::JOYSTICK_MOTION;
-	ievent.device = find_device(p_device);
-	ievent.joy_motion.axis = p_axis;
-	ievent.joy_motion.axis_value = p_value;
+	OS_Android::JoystickEvent jevent;
+	jevent.device = p_device;
+	jevent.type = OS_Android::JOY_EVENT_AXIS;
+	jevent.index = p_axis;
+	jevent.value = p_value;
 
 	input_mutex->lock();
-	key_events.push_back(ievent);
+	joy_events.push_back(jevent);
 	input_mutex->unlock();
 };
 
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv * env, jobject obj, jint p_device, jint p_hat_x, jint p_hat_y) {
+	OS_Android::JoystickEvent jevent;
+	jevent.device = p_device;
+	jevent.type = OS_Android::JOY_EVENT_HAT;
+	int hat = 0;
+	if (p_hat_x != 0) {
+		if (p_hat_x < 0) hat |= InputDefault::HAT_MASK_LEFT;
+		else hat |= InputDefault::HAT_MASK_RIGHT;
+	}
+	if (p_hat_y != 0) {
+		if (p_hat_y < 0) hat |= InputDefault::HAT_MASK_UP;
+		else hat |= InputDefault::HAT_MASK_DOWN;
+	}
+	jevent.hat = hat;
+	input_mutex->lock();
+	joy_events.push_back(jevent);
+	input_mutex->unlock();
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv * env, jobject obj, jint p_device, jboolean p_connected, jstring p_name) {
+	if (os_android) {
+		String name = env->GetStringUTFChars( p_name, NULL );
+		os_android->joy_connection_changed(p_device, p_connected, name);
+	}
+}
 
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv * env, jobject obj, jint p_scancode, jint p_unicode_char, jboolean p_pressed) {
 

+ 2 - 0
platform/android/java_glue.h

@@ -45,6 +45,8 @@ extern "C" {
 	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv * env, jobject obj, jint p_scancode, jint p_unicode_char, jboolean p_pressed);
 	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv * env, jobject obj, jint p_device, jint p_button, jboolean p_pressed);
 	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv * env, jobject obj, jint p_device, jint p_axis, jfloat p_value);
+	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv * env, jobject obj, jint p_device, jint p_hat_x, jint p_hat_y);
+	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv * env, jobject obj, jint p_device, jboolean p_connected, jstring p_name);
 	JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv * env, jobject obj);
     JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv * env, jobject obj,  jfloat x, jfloat y, jfloat z);
     JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv * env, jobject obj);

+ 31 - 0
platform/android/os_android.cpp

@@ -370,6 +370,25 @@ void OS_Android::main_loop_focusin(){
 
 }
 
+void OS_Android::process_joy_event(OS_Android::JoystickEvent p_event) {
+
+	switch (p_event.type) {
+	case JOY_EVENT_BUTTON:
+		last_id = input->joy_button(last_id, p_event.device, p_event.index, p_event.pressed);
+		break;
+	case JOY_EVENT_AXIS:
+		InputDefault::JoyAxis value;
+		value.min = -1;
+		value.value = p_event.value;
+		last_id = input->joy_axis(last_id, p_event.device, p_event.index, value);
+		break;
+	case JOY_EVENT_HAT:
+		last_id = input->joy_hat(last_id, p_event.device, p_event.hat);
+		break;
+	default:
+		return;
+	}
+}
 
 void OS_Android::process_event(InputEvent p_event) {
 
@@ -742,6 +761,18 @@ void OS_Android::set_context_is_16_bits(bool p_is_16) {
 		rasterizer->set_force_16_bits_fbo(p_is_16);
 }
 
+void OS_Android::joy_connection_changed(int p_device, bool p_connected, String p_name) {
+	return input->joy_connection_changed(p_device, p_connected, p_name, "");
+}
+
+bool OS_Android::is_joy_known(int p_device) {
+	return input->is_joy_mapped(p_device);
+}
+
+String OS_Android::get_joy_guid(int p_device) const {
+	return input->get_joy_guid_remapped(p_device);
+}
+
 OS_Android::OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk,  SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id,GetSystemDirFunc p_get_sdir_func, VideoPlayFunc p_video_play_func, VideoIsPlayingFunc p_video_is_playing_func, VideoPauseFunc p_video_pause_func, VideoStopFunc p_video_stop_func, SetKeepScreenOnFunc p_set_keep_screen_on_func, bool p_use_apk_expansion) {
 
 

+ 21 - 0
platform/android/os_android.h

@@ -83,6 +83,22 @@ public:
 		Point2 pos;
 	};
 
+	enum {
+		JOY_EVENT_BUTTON = 0,
+		JOY_EVENT_AXIS = 1,
+		JOY_EVENT_HAT = 2
+	};
+
+	struct JoystickEvent {
+
+		int device;
+		int type;
+		int index;
+		bool pressed;
+		float value;
+		int hat;
+	};
+
 private:
 
 	Vector<TouchPos> touch;
@@ -224,6 +240,7 @@ public:
 
 	void process_accelerometer(const Vector3& p_accelerometer);
 	void process_touch(int p_what,int p_pointer, const Vector<TouchPos>& p_points);
+	void process_joy_event(JoystickEvent p_event);
 	void process_event(InputEvent p_event);
 	void init_video_mode(int p_video_width,int p_video_height);
 
@@ -232,6 +249,10 @@ public:
 	virtual void native_video_pause();
 	virtual void native_video_stop();
 
+	virtual bool is_joy_known(int p_device);
+	virtual String get_joy_guid(int p_device) const;
+	void joy_connection_changed(int p_device, bool p_connected, String p_name);
+
 	OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk,  SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id,GetSystemDirFunc p_get_sdir_func, VideoPlayFunc p_video_play_func, VideoIsPlayingFunc p_video_is_playing_func, VideoPauseFunc p_video_pause_func, VideoStopFunc p_video_stop_func, SetKeepScreenOnFunc p_set_keep_screen_on_func, bool p_use_apk_expansion);
 	~OS_Android();