/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* 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.badlogic.gdx.backends.android;
import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Vibrator;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Graphics.DisplayMode;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.Pool;
/**
* An implementation of the {@link Input} interface for Android.
*
* @author mzechner
*/
/** @author jshapcot */
public class AndroidInput implements Input, OnKeyListener, OnTouchListener {
static class KeyEvent {
static final int KEY_DOWN = 0;
static final int KEY_UP = 1;
static final int KEY_TYPED = 2;
long timeStamp;
int type;
int keyCode;
char keyChar;
}
static class TouchEvent {
static final int TOUCH_DOWN = 0;
static final int TOUCH_UP = 1;
static final int TOUCH_DRAGGED = 2;
long timeStamp;
int type;
int x;
int y;
int pointer;
}
Pool<KeyEvent> usedKeyEvents = new Pool<KeyEvent>(16, 1000) {
protected KeyEvent newObject() {
return new KeyEvent();
}
};
Pool<TouchEvent> usedTouchEvents = new Pool<TouchEvent>(16, 1000) {
protected TouchEvent newObject() {
return new TouchEvent();
}
};
ArrayList<OnKeyListener> keyListeners = new ArrayList();
ArrayList<KeyEvent> keyEvents = new ArrayList();
ArrayList<TouchEvent> touchEvents = new ArrayList();
int[] touchX = new int[20];
int[] touchY = new int[20];
int[] deltaX = new int[20];
int[] deltaY = new int[20];
boolean[] touched = new boolean[20];
int[] realId = new int[10];
final boolean hasMultitouch;
private IntMap<Object> keys = new IntMap<Object>();
private SensorManager manager;
public boolean accelerometerAvailable = false;
private final float[] accelerometerValues = new float[3];
private String text = null;
private TextInputListener textListener = null;
private Handler handle;
final Application app;
final Context context;
private final AndroidTouchHandler touchHandler;
private int sleepTime = 0;
private boolean catchBack = false;
private boolean catchMenu = false;
protected final Vibrator vibrator;
private boolean compassAvailable = false;
boolean keyboardAvailable;
private final float[] magneticFieldValues = new float[3];
private float azimuth = 0;
private float pitch = 0;
private float roll = 0;
private float inclination = 0;
private boolean justTouched = false;
private InputProcessor processor;
private final AndroidApplicationConfiguration config;
private final Orientation nativeOrientation;
private long currentEventTimeStamp = System.nanoTime();
private final AndroidOnscreenKeyboard onscreenKeyboard;
private SensorEventListener accelerometerListener;
private SensorEventListener compassListener;
public AndroidInput(Application activity, Context context, Object view, AndroidApplicationConfiguration config) {
// we hook into View, for LWPs we call onTouch below directly from
// within the AndroidLivewallpaperEngine#onTouchEvent() method.
if (view instanceof View) {
View v = (View) view;
v.setOnKeyListener(this);
v.setOnTouchListener(this);
v.setFocusable(true);
v.setFocusableInTouchMode(true);
v.requestFocus();
v.requestFocusFromTouch();
}
this.config = config;
this.onscreenKeyboard = new AndroidOnscreenKeyboard(context, new Handler(), this);
for (int i = 0; i < realId.length; i++)
realId[i] = -1;
handle = new Handler();
this.app = activity;
this.context = context;
this.sleepTime = config.touchSleepTime;
int sdkVersion = Integer.parseInt(android.os.Build.VERSION.SDK);
if (sdkVersion >= 5)
touchHandler = new AndroidMultiTouchHandler();
else
touchHandler = new AndroidSingleTouchHandler();
hasMultitouch = touchHandler.supportsMultitouch(context);
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
int rotation = getRotation();
DisplayMode mode = app.getGraphics().getDesktopDisplayMode();
if (((rotation == 0 || rotation == 180) && (mode.width >= mode.height))
|| ((rotation == 90 || rotation == 270) && (mode.width <= mode.height))) {
nativeOrientation = Orientation.Landscape;
} else {
nativeOrientation = Orientation.Portrait;
}
}
@Override
public float getAccelerometerX() {
return accelerometerValues[0];
}
@Override
public float getAccelerometerY() {
return accelerometerValues[1];
}
@Override
public float getAccelerometerZ() {
return accelerometerValues[2];
}
@Override
public void getTextInput(final TextInputListener listener, final String title, final String text) {
handle.post(new Runnable() {
public void run() {
AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(title);
final EditText input = new EditText(context);
input.setText(text);
input.setSingleLine();
alert.setView(input);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
listener.input(input.getText().toString());
}
});
}
});
alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
listener.canceled();
}
});
}
});
alert.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
listener.canceled();
}
});
}
});
alert.show();
}
});
}
public void getPlaceholderTextInput(final TextInputListener listener, final String title, final String placeholder) {
handle.post(new Runnable() {
public void run() {
AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(title);
final EditText input = new EditText(context);
input.setHint(placeholder);
input.setSingleLine();
alert.setView(input);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
listener.input(input.getText().toString());
}
});
}
});
alert.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
listener.canceled();
}
});
}
});
alert.show();
}
});
}
@Override
public int getX() {
synchronized (this) {
return touchX[0];
}
}
@Override
public int getY() {
synchronized (this) {
return touchY[0];
}
}
@Override
public int getX(int pointer) {
synchronized (this) {
return touchX[pointer];
}
}
@Override
public int getY(int pointer) {
synchronized (this) {
return touchY[pointer];
}
}
public boolean isTouched(int pointer) {
synchronized (this) {
return touched[pointer];
}
}
@Override
public boolean isKeyPressed(int key) {
synchronized (this) {
if (key == Input.Keys.ANY_KEY)
return keys.size > 0;
else
return keys.containsKey(key);
}
}
@Override
public boolean isTouched() {
synchronized (this) {
return touched[0];
}
}
public void setInputProcessor(InputProcessor processor) {
synchronized (this) {
this.processor = processor;
}
}
void processEvents() {
synchronized (this) {
justTouched = false;
if (processor != null) {
final InputProcessor processor = this.processor;
int len = keyEvents.size();
for (int i = 0; i < len; i++) {
KeyEvent e = keyEvents.get(i);
currentEventTimeStamp = e.timeStamp;
switch (e.type) {
case KeyEvent.KEY_DOWN:
processor.keyDown(e.keyCode);
break;
case KeyEvent.KEY_UP:
processor.keyUp(e.keyCode);
break;
case KeyEvent.KEY_TYPED:
processor.keyTyped(e.keyChar);
}
usedKeyEvents.free(e);
}
len = touchEvents.size();
for (int i = 0; i < len; i++) {
TouchEvent e = touchEvents.get(i);
currentEventTimeStamp = e.timeStamp;
switch (e.type) {
case TouchEvent.TOUCH_DOWN:
processor.touchDown(e.x, e.y, e.pointer, Buttons.LEFT);
justTouched = true;
break;
case TouchEvent.TOUCH_UP:
processor.touchUp(e.x, e.y, e.pointer, Buttons.LEFT);
break;
case TouchEvent.TOUCH_DRAGGED:
processor.touchDragged(e.x, e.y, e.pointer);
}
usedTouchEvents.free(e);
}
} else {
int len = touchEvents.size();
for (int i = 0; i < len; i++) {
TouchEvent e = touchEvents.get(i);
if (e.type == TouchEvent.TOUCH_DOWN)
justTouched = true;
usedTouchEvents.free(e);
}
len = keyEvents.size();
for (int i = 0; i < len; i++) {
usedKeyEvents.free(keyEvents.get(i));
}
}
if (touchEvents.size() == 0) {
for (int i = 0; i < deltaX.length; i++) {
deltaX[0] = 0;
deltaY[0] = 0;
}
}
keyEvents.clear();
touchEvents.clear();
}
}
boolean requestFocus = true;
@Override
public boolean onTouch(View view, MotionEvent event) {
if (requestFocus && view != null) {
view.requestFocus();
view.requestFocusFromTouch();
requestFocus = false;
}
// synchronized in handler.postTouchEvent()
touchHandler.onTouch(event, this);
if (sleepTime != 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
return true;
}
/**
* Called in {@link AndroidLiveWallpaperService} on tap
*
* @param x
* @param y
*/
public void onTap(int x, int y) {
postTap(x, y);
}
/**
* Called in {@link AndroidLiveWallpaperService} on drop
*
* @param x
* @param y
*/
public void onDrop(int x, int y) {
postTap(x, y);
}
protected void postTap(int x, int y) {
synchronized (this) {
TouchEvent event = usedTouchEvents.obtain();
event.timeStamp = System.nanoTime();
event.pointer = 0;
event.x = x;
event.y = y;
event.type = TouchEvent.TOUCH_DOWN;
touchEvents.add(event);
event = usedTouchEvents.obtain();
event.timeStamp = System.nanoTime();
event.pointer = 0;
event.x = x;
event.y = y;
event.type = TouchEvent.TOUCH_UP;
touchEvents.add(event);
}
Gdx.app.getGraphics().requestRendering();
}
@Override
public boolean onKey(View v, int keyCode, android.view.KeyEvent e) {
for (int i = 0, n = keyListeners.size(); i < n; i++)
if (keyListeners.get(i).onKey(v, keyCode, e))
return true;
synchronized (this) {
char character = (char) e.getUnicodeChar();
// Android doesn't report a unicode char for back space. hrm...
if (keyCode == 67)
character = '\b';
KeyEvent event = null;
switch (e.getAction()) {
case android.view.KeyEvent.ACTION_DOWN:
event = usedKeyEvents.obtain();
event.keyChar = 0;
event.keyCode = e.getKeyCode();
event.type = KeyEvent.KEY_DOWN;
// Xperia hack for circle key. gah...
if (keyCode == android.view.KeyEvent.KEYCODE_BACK && e.isAltPressed()) {
keyCode = Keys.BUTTON_CIRCLE;
event.keyCode = keyCode;
}
keyEvents.add(event);
keys.put(event.keyCode, null);
break;
case android.view.KeyEvent.ACTION_UP:
event = usedKeyEvents.obtain();
event.keyChar = 0;
event.keyCode = e.getKeyCode();
event.type = KeyEvent.KEY_UP;
// Xperia hack for circle key. gah...
if (keyCode == android.view.KeyEvent.KEYCODE_BACK && e.isAltPressed()) {
keyCode = Keys.BUTTON_CIRCLE;
event.keyCode = keyCode;
}
keyEvents.add(event);
event = usedKeyEvents.obtain();
event.keyChar = character;
event.keyCode = 0;
event.type = KeyEvent.KEY_TYPED;
keyEvents.add(event);
if (keyCode == Keys.BUTTON_CIRCLE)
keys.remove(Keys.BUTTON_CIRCLE);
else
keys.remove(e.getKeyCode());
}
app.getGraphics().requestRendering();
}
// circle button on Xperia Play shouldn't need catchBack == true
if (keyCode == Keys.BUTTON_CIRCLE)
return true;
if (catchBack && keyCode == android.view.KeyEvent.KEYCODE_BACK)
return true;
if (catchMenu && keyCode == android.view.KeyEvent.KEYCODE_MENU)
return true;
return false;
}
@Override
public void setOnscreenKeyboardVisible(final boolean visible) {
// onscreenKeyboard.setVisible(visible);
handle.post(new Runnable() {
public void run() {
InputMethodManager manager = (InputMethodManager) context
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (visible) {
View view = ((AndroidGraphics) app.getGraphics()).getView();
view.setFocusable(true);
view.setFocusableInTouchMode(true);
manager.showSoftInput(((AndroidGraphics) app.getGraphics()).getView(), 0);
} else {
manager.hideSoftInputFromWindow(((AndroidGraphics) app.getGraphics()).getView().getWindowToken(), 0);
}
}
});
}
@Override
public void setCatchBackKey(boolean catchBack) {
this.catchBack = catchBack;
}
@Override
public void setCatchMenuKey(boolean catchMenu) {
this.catchMenu = catchMenu;
}
@Override
public void vibrate(int milliseconds) {
vibrator.vibrate(milliseconds);
}
@Override
public void vibrate(long[] pattern, int repeat) {
vibrator.vibrate(pattern, repeat);
}
@Override
public void cancelVibrate() {
vibrator.cancel();
}
@Override
public boolean justTouched() {
return justTouched;
}
@Override
public boolean isButtonPressed(int button) {
if (button == Buttons.LEFT)
return isTouched();
else
return false;
}
final float[] R = new float[9];
final float[] orientation = new float[3];
private void updateOrientation() {
if (SensorManager.getRotationMatrix(R, null, accelerometerValues, magneticFieldValues)) {
SensorManager.getOrientation(R, orientation);
azimuth = (float) Math.toDegrees(orientation[0]);
pitch = (float) Math.toDegrees(orientation[1]);
roll = (float) Math.toDegrees(orientation[2]);
}
}
/**
* Returns the rotation matrix describing the devices rotation as per <a href=
* "http://developer.android.com/reference/android/hardware/SensorManager.html#getRotationMatrix(float[], float[], float[], float[])"
* >SensorManager#getRotationMatrix(float[], float[], float[], float[])</a>. Does not manipulate the matrix if the
* platform does not have an accelerometer.
*
* @param matrix
*/
public void getRotationMatrix(float[] matrix) {
SensorManager.getRotationMatrix(matrix, null, accelerometerValues, magneticFieldValues);
}
@Override
public float getAzimuth() {
if (!compassAvailable)
return 0;
updateOrientation();
return azimuth;
}
@Override
public float getPitch() {
if (!compassAvailable)
return 0;
updateOrientation();
return pitch;
}
@Override
public float getRoll() {
if (!compassAvailable)
return 0;
updateOrientation();
return roll;
}
void registerSensorListeners() {
if (config.useAccelerometer) {
manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
if (manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() == 0) {
accelerometerAvailable = false;
} else {
Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0);
accelerometerListener = new SensorListener(this.nativeOrientation, this.accelerometerValues,
this.magneticFieldValues);
accelerometerAvailable = manager.registerListener(accelerometerListener, accelerometer,
SensorManager.SENSOR_DELAY_GAME);
}
} else
accelerometerAvailable = false;
if (config.useCompass) {
if (manager == null)
manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor sensor = manager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
if (sensor != null) {
compassAvailable = accelerometerAvailable;
if (compassAvailable) {
compassListener = new SensorListener(this.nativeOrientation, this.accelerometerValues,
this.magneticFieldValues);
compassAvailable = manager.registerListener(compassListener, sensor,
SensorManager.SENSOR_DELAY_GAME);
}
} else {
compassAvailable = false;
}
} else
compassAvailable = false;
Gdx.app.log("AndroidInput", "sensor listener setup");
}
void unregisterSensorListeners() {
if (manager != null) {
if (accelerometerListener != null) {
manager.unregisterListener(accelerometerListener);
accelerometerListener = null;
}
if (compassListener != null) {
manager.unregisterListener(compassListener);
compassListener = null;
}
manager = null;
}
Gdx.app.log("AndroidInput", "sensor listener tear down");
}
@Override
public InputProcessor getInputProcessor() {
return this.processor;
}
@Override
public boolean isPeripheralAvailable(Peripheral peripheral) {
if (peripheral == Peripheral.Accelerometer)
return accelerometerAvailable;
if (peripheral == Peripheral.Compass)
return compassAvailable;
if (peripheral == Peripheral.HardwareKeyboard)
return keyboardAvailable;
if (peripheral == Peripheral.OnscreenKeyboard)
return true;
if (peripheral == Peripheral.Vibrator)
return vibrator != null;
if (peripheral == Peripheral.MultitouchScreen)
return hasMultitouch;
return false;
}
public int getFreePointerIndex() {
int len = realId.length;
for (int i = 0; i < len; i++) {
if (realId[i] == -1)
return i;
}
int[] tmp = new int[realId.length + 1];
System.arraycopy(realId, 0, tmp, 0, realId.length);
realId = tmp;
return tmp.length - 1;
}
public int lookUpPointerIndex(int pointerId) {
int len = realId.length;
for (int i = 0; i < len; i++) {
if (realId[i] == pointerId)
return i;
}
StringBuffer buf = new StringBuffer();
for (int i = 0; i < len; i++) {
buf.append(i + ":" + realId[i] + " ");
}
Gdx.app.log("AndroidInput", "Pointer ID lookup failed: " + pointerId + ", " + buf.toString());
return -1;
}
@Override
public int getRotation() {
int orientation = 0;
if (context instanceof Activity) {
orientation = ((Activity) context).getWindowManager().getDefaultDisplay().getOrientation();
} else {
orientation = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
.getOrientation();
}
switch (orientation) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
return 0;
}
}
@Override
public Orientation getNativeOrientation() {
return nativeOrientation;
}
@Override
public void setCursorCatched(boolean catched) {
}
@Override
public boolean isCursorCatched() {
return false;
}
@Override
public int getDeltaX() {
return deltaX[0];
}
@Override
public int getDeltaX(int pointer) {
return deltaX[pointer];
}
@Override
public int getDeltaY() {
return deltaY[0];
}
@Override
public int getDeltaY(int pointer) {
return deltaY[pointer];
}
@Override
public void setCursorPosition(int x, int y) {
}
@Override
public long getCurrentEventTime() {
return currentEventTimeStamp;
}
public void addKeyListener(OnKeyListener listener) {
keyListeners.add(listener);
}
/**
* Our implementation of SensorEventListener. Because Android doesn't like it when we register more than one Sensor
* to a single SensorEventListener, we add one of these for each Sensor. Could use an anonymous class, but I don't
* see any harm in explicitly defining it here. Correct me if I am wrong.
*/
private class SensorListener implements SensorEventListener {
final float[] accelerometerValues;
final float[] magneticFieldValues;
final Orientation nativeOrientation;
SensorListener(Orientation nativeOrientation, float[] accelerometerValues, float[] magneticFieldValues) {
this.accelerometerValues = accelerometerValues;
this.magneticFieldValues = magneticFieldValues;
this.nativeOrientation = nativeOrientation;
}
@Override
public void onAccuracyChanged(Sensor arg0, int arg1) {
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
if (nativeOrientation == Orientation.Portrait) {
System.arraycopy(event.values, 0, accelerometerValues, 0, accelerometerValues.length);
} else {
accelerometerValues[0] = event.values[1];
accelerometerValues[1] = -event.values[0];
accelerometerValues[2] = event.values[2];
}
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(event.values, 0, magneticFieldValues, 0, magneticFieldValues.length);
}
}
}
}