18 changed files with 5717 additions and 0 deletions
@ -0,0 +1,71 @@ |
|||||||
|
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); |
||||||
|
def buildAsApplication = !buildAsLibrary |
||||||
|
if (buildAsApplication) { |
||||||
|
apply plugin: 'com.android.application' |
||||||
|
} |
||||||
|
else { |
||||||
|
apply plugin: 'com.android.library' |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
compileSdkVersion 26 |
||||||
|
defaultConfig { |
||||||
|
if (buildAsApplication) { |
||||||
|
applicationId "org.libsdl.app" |
||||||
|
} |
||||||
|
minSdkVersion 16 |
||||||
|
targetSdkVersion 26 |
||||||
|
versionCode 1 |
||||||
|
versionName "1.0" |
||||||
|
externalNativeBuild { |
||||||
|
ndkBuild { |
||||||
|
arguments "APP_PLATFORM=android-16" |
||||||
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' |
||||||
|
} |
||||||
|
// cmake { |
||||||
|
// arguments "-DANDROID_APP_PLATFORM=android-16", "-DANDROID_STL=c++_static" |
||||||
|
// // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' |
||||||
|
// abiFilters 'arm64-v8a' |
||||||
|
// } |
||||||
|
} |
||||||
|
} |
||||||
|
buildTypes { |
||||||
|
release { |
||||||
|
minifyEnabled false |
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' |
||||||
|
} |
||||||
|
} |
||||||
|
if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { |
||||||
|
sourceSets.main { |
||||||
|
jniLibs.srcDir 'libs' |
||||||
|
} |
||||||
|
externalNativeBuild { |
||||||
|
ndkBuild { |
||||||
|
path 'jni/Android.mk' |
||||||
|
} |
||||||
|
// cmake { |
||||||
|
// path 'jni/CMakeLists.txt' |
||||||
|
// } |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
lintOptions { |
||||||
|
abortOnError false |
||||||
|
} |
||||||
|
|
||||||
|
if (buildAsLibrary) { |
||||||
|
libraryVariants.all { variant -> |
||||||
|
variant.outputs.each { output -> |
||||||
|
def outputFile = output.outputFile |
||||||
|
if (outputFile != null && outputFile.name.endsWith(".aar")) { |
||||||
|
def fileName = "org.libsdl.app.aar"; |
||||||
|
output.outputFile = new File(outputFile.parent, fileName); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs') |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# Add project specific ProGuard rules here. |
||||||
|
# By default, the flags in this file are appended to flags specified |
||||||
|
# in [sdk]/tools/proguard/proguard-android.txt |
||||||
|
# You can edit the include path and order by changing the proguardFiles |
||||||
|
# directive in build.gradle. |
||||||
|
# |
||||||
|
# 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 *; |
||||||
|
#} |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<!-- Replace com.test.game with the identifier of your game below, e.g. |
||||||
|
com.gamemaker.game |
||||||
|
--> |
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
package="org.libsdl.app" |
||||||
|
android:versionCode="1" |
||||||
|
android:versionName="1.0" |
||||||
|
android:installLocation="auto"> |
||||||
|
|
||||||
|
<!-- OpenGL ES 2.0 --> |
||||||
|
<uses-feature android:glEsVersion="0x00020000" /> |
||||||
|
|
||||||
|
<!-- Touchscreen support --> |
||||||
|
<uses-feature |
||||||
|
android:name="android.hardware.touchscreen" |
||||||
|
android:required="false" /> |
||||||
|
|
||||||
|
<!-- Game controller support --> |
||||||
|
<uses-feature |
||||||
|
android:name="android.hardware.bluetooth" |
||||||
|
android:required="false" /> |
||||||
|
<uses-feature |
||||||
|
android:name="android.hardware.gamepad" |
||||||
|
android:required="false" /> |
||||||
|
<uses-feature |
||||||
|
android:name="android.hardware.usb.host" |
||||||
|
android:required="false" /> |
||||||
|
|
||||||
|
<!-- External mouse input events --> |
||||||
|
<uses-feature |
||||||
|
android:name="android.hardware.type.pc" |
||||||
|
android:required="false" /> |
||||||
|
|
||||||
|
<!-- Audio recording support --> |
||||||
|
<!-- if you want to capture audio, uncomment this. --> |
||||||
|
<!-- <uses-feature |
||||||
|
android:name="android.hardware.microphone" |
||||||
|
android:required="false" /> --> |
||||||
|
|
||||||
|
<!-- Allow writing to external storage --> |
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
||||||
|
<!-- Allow access to Bluetooth devices --> |
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" /> |
||||||
|
<!-- Allow access to the vibrator --> |
||||||
|
<uses-permission android:name="android.permission.VIBRATE" /> |
||||||
|
|
||||||
|
<!-- if you want to capture audio, uncomment this. --> |
||||||
|
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> --> |
||||||
|
|
||||||
|
<!-- Create a Java class extending SDLActivity and place it in a |
||||||
|
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java |
||||||
|
|
||||||
|
then replace "SDLActivity" with the name of your class (e.g. "MyGame") |
||||||
|
in the XML below. |
||||||
|
|
||||||
|
An example Java class can be found in README-android.md |
||||||
|
--> |
||||||
|
<application android:label="@string/app_name" |
||||||
|
android:icon="@mipmap/ic_launcher" |
||||||
|
android:allowBackup="true" |
||||||
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" |
||||||
|
android:hardwareAccelerated="true" > |
||||||
|
|
||||||
|
<!-- Example of setting SDL hints from AndroidManifest.xml: |
||||||
|
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/> |
||||||
|
--> |
||||||
|
|
||||||
|
<activity android:name="SDLActivity" |
||||||
|
android:label="@string/app_name" |
||||||
|
android:alwaysRetainTaskState="true" |
||||||
|
android:launchMode="singleInstance" |
||||||
|
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" |
||||||
|
> |
||||||
|
<intent-filter> |
||||||
|
<action android:name="android.intent.action.MAIN" /> |
||||||
|
<category android:name="android.intent.category.LAUNCHER" /> |
||||||
|
</intent-filter> |
||||||
|
<!-- Drop file event --> |
||||||
|
<!-- |
||||||
|
<intent-filter> |
||||||
|
<action android:name="android.intent.action.VIEW" /> |
||||||
|
<category android:name="android.intent.category.DEFAULT" /> |
||||||
|
<data android:mimeType="*/*" /> |
||||||
|
</intent-filter> |
||||||
|
--> |
||||||
|
</activity> |
||||||
|
</application> |
||||||
|
|
||||||
|
</manifest> |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.hardware.usb.UsbDevice; |
||||||
|
|
||||||
|
interface HIDDevice |
||||||
|
{ |
||||||
|
public int getId(); |
||||||
|
public int getVendorId(); |
||||||
|
public int getProductId(); |
||||||
|
public String getSerialNumber(); |
||||||
|
public int getVersion(); |
||||||
|
public String getManufacturerName(); |
||||||
|
public String getProductName(); |
||||||
|
public UsbDevice getDevice(); |
||||||
|
public boolean open(); |
||||||
|
public int sendFeatureReport(byte[] report); |
||||||
|
public int sendOutputReport(byte[] report); |
||||||
|
public boolean getFeatureReport(byte[] report); |
||||||
|
public void setFrozen(boolean frozen); |
||||||
|
public void close(); |
||||||
|
public void shutdown(); |
||||||
|
} |
||||||
@ -0,0 +1,650 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.bluetooth.BluetoothDevice; |
||||||
|
import android.bluetooth.BluetoothGatt; |
||||||
|
import android.bluetooth.BluetoothGattCallback; |
||||||
|
import android.bluetooth.BluetoothGattCharacteristic; |
||||||
|
import android.bluetooth.BluetoothGattDescriptor; |
||||||
|
import android.bluetooth.BluetoothManager; |
||||||
|
import android.bluetooth.BluetoothProfile; |
||||||
|
import android.bluetooth.BluetoothGattService; |
||||||
|
import android.hardware.usb.UsbDevice; |
||||||
|
import android.os.Handler; |
||||||
|
import android.os.Looper; |
||||||
|
import android.util.Log; |
||||||
|
import android.os.*; |
||||||
|
|
||||||
|
//import com.android.internal.util.HexDump;
|
||||||
|
|
||||||
|
import java.lang.Runnable; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.LinkedList; |
||||||
|
import java.util.UUID; |
||||||
|
|
||||||
|
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { |
||||||
|
|
||||||
|
private static final String TAG = "hidapi"; |
||||||
|
private HIDDeviceManager mManager; |
||||||
|
private BluetoothDevice mDevice; |
||||||
|
private int mDeviceId; |
||||||
|
private BluetoothGatt mGatt; |
||||||
|
private boolean mIsRegistered = false; |
||||||
|
private boolean mIsConnected = false; |
||||||
|
private boolean mIsChromebook = false; |
||||||
|
private boolean mIsReconnecting = false; |
||||||
|
private boolean mFrozen = false; |
||||||
|
private LinkedList<GattOperation> mOperations; |
||||||
|
GattOperation mCurrentOperation = null; |
||||||
|
private Handler mHandler; |
||||||
|
|
||||||
|
private static final int TRANSPORT_AUTO = 0; |
||||||
|
private static final int TRANSPORT_BREDR = 1; |
||||||
|
private static final int TRANSPORT_LE = 2; |
||||||
|
|
||||||
|
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; |
||||||
|
|
||||||
|
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); |
||||||
|
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); |
||||||
|
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); |
||||||
|
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; |
||||||
|
|
||||||
|
static class GattOperation { |
||||||
|
private enum Operation { |
||||||
|
CHR_READ, |
||||||
|
CHR_WRITE, |
||||||
|
ENABLE_NOTIFICATION |
||||||
|
} |
||||||
|
|
||||||
|
Operation mOp; |
||||||
|
UUID mUuid; |
||||||
|
byte[] mValue; |
||||||
|
BluetoothGatt mGatt; |
||||||
|
boolean mResult = true; |
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { |
||||||
|
mGatt = gatt; |
||||||
|
mOp = operation; |
||||||
|
mUuid = uuid; |
||||||
|
} |
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { |
||||||
|
mGatt = gatt; |
||||||
|
mOp = operation; |
||||||
|
mUuid = uuid; |
||||||
|
mValue = value; |
||||||
|
} |
||||||
|
|
||||||
|
public void run() { |
||||||
|
// This is executed in main thread
|
||||||
|
BluetoothGattCharacteristic chr; |
||||||
|
|
||||||
|
switch (mOp) { |
||||||
|
case CHR_READ: |
||||||
|
chr = getCharacteristic(mUuid); |
||||||
|
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||||
|
if (!mGatt.readCharacteristic(chr)) { |
||||||
|
Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); |
||||||
|
mResult = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
mResult = true; |
||||||
|
break; |
||||||
|
case CHR_WRITE: |
||||||
|
chr = getCharacteristic(mUuid); |
||||||
|
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||||
|
chr.setValue(mValue); |
||||||
|
if (!mGatt.writeCharacteristic(chr)) { |
||||||
|
Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); |
||||||
|
mResult = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
mResult = true; |
||||||
|
break; |
||||||
|
case ENABLE_NOTIFICATION: |
||||||
|
chr = getCharacteristic(mUuid); |
||||||
|
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||||
|
if (chr != null) { |
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); |
||||||
|
if (cccd != null) { |
||||||
|
int properties = chr.getProperties(); |
||||||
|
byte[] value; |
||||||
|
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { |
||||||
|
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; |
||||||
|
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { |
||||||
|
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; |
||||||
|
} else { |
||||||
|
Log.e(TAG, "Unable to start notifications on input characteristic"); |
||||||
|
mResult = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
mGatt.setCharacteristicNotification(chr, true); |
||||||
|
cccd.setValue(value); |
||||||
|
if (!mGatt.writeDescriptor(cccd)) { |
||||||
|
Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); |
||||||
|
mResult = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
mResult = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public boolean finish() { |
||||||
|
return mResult; |
||||||
|
} |
||||||
|
|
||||||
|
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { |
||||||
|
BluetoothGattService valveService = mGatt.getService(steamControllerService); |
||||||
|
if (valveService == null) |
||||||
|
return null; |
||||||
|
return valveService.getCharacteristic(uuid); |
||||||
|
} |
||||||
|
|
||||||
|
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { |
||||||
|
return new GattOperation(gatt, Operation.CHR_READ, uuid); |
||||||
|
} |
||||||
|
|
||||||
|
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { |
||||||
|
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); |
||||||
|
} |
||||||
|
|
||||||
|
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { |
||||||
|
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { |
||||||
|
mManager = manager; |
||||||
|
mDevice = device; |
||||||
|
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); |
||||||
|
mIsRegistered = false; |
||||||
|
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); |
||||||
|
mOperations = new LinkedList<GattOperation>(); |
||||||
|
mHandler = new Handler(Looper.getMainLooper()); |
||||||
|
|
||||||
|
mGatt = connectGatt(); |
||||||
|
// final HIDDeviceBLESteamController finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.checkConnectionForChromebookIssue();
|
||||||
|
// }
|
||||||
|
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||||
|
} |
||||||
|
|
||||||
|
public String getIdentifier() { |
||||||
|
return String.format("SteamController.%s", mDevice.getAddress()); |
||||||
|
} |
||||||
|
|
||||||
|
public BluetoothGatt getGatt() { |
||||||
|
return mGatt; |
||||||
|
} |
||||||
|
|
||||||
|
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||||
|
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||||
|
private BluetoothGatt connectGatt(boolean managed) { |
||||||
|
if (Build.VERSION.SDK_INT >= 23) { |
||||||
|
try { |
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); |
||||||
|
} catch (Exception e) { |
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this); |
||||||
|
} |
||||||
|
} else { |
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private BluetoothGatt connectGatt() { |
||||||
|
return connectGatt(false); |
||||||
|
} |
||||||
|
|
||||||
|
protected int getConnectionState() { |
||||||
|
|
||||||
|
Context context = mManager.getContext(); |
||||||
|
if (context == null) { |
||||||
|
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED; |
||||||
|
} |
||||||
|
|
||||||
|
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); |
||||||
|
if (btManager == null) { |
||||||
|
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||||
|
// we instantiate a device to start with?
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED; |
||||||
|
} |
||||||
|
|
||||||
|
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); |
||||||
|
} |
||||||
|
|
||||||
|
public void reconnect() { |
||||||
|
|
||||||
|
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { |
||||||
|
mGatt.disconnect(); |
||||||
|
mGatt = connectGatt(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
protected void checkConnectionForChromebookIssue() { |
||||||
|
if (!mIsChromebook) { |
||||||
|
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||||
|
// over and over.
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
int connectionState = getConnectionState(); |
||||||
|
|
||||||
|
switch (connectionState) { |
||||||
|
case BluetoothProfile.STATE_CONNECTED: |
||||||
|
if (!mIsConnected) { |
||||||
|
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||||
|
// to try to recover.
|
||||||
|
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); |
||||||
|
mIsReconnecting = true; |
||||||
|
mGatt.disconnect(); |
||||||
|
mGatt = connectGatt(false); |
||||||
|
break; |
||||||
|
} |
||||||
|
else if (!isRegistered()) { |
||||||
|
if (mGatt.getServices().size() > 0) { |
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); |
||||||
|
probeService(this); |
||||||
|
} |
||||||
|
else { |
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); |
||||||
|
mIsReconnecting = true; |
||||||
|
mGatt.disconnect(); |
||||||
|
mGatt = connectGatt(false); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); |
||||||
|
return; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
case BluetoothProfile.STATE_DISCONNECTED: |
||||||
|
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); |
||||||
|
|
||||||
|
mIsReconnecting = true; |
||||||
|
mGatt.disconnect(); |
||||||
|
mGatt = connectGatt(false); |
||||||
|
break; |
||||||
|
|
||||||
|
case BluetoothProfile.STATE_CONNECTING: |
||||||
|
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
final HIDDeviceBLESteamController finalThis = this; |
||||||
|
mHandler.postDelayed(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
finalThis.checkConnectionForChromebookIssue(); |
||||||
|
} |
||||||
|
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isRegistered() { |
||||||
|
return mIsRegistered; |
||||||
|
} |
||||||
|
|
||||||
|
private void setRegistered() { |
||||||
|
mIsRegistered = true; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean probeService(HIDDeviceBLESteamController controller) { |
||||||
|
|
||||||
|
if (isRegistered()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (!mIsConnected) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
Log.v(TAG, "probeService controller=" + controller); |
||||||
|
|
||||||
|
for (BluetoothGattService service : mGatt.getServices()) { |
||||||
|
if (service.getUuid().equals(steamControllerService)) { |
||||||
|
Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); |
||||||
|
|
||||||
|
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { |
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) { |
||||||
|
Log.v(TAG, "Found input characteristic"); |
||||||
|
// Start notifications
|
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); |
||||||
|
if (cccd != null) { |
||||||
|
enableNotification(chr.getUuid()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { |
||||||
|
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); |
||||||
|
mIsConnected = false; |
||||||
|
mIsReconnecting = true; |
||||||
|
mGatt.disconnect(); |
||||||
|
mGatt = connectGatt(false); |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void finishCurrentGattOperation() { |
||||||
|
GattOperation op = null; |
||||||
|
synchronized (mOperations) { |
||||||
|
if (mCurrentOperation != null) { |
||||||
|
op = mCurrentOperation; |
||||||
|
mCurrentOperation = null; |
||||||
|
} |
||||||
|
} |
||||||
|
if (op != null) { |
||||||
|
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||||
|
|
||||||
|
// Our operation failed, let's add it back to the beginning of our queue.
|
||||||
|
if (!result) { |
||||||
|
mOperations.addFirst(op); |
||||||
|
} |
||||||
|
} |
||||||
|
executeNextGattOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
private void executeNextGattOperation() { |
||||||
|
synchronized (mOperations) { |
||||||
|
if (mCurrentOperation != null) |
||||||
|
return; |
||||||
|
|
||||||
|
if (mOperations.isEmpty()) |
||||||
|
return; |
||||||
|
|
||||||
|
mCurrentOperation = mOperations.removeFirst(); |
||||||
|
} |
||||||
|
|
||||||
|
// Run in main thread
|
||||||
|
mHandler.post(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
synchronized (mOperations) { |
||||||
|
if (mCurrentOperation == null) { |
||||||
|
Log.e(TAG, "Current operation null in executor?"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
mCurrentOperation.run(); |
||||||
|
// now wait for the GATT callback and when it comes, finish this operation
|
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void queueGattOperation(GattOperation op) { |
||||||
|
synchronized (mOperations) { |
||||||
|
mOperations.add(op); |
||||||
|
} |
||||||
|
executeNextGattOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
private void enableNotification(UUID chrUuid) { |
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); |
||||||
|
queueGattOperation(op); |
||||||
|
} |
||||||
|
|
||||||
|
public void writeCharacteristic(UUID uuid, byte[] value) { |
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); |
||||||
|
queueGattOperation(op); |
||||||
|
} |
||||||
|
|
||||||
|
public void readCharacteristic(UUID uuid) { |
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); |
||||||
|
queueGattOperation(op); |
||||||
|
} |
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////// BluetoothGattCallback overridden methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { |
||||||
|
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||||
|
mIsReconnecting = false; |
||||||
|
if (newState == 2) { |
||||||
|
mIsConnected = true; |
||||||
|
// Run directly, without GattOperation
|
||||||
|
if (!isRegistered()) { |
||||||
|
mHandler.post(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
mGatt.discoverServices(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
else if (newState == 0) { |
||||||
|
mIsConnected = false; |
||||||
|
} |
||||||
|
|
||||||
|
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||||
|
} |
||||||
|
|
||||||
|
public void onServicesDiscovered(BluetoothGatt gatt, int status) { |
||||||
|
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||||
|
if (status == 0) { |
||||||
|
if (gatt.getServices().size() == 0) { |
||||||
|
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); |
||||||
|
mIsReconnecting = true; |
||||||
|
mIsConnected = false; |
||||||
|
gatt.disconnect(); |
||||||
|
mGatt = connectGatt(false); |
||||||
|
} |
||||||
|
else { |
||||||
|
probeService(this); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { |
||||||
|
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { |
||||||
|
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); |
||||||
|
} |
||||||
|
|
||||||
|
finishCurrentGattOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { |
||||||
|
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic)) { |
||||||
|
// Only register controller with the native side once it has been fully configured
|
||||||
|
if (!isRegistered()) { |
||||||
|
Log.v(TAG, "Registering Steam Controller with ID: " + getId()); |
||||||
|
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); |
||||||
|
setRegistered(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
finishCurrentGattOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { |
||||||
|
// Enable this for verbose logging of controller input reports
|
||||||
|
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { |
||||||
|
mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { |
||||||
|
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||||
|
} |
||||||
|
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { |
||||||
|
BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); |
||||||
|
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||||
|
|
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) { |
||||||
|
boolean hasWrittenInputDescriptor = true; |
||||||
|
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); |
||||||
|
if (reportChr != null) { |
||||||
|
Log.v(TAG, "Writing report characteristic to enter valve mode"); |
||||||
|
reportChr.setValue(enterValveMode); |
||||||
|
gatt.writeCharacteristic(reportChr); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
finishCurrentGattOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { |
||||||
|
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||||
|
} |
||||||
|
|
||||||
|
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { |
||||||
|
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||||
|
} |
||||||
|
|
||||||
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { |
||||||
|
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||||
|
} |
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////// Public API
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override |
||||||
|
public int getId() { |
||||||
|
return mDeviceId; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVendorId() { |
||||||
|
// Valve Corporation
|
||||||
|
final int VALVE_USB_VID = 0x28DE; |
||||||
|
return VALVE_USB_VID; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getProductId() { |
||||||
|
// We don't have an easy way to query from the Bluetooth device, but we know what it is
|
||||||
|
final int D0G_BLE2_PID = 0x1106; |
||||||
|
return D0G_BLE2_PID; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getSerialNumber() { |
||||||
|
// This will be read later via feature report by Steam
|
||||||
|
return "12345"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVersion() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getManufacturerName() { |
||||||
|
return "Valve Corporation"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getProductName() { |
||||||
|
return "Steam Controller"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public UsbDevice getDevice() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean open() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int sendFeatureReport(byte[] report) { |
||||||
|
if (!isRegistered()) { |
||||||
|
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); |
||||||
|
if (mIsConnected) { |
||||||
|
probeService(this); |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
// We need to skip the first byte, as that doesn't go over the air
|
||||||
|
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); |
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||||
|
writeCharacteristic(reportCharacteristic, actual_report); |
||||||
|
return report.length; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int sendOutputReport(byte[] report) { |
||||||
|
if (!isRegistered()) { |
||||||
|
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); |
||||||
|
if (mIsConnected) { |
||||||
|
probeService(this); |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
|
||||||
|
writeCharacteristic(reportCharacteristic, report); |
||||||
|
return report.length; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean getFeatureReport(byte[] report) { |
||||||
|
if (!isRegistered()) { |
||||||
|
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); |
||||||
|
if (mIsConnected) { |
||||||
|
probeService(this); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
//Log.v(TAG, "getFeatureReport");
|
||||||
|
readCharacteristic(reportCharacteristic); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setFrozen(boolean frozen) { |
||||||
|
mFrozen = frozen; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void shutdown() { |
||||||
|
close(); |
||||||
|
|
||||||
|
BluetoothGatt g = mGatt; |
||||||
|
if (g != null) { |
||||||
|
g.disconnect(); |
||||||
|
g.close(); |
||||||
|
mGatt = null; |
||||||
|
} |
||||||
|
mManager = null; |
||||||
|
mIsRegistered = false; |
||||||
|
mIsConnected = false; |
||||||
|
mOperations.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,678 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.app.AlertDialog; |
||||||
|
import android.app.PendingIntent; |
||||||
|
import android.bluetooth.BluetoothAdapter; |
||||||
|
import android.bluetooth.BluetoothDevice; |
||||||
|
import android.bluetooth.BluetoothManager; |
||||||
|
import android.bluetooth.BluetoothProfile; |
||||||
|
import android.util.Log; |
||||||
|
import android.content.BroadcastReceiver; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.DialogInterface; |
||||||
|
import android.content.Intent; |
||||||
|
import android.content.IntentFilter; |
||||||
|
import android.content.SharedPreferences; |
||||||
|
import android.content.pm.PackageManager; |
||||||
|
import android.hardware.usb.*; |
||||||
|
import android.os.Handler; |
||||||
|
import android.os.Looper; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class HIDDeviceManager { |
||||||
|
private static final String TAG = "hidapi"; |
||||||
|
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; |
||||||
|
|
||||||
|
private static HIDDeviceManager sManager; |
||||||
|
private static int sManagerRefCount = 0; |
||||||
|
|
||||||
|
public static HIDDeviceManager acquire(Context context) { |
||||||
|
if (sManagerRefCount == 0) { |
||||||
|
sManager = new HIDDeviceManager(context); |
||||||
|
} |
||||||
|
++sManagerRefCount; |
||||||
|
return sManager; |
||||||
|
} |
||||||
|
|
||||||
|
public static void release(HIDDeviceManager manager) { |
||||||
|
if (manager == sManager) { |
||||||
|
--sManagerRefCount; |
||||||
|
if (sManagerRefCount == 0) { |
||||||
|
sManager.close(); |
||||||
|
sManager = null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Context mContext; |
||||||
|
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>(); |
||||||
|
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>(); |
||||||
|
private int mNextDeviceId = 0; |
||||||
|
private SharedPreferences mSharedPreferences = null; |
||||||
|
private boolean mIsChromebook = false; |
||||||
|
private UsbManager mUsbManager; |
||||||
|
private Handler mHandler; |
||||||
|
private BluetoothManager mBluetoothManager; |
||||||
|
private List<BluetoothDevice> mLastBluetoothDevices; |
||||||
|
|
||||||
|
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { |
||||||
|
@Override |
||||||
|
public void onReceive(Context context, Intent intent) { |
||||||
|
String action = intent.getAction(); |
||||||
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { |
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); |
||||||
|
handleUsbDeviceAttached(usbDevice); |
||||||
|
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { |
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); |
||||||
|
handleUsbDeviceDetached(usbDevice); |
||||||
|
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { |
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); |
||||||
|
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { |
||||||
|
@Override |
||||||
|
public void onReceive(Context context, Intent intent) { |
||||||
|
String action = intent.getAction(); |
||||||
|
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { |
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
||||||
|
Log.d(TAG, "Bluetooth device connected: " + device); |
||||||
|
|
||||||
|
if (isSteamController(device)) { |
||||||
|
connectBluetoothDevice(device); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { |
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
||||||
|
Log.d(TAG, "Bluetooth device disconnected: " + device); |
||||||
|
|
||||||
|
disconnectBluetoothDevice(device); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private HIDDeviceManager(final Context context) { |
||||||
|
mContext = context; |
||||||
|
|
||||||
|
// Make sure we have the HIDAPI library loaded with the native functions
|
||||||
|
try { |
||||||
|
SDL.loadLibrary("hidapi"); |
||||||
|
} catch (Throwable e) { |
||||||
|
Log.w(TAG, "Couldn't load hidapi: " + e.toString()); |
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context); |
||||||
|
builder.setCancelable(false); |
||||||
|
builder.setTitle("SDL HIDAPI Error"); |
||||||
|
builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage()); |
||||||
|
builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(DialogInterface dialog, int which) { |
||||||
|
try { |
||||||
|
// If our context is an activity, exit rather than crashing when we can't
|
||||||
|
// call our native functions.
|
||||||
|
Activity activity = (Activity)context; |
||||||
|
|
||||||
|
activity.finish(); |
||||||
|
} |
||||||
|
catch (ClassCastException cce) { |
||||||
|
// Context wasn't an activity, there's nothing we can do. Give up and return.
|
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
builder.show(); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
HIDDeviceRegisterCallback(); |
||||||
|
|
||||||
|
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); |
||||||
|
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); |
||||||
|
|
||||||
|
// if (shouldClear) {
|
||||||
|
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||||
|
// spedit.clear();
|
||||||
|
// spedit.commit();
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
{ |
||||||
|
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); |
||||||
|
} |
||||||
|
|
||||||
|
initializeUSB(); |
||||||
|
initializeBluetooth(); |
||||||
|
} |
||||||
|
|
||||||
|
public Context getContext() { |
||||||
|
return mContext; |
||||||
|
} |
||||||
|
|
||||||
|
public int getDeviceIDForIdentifier(String identifier) { |
||||||
|
SharedPreferences.Editor spedit = mSharedPreferences.edit(); |
||||||
|
|
||||||
|
int result = mSharedPreferences.getInt(identifier, 0); |
||||||
|
if (result == 0) { |
||||||
|
result = mNextDeviceId++; |
||||||
|
spedit.putInt("next_device_id", mNextDeviceId); |
||||||
|
} |
||||||
|
|
||||||
|
spedit.putInt(identifier, result); |
||||||
|
spedit.commit(); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private void initializeUSB() { |
||||||
|
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); |
||||||
|
|
||||||
|
/* |
||||||
|
// Logging
|
||||||
|
for (UsbDevice device : mUsbManager.getDeviceList().values()) { |
||||||
|
Log.i(TAG,"Path: " + device.getDeviceName()); |
||||||
|
Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); |
||||||
|
Log.i(TAG,"Product: " + device.getProductName()); |
||||||
|
Log.i(TAG,"ID: " + device.getDeviceId()); |
||||||
|
Log.i(TAG,"Class: " + device.getDeviceClass()); |
||||||
|
Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); |
||||||
|
Log.i(TAG,"Vendor ID " + device.getVendorId()); |
||||||
|
Log.i(TAG,"Product ID: " + device.getProductId()); |
||||||
|
Log.i(TAG,"Interface count: " + device.getInterfaceCount()); |
||||||
|
Log.i(TAG,"---------------------------------------"); |
||||||
|
|
||||||
|
// Get interface details
|
||||||
|
for (int index = 0; index < device.getInterfaceCount(); index++) { |
||||||
|
UsbInterface mUsbInterface = device.getInterface(index); |
||||||
|
Log.i(TAG," ***** *****"); |
||||||
|
Log.i(TAG," Interface index: " + index); |
||||||
|
Log.i(TAG," Interface ID: " + mUsbInterface.getId()); |
||||||
|
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); |
||||||
|
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); |
||||||
|
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); |
||||||
|
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); |
||||||
|
|
||||||
|
// Get endpoint details
|
||||||
|
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) |
||||||
|
{ |
||||||
|
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); |
||||||
|
Log.i(TAG," ++++ ++++ ++++"); |
||||||
|
Log.i(TAG," Endpoint index: " + epi); |
||||||
|
Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); |
||||||
|
Log.i(TAG," Direction: " + mEndpoint.getDirection()); |
||||||
|
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); |
||||||
|
Log.i(TAG," Interval: " + mEndpoint.getInterval()); |
||||||
|
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); |
||||||
|
Log.i(TAG," Type: " + mEndpoint.getType()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Log.i(TAG," No more devices connected."); |
||||||
|
*/ |
||||||
|
|
||||||
|
// Register for USB broadcasts and permission completions
|
||||||
|
IntentFilter filter = new IntentFilter(); |
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); |
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); |
||||||
|
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); |
||||||
|
mContext.registerReceiver(mUsbBroadcast, filter); |
||||||
|
|
||||||
|
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { |
||||||
|
handleUsbDeviceAttached(usbDevice); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
UsbManager getUSBManager() { |
||||||
|
return mUsbManager; |
||||||
|
} |
||||||
|
|
||||||
|
private void shutdownUSB() { |
||||||
|
try { |
||||||
|
mContext.unregisterReceiver(mUsbBroadcast); |
||||||
|
} catch (Exception e) { |
||||||
|
// We may not have registered, that's okay
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { |
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { |
||||||
|
final int XB360_IFACE_SUBCLASS = 93; |
||||||
|
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||||
|
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||||
|
final int[] SUPPORTED_VENDORS = { |
||||||
|
0x0079, // GPD Win 2
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
|
0x11c9, // Nacon
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x24c6, // PowerA
|
||||||
|
}; |
||||||
|
|
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && |
||||||
|
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && |
||||||
|
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || |
||||||
|
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { |
||||||
|
int vendor_id = usbDevice.getVendorId(); |
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) { |
||||||
|
if (vendor_id == supportedVid) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { |
||||||
|
final int XB1_IFACE_SUBCLASS = 71; |
||||||
|
final int XB1_IFACE_PROTOCOL = 208; |
||||||
|
final int[] SUPPORTED_VENDORS = { |
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x2e24, // Hyperkin
|
||||||
|
}; |
||||||
|
|
||||||
|
if (usbInterface.getId() == 0 && |
||||||
|
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && |
||||||
|
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && |
||||||
|
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { |
||||||
|
int vendor_id = usbDevice.getVendorId(); |
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) { |
||||||
|
if (vendor_id == supportedVid) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private void handleUsbDeviceAttached(UsbDevice usbDevice) { |
||||||
|
connectHIDDeviceUSB(usbDevice); |
||||||
|
} |
||||||
|
|
||||||
|
private void handleUsbDeviceDetached(UsbDevice usbDevice) { |
||||||
|
List<Integer> devices = new ArrayList<Integer>(); |
||||||
|
for (HIDDevice device : mDevicesById.values()) { |
||||||
|
if (usbDevice.equals(device.getDevice())) { |
||||||
|
devices.add(device.getId()); |
||||||
|
} |
||||||
|
} |
||||||
|
for (int id : devices) { |
||||||
|
HIDDevice device = mDevicesById.get(id); |
||||||
|
mDevicesById.remove(id); |
||||||
|
device.shutdown(); |
||||||
|
HIDDeviceDisconnected(id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { |
||||||
|
for (HIDDevice device : mDevicesById.values()) { |
||||||
|
if (usbDevice.equals(device.getDevice())) { |
||||||
|
boolean opened = false; |
||||||
|
if (permission_granted) { |
||||||
|
opened = device.open(); |
||||||
|
} |
||||||
|
HIDDeviceOpenResult(device.getId(), opened); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void connectHIDDeviceUSB(UsbDevice usbDevice) { |
||||||
|
synchronized (this) { |
||||||
|
int interface_mask = 0; |
||||||
|
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { |
||||||
|
UsbInterface usbInterface = usbDevice.getInterface(interface_index); |
||||||
|
if (isHIDDeviceInterface(usbDevice, usbInterface)) { |
||||||
|
// Check to see if we've already added this interface
|
||||||
|
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||||
|
int interface_id = usbInterface.getId(); |
||||||
|
if ((interface_mask & (1 << interface_id)) != 0) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
interface_mask |= (1 << interface_id); |
||||||
|
|
||||||
|
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); |
||||||
|
int id = device.getId(); |
||||||
|
mDevicesById.put(id, device); |
||||||
|
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void initializeBluetooth() { |
||||||
|
Log.d(TAG, "Initializing Bluetooth"); |
||||||
|
|
||||||
|
if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { |
||||||
|
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||||
|
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); |
||||||
|
if (mBluetoothManager == null) { |
||||||
|
// This device doesn't support Bluetooth.
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); |
||||||
|
if (btAdapter == null) { |
||||||
|
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Get our bonded devices.
|
||||||
|
for (BluetoothDevice device : btAdapter.getBondedDevices()) { |
||||||
|
|
||||||
|
Log.d(TAG, "Bluetooth device available: " + device); |
||||||
|
if (isSteamController(device)) { |
||||||
|
connectBluetoothDevice(device); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||||
|
IntentFilter filter = new IntentFilter(); |
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); |
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); |
||||||
|
mContext.registerReceiver(mBluetoothBroadcast, filter); |
||||||
|
|
||||||
|
if (mIsChromebook) { |
||||||
|
mHandler = new Handler(Looper.getMainLooper()); |
||||||
|
mLastBluetoothDevices = new ArrayList<BluetoothDevice>(); |
||||||
|
|
||||||
|
// final HIDDeviceManager finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.chromebookConnectionHandler();
|
||||||
|
// }
|
||||||
|
// }, 5000);
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void shutdownBluetooth() { |
||||||
|
try { |
||||||
|
mContext.unregisterReceiver(mBluetoothBroadcast); |
||||||
|
} catch (Exception e) { |
||||||
|
// We may not have registered, that's okay
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||||
|
// This function provides a sort of dummy version of that, watching for changes in the
|
||||||
|
// connected devices and attempting to add controllers as things change.
|
||||||
|
public void chromebookConnectionHandler() { |
||||||
|
if (!mIsChromebook) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>(); |
||||||
|
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>(); |
||||||
|
|
||||||
|
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); |
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : currentConnected) { |
||||||
|
if (!mLastBluetoothDevices.contains(bluetoothDevice)) { |
||||||
|
connected.add(bluetoothDevice); |
||||||
|
} |
||||||
|
} |
||||||
|
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { |
||||||
|
if (!currentConnected.contains(bluetoothDevice)) { |
||||||
|
disconnected.add(bluetoothDevice); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mLastBluetoothDevices = currentConnected; |
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : disconnected) { |
||||||
|
disconnectBluetoothDevice(bluetoothDevice); |
||||||
|
} |
||||||
|
for (BluetoothDevice bluetoothDevice : connected) { |
||||||
|
connectBluetoothDevice(bluetoothDevice); |
||||||
|
} |
||||||
|
|
||||||
|
final HIDDeviceManager finalThis = this; |
||||||
|
mHandler.postDelayed(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
finalThis.chromebookConnectionHandler(); |
||||||
|
} |
||||||
|
}, 10000); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { |
||||||
|
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); |
||||||
|
synchronized (this) { |
||||||
|
if (mBluetoothDevices.containsKey(bluetoothDevice)) { |
||||||
|
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); |
||||||
|
|
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); |
||||||
|
device.reconnect(); |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); |
||||||
|
int id = device.getId(); |
||||||
|
mBluetoothDevices.put(bluetoothDevice, device); |
||||||
|
mDevicesById.put(id, device); |
||||||
|
|
||||||
|
// The Steam Controller will mark itself connected once initialization is complete
|
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { |
||||||
|
synchronized (this) { |
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); |
||||||
|
if (device == null) |
||||||
|
return; |
||||||
|
|
||||||
|
int id = device.getId(); |
||||||
|
mBluetoothDevices.remove(bluetoothDevice); |
||||||
|
mDevicesById.remove(id); |
||||||
|
device.shutdown(); |
||||||
|
HIDDeviceDisconnected(id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isSteamController(BluetoothDevice bluetoothDevice) { |
||||||
|
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||||
|
if (bluetoothDevice == null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// If the device has no local name, we really don't want to try an equality check against it.
|
||||||
|
if (bluetoothDevice.getName() == null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); |
||||||
|
} |
||||||
|
|
||||||
|
private void close() { |
||||||
|
shutdownUSB(); |
||||||
|
shutdownBluetooth(); |
||||||
|
synchronized (this) { |
||||||
|
for (HIDDevice device : mDevicesById.values()) { |
||||||
|
device.shutdown(); |
||||||
|
} |
||||||
|
mDevicesById.clear(); |
||||||
|
mBluetoothDevices.clear(); |
||||||
|
HIDDeviceReleaseCallback(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void setFrozen(boolean frozen) { |
||||||
|
synchronized (this) { |
||||||
|
for (HIDDevice device : mDevicesById.values()) { |
||||||
|
device.setFrozen(frozen); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private HIDDevice getDevice(int id) { |
||||||
|
synchronized (this) { |
||||||
|
HIDDevice result = mDevicesById.get(id); |
||||||
|
if (result == null) { |
||||||
|
Log.v(TAG, "No device for id: " + id); |
||||||
|
Log.v(TAG, "Available devices: " + mDevicesById.keySet()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////// JNI interface functions
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public boolean openDevice(int deviceID) { |
||||||
|
Log.v(TAG, "openDevice deviceID=" + deviceID); |
||||||
|
HIDDevice device = getDevice(deviceID); |
||||||
|
if (device == null) { |
||||||
|
HIDDeviceDisconnected(deviceID); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Look to see if this is a USB device and we have permission to access it
|
||||||
|
UsbDevice usbDevice = device.getDevice(); |
||||||
|
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { |
||||||
|
HIDDeviceOpenPending(deviceID); |
||||||
|
try { |
||||||
|
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0)); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); |
||||||
|
HIDDeviceOpenResult(deviceID, false); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
return device.open(); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public int sendOutputReport(int deviceID, byte[] report) { |
||||||
|
try { |
||||||
|
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device; |
||||||
|
device = getDevice(deviceID); |
||||||
|
if (device == null) { |
||||||
|
HIDDeviceDisconnected(deviceID); |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
return device.sendOutputReport(report); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
public int sendFeatureReport(int deviceID, byte[] report) { |
||||||
|
try { |
||||||
|
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device; |
||||||
|
device = getDevice(deviceID); |
||||||
|
if (device == null) { |
||||||
|
HIDDeviceDisconnected(deviceID); |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
return device.sendFeatureReport(report); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean getFeatureReport(int deviceID, byte[] report) { |
||||||
|
try { |
||||||
|
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
|
||||||
|
HIDDevice device; |
||||||
|
device = getDevice(deviceID); |
||||||
|
if (device == null) { |
||||||
|
HIDDeviceDisconnected(deviceID); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return device.getFeatureReport(report); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public void closeDevice(int deviceID) { |
||||||
|
try { |
||||||
|
Log.v(TAG, "closeDevice deviceID=" + deviceID); |
||||||
|
HIDDevice device; |
||||||
|
device = getDevice(deviceID); |
||||||
|
if (device == null) { |
||||||
|
HIDDeviceDisconnected(deviceID); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
device.close(); |
||||||
|
} catch (Exception e) { |
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////// Native methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private native void HIDDeviceRegisterCallback(); |
||||||
|
private native void HIDDeviceReleaseCallback(); |
||||||
|
|
||||||
|
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); |
||||||
|
native void HIDDeviceOpenPending(int deviceID); |
||||||
|
native void HIDDeviceOpenResult(int deviceID, boolean opened); |
||||||
|
native void HIDDeviceDisconnected(int deviceID); |
||||||
|
|
||||||
|
native void HIDDeviceInputReport(int deviceID, byte[] report); |
||||||
|
native void HIDDeviceFeatureReport(int deviceID, byte[] report); |
||||||
|
} |
||||||
@ -0,0 +1,309 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.hardware.usb.*; |
||||||
|
import android.os.Build; |
||||||
|
import android.util.Log; |
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
class HIDDeviceUSB implements HIDDevice { |
||||||
|
|
||||||
|
private static final String TAG = "hidapi"; |
||||||
|
|
||||||
|
protected HIDDeviceManager mManager; |
||||||
|
protected UsbDevice mDevice; |
||||||
|
protected int mInterfaceIndex; |
||||||
|
protected int mInterface; |
||||||
|
protected int mDeviceId; |
||||||
|
protected UsbDeviceConnection mConnection; |
||||||
|
protected UsbEndpoint mInputEndpoint; |
||||||
|
protected UsbEndpoint mOutputEndpoint; |
||||||
|
protected InputThread mInputThread; |
||||||
|
protected boolean mRunning; |
||||||
|
protected boolean mFrozen; |
||||||
|
|
||||||
|
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { |
||||||
|
mManager = manager; |
||||||
|
mDevice = usbDevice; |
||||||
|
mInterfaceIndex = interface_index; |
||||||
|
mInterface = mDevice.getInterface(mInterfaceIndex).getId(); |
||||||
|
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); |
||||||
|
mRunning = false; |
||||||
|
} |
||||||
|
|
||||||
|
public String getIdentifier() { |
||||||
|
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getId() { |
||||||
|
return mDeviceId; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVendorId() { |
||||||
|
return mDevice.getVendorId(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getProductId() { |
||||||
|
return mDevice.getProductId(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getSerialNumber() { |
||||||
|
String result = null; |
||||||
|
if (Build.VERSION.SDK_INT >= 21) { |
||||||
|
try { |
||||||
|
result = mDevice.getSerialNumber(); |
||||||
|
} |
||||||
|
catch (SecurityException exception) { |
||||||
|
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||||
|
} |
||||||
|
} |
||||||
|
if (result == null) { |
||||||
|
result = ""; |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVersion() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getManufacturerName() { |
||||||
|
String result = null; |
||||||
|
if (Build.VERSION.SDK_INT >= 21) { |
||||||
|
result = mDevice.getManufacturerName(); |
||||||
|
} |
||||||
|
if (result == null) { |
||||||
|
result = String.format("%x", getVendorId()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getProductName() { |
||||||
|
String result = null; |
||||||
|
if (Build.VERSION.SDK_INT >= 21) { |
||||||
|
result = mDevice.getProductName(); |
||||||
|
} |
||||||
|
if (result == null) { |
||||||
|
result = String.format("%x", getProductId()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public UsbDevice getDevice() { |
||||||
|
return mDevice; |
||||||
|
} |
||||||
|
|
||||||
|
public String getDeviceName() { |
||||||
|
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean open() { |
||||||
|
mConnection = mManager.getUSBManager().openDevice(mDevice); |
||||||
|
if (mConnection == null) { |
||||||
|
Log.w(TAG, "Unable to open USB device " + getDeviceName()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Force claim our interface
|
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex); |
||||||
|
if (!mConnection.claimInterface(iface, true)) { |
||||||
|
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); |
||||||
|
close(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
for (int j = 0; j < iface.getEndpointCount(); j++) { |
||||||
|
UsbEndpoint endpt = iface.getEndpoint(j); |
||||||
|
switch (endpt.getDirection()) { |
||||||
|
case UsbConstants.USB_DIR_IN: |
||||||
|
if (mInputEndpoint == null) { |
||||||
|
mInputEndpoint = endpt; |
||||||
|
} |
||||||
|
break; |
||||||
|
case UsbConstants.USB_DIR_OUT: |
||||||
|
if (mOutputEndpoint == null) { |
||||||
|
mOutputEndpoint = endpt; |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (mInputEndpoint == null || mOutputEndpoint == null) { |
||||||
|
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); |
||||||
|
close(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Start listening for input
|
||||||
|
mRunning = true; |
||||||
|
mInputThread = new InputThread(); |
||||||
|
mInputThread.start(); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int sendFeatureReport(byte[] report) { |
||||||
|
int res = -1; |
||||||
|
int offset = 0; |
||||||
|
int length = report.length; |
||||||
|
boolean skipped_report_id = false; |
||||||
|
byte report_number = report[0]; |
||||||
|
|
||||||
|
if (report_number == 0x0) { |
||||||
|
++offset; |
||||||
|
--length; |
||||||
|
skipped_report_id = true; |
||||||
|
} |
||||||
|
|
||||||
|
res = mConnection.controlTransfer( |
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, |
||||||
|
0x09/*HID set_report*/, |
||||||
|
(3/*HID feature*/ << 8) | report_number, |
||||||
|
mInterface, |
||||||
|
report, offset, length, |
||||||
|
1000/*timeout millis*/); |
||||||
|
|
||||||
|
if (res < 0) { |
||||||
|
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
if (skipped_report_id) { |
||||||
|
++length; |
||||||
|
} |
||||||
|
return length; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int sendOutputReport(byte[] report) { |
||||||
|
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); |
||||||
|
if (r != report.length) { |
||||||
|
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); |
||||||
|
} |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean getFeatureReport(byte[] report) { |
||||||
|
int res = -1; |
||||||
|
int offset = 0; |
||||||
|
int length = report.length; |
||||||
|
boolean skipped_report_id = false; |
||||||
|
byte report_number = report[0]; |
||||||
|
|
||||||
|
if (report_number == 0x0) { |
||||||
|
/* Offset the return buffer by 1, so that the report ID |
||||||
|
will remain in byte 0. */ |
||||||
|
++offset; |
||||||
|
--length; |
||||||
|
skipped_report_id = true; |
||||||
|
} |
||||||
|
|
||||||
|
res = mConnection.controlTransfer( |
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, |
||||||
|
0x01/*HID get_report*/, |
||||||
|
(3/*HID feature*/ << 8) | report_number, |
||||||
|
mInterface, |
||||||
|
report, offset, length, |
||||||
|
1000/*timeout millis*/); |
||||||
|
|
||||||
|
if (res < 0) { |
||||||
|
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (skipped_report_id) { |
||||||
|
++res; |
||||||
|
++length; |
||||||
|
} |
||||||
|
|
||||||
|
byte[] data; |
||||||
|
if (res == length) { |
||||||
|
data = report; |
||||||
|
} else { |
||||||
|
data = Arrays.copyOfRange(report, 0, res); |
||||||
|
} |
||||||
|
mManager.HIDDeviceFeatureReport(mDeviceId, data); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
mRunning = false; |
||||||
|
if (mInputThread != null) { |
||||||
|
while (mInputThread.isAlive()) { |
||||||
|
mInputThread.interrupt(); |
||||||
|
try { |
||||||
|
mInputThread.join(); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
// Keep trying until we're done
|
||||||
|
} |
||||||
|
} |
||||||
|
mInputThread = null; |
||||||
|
} |
||||||
|
if (mConnection != null) { |
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex); |
||||||
|
mConnection.releaseInterface(iface); |
||||||
|
mConnection.close(); |
||||||
|
mConnection = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void shutdown() { |
||||||
|
close(); |
||||||
|
mManager = null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setFrozen(boolean frozen) { |
||||||
|
mFrozen = frozen; |
||||||
|
} |
||||||
|
|
||||||
|
protected class InputThread extends Thread { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
int packetSize = mInputEndpoint.getMaxPacketSize(); |
||||||
|
byte[] packet = new byte[packetSize]; |
||||||
|
while (mRunning) { |
||||||
|
int r; |
||||||
|
try |
||||||
|
{ |
||||||
|
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); |
||||||
|
} |
||||||
|
catch (Exception e) |
||||||
|
{ |
||||||
|
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); |
||||||
|
break; |
||||||
|
} |
||||||
|
if (r < 0) { |
||||||
|
// Could be a timeout or an I/O error
|
||||||
|
} |
||||||
|
if (r > 0) { |
||||||
|
byte[] data; |
||||||
|
if (r == packetSize) { |
||||||
|
data = packet; |
||||||
|
} else { |
||||||
|
data = Arrays.copyOfRange(packet, 0, r); |
||||||
|
} |
||||||
|
|
||||||
|
if (!mFrozen) { |
||||||
|
mManager.HIDDeviceInputReport(mDeviceId, data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
|
||||||
|
import java.lang.Class; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
|
||||||
|
/** |
||||||
|
SDL library initialization |
||||||
|
*/ |
||||||
|
public class SDL { |
||||||
|
|
||||||
|
// This function should be called first and sets up the native code
|
||||||
|
// so it can call into the Java classes
|
||||||
|
public static void setupJNI() { |
||||||
|
SDLActivity.nativeSetupJNI(); |
||||||
|
SDLAudioManager.nativeSetupJNI(); |
||||||
|
SDLControllerManager.nativeSetupJNI(); |
||||||
|
} |
||||||
|
|
||||||
|
// This function should be called each time the activity is started
|
||||||
|
public static void initialize() { |
||||||
|
setContext(null); |
||||||
|
|
||||||
|
SDLActivity.initialize(); |
||||||
|
SDLAudioManager.initialize(); |
||||||
|
SDLControllerManager.initialize(); |
||||||
|
} |
||||||
|
|
||||||
|
// This function stores the current activity (SDL or not)
|
||||||
|
public static void setContext(Context context) { |
||||||
|
mContext = context; |
||||||
|
} |
||||||
|
|
||||||
|
public static Context getContext() { |
||||||
|
return mContext; |
||||||
|
} |
||||||
|
|
||||||
|
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { |
||||||
|
|
||||||
|
if (libraryName == null) { |
||||||
|
throw new NullPointerException("No library name provided."); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||||
|
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||||
|
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||||
|
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||||
|
// internally.)
|
||||||
|
//
|
||||||
|
// To use ReLinker, just add it as a dependency. For more information, see
|
||||||
|
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||||
|
//
|
||||||
|
Class<?> relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); |
||||||
|
Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); |
||||||
|
Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context"); |
||||||
|
Class<?> stringClass = mContext.getClassLoader().loadClass("java.lang.String"); |
||||||
|
|
||||||
|
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||||
|
// they've changed during updates.
|
||||||
|
Method forceMethod = relinkClass.getDeclaredMethod("force"); |
||||||
|
Object relinkInstance = forceMethod.invoke(null); |
||||||
|
Class<?> relinkInstanceClass = relinkInstance.getClass(); |
||||||
|
|
||||||
|
// Actually load the library!
|
||||||
|
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); |
||||||
|
loadMethod.invoke(relinkInstance, mContext, libraryName, null, null); |
||||||
|
} |
||||||
|
catch (final Throwable e) { |
||||||
|
// Fall back
|
||||||
|
try { |
||||||
|
System.loadLibrary(libraryName); |
||||||
|
} |
||||||
|
catch (final UnsatisfiedLinkError ule) { |
||||||
|
throw ule; |
||||||
|
} |
||||||
|
catch (final SecurityException se) { |
||||||
|
throw se; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected static Context mContext; |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,390 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import android.media.AudioFormat; |
||||||
|
import android.media.AudioManager; |
||||||
|
import android.media.AudioRecord; |
||||||
|
import android.media.AudioTrack; |
||||||
|
import android.media.MediaRecorder; |
||||||
|
import android.os.Build; |
||||||
|
import android.util.Log; |
||||||
|
|
||||||
|
public class SDLAudioManager |
||||||
|
{ |
||||||
|
protected static final String TAG = "SDLAudio"; |
||||||
|
|
||||||
|
protected static AudioTrack mAudioTrack; |
||||||
|
protected static AudioRecord mAudioRecord; |
||||||
|
|
||||||
|
public static void initialize() { |
||||||
|
mAudioTrack = null; |
||||||
|
mAudioRecord = null; |
||||||
|
} |
||||||
|
|
||||||
|
// Audio
|
||||||
|
|
||||||
|
protected static String getAudioFormatString(int audioFormat) { |
||||||
|
switch (audioFormat) { |
||||||
|
case AudioFormat.ENCODING_PCM_8BIT: |
||||||
|
return "8-bit"; |
||||||
|
case AudioFormat.ENCODING_PCM_16BIT: |
||||||
|
return "16-bit"; |
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT: |
||||||
|
return "float"; |
||||||
|
default: |
||||||
|
return Integer.toString(audioFormat); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { |
||||||
|
int channelConfig; |
||||||
|
int sampleSize; |
||||||
|
int frameSize; |
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); |
||||||
|
|
||||||
|
/* On older devices let's use known good settings */ |
||||||
|
if (Build.VERSION.SDK_INT < 21) { |
||||||
|
if (desiredChannels > 2) { |
||||||
|
desiredChannels = 2; |
||||||
|
} |
||||||
|
if (sampleRate < 8000) { |
||||||
|
sampleRate = 8000; |
||||||
|
} else if (sampleRate > 48000) { |
||||||
|
sampleRate = 48000; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { |
||||||
|
int minSDKVersion = (isCapture ? 23 : 21); |
||||||
|
if (Build.VERSION.SDK_INT < minSDKVersion) { |
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT; |
||||||
|
} |
||||||
|
} |
||||||
|
switch (audioFormat) |
||||||
|
{ |
||||||
|
case AudioFormat.ENCODING_PCM_8BIT: |
||||||
|
sampleSize = 1; |
||||||
|
break; |
||||||
|
case AudioFormat.ENCODING_PCM_16BIT: |
||||||
|
sampleSize = 2; |
||||||
|
break; |
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT: |
||||||
|
sampleSize = 4; |
||||||
|
break; |
||||||
|
default: |
||||||
|
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); |
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT; |
||||||
|
sampleSize = 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
if (isCapture) { |
||||||
|
switch (desiredChannels) { |
||||||
|
case 1: |
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_MONO; |
||||||
|
break; |
||||||
|
case 2: |
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO; |
||||||
|
break; |
||||||
|
default: |
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); |
||||||
|
desiredChannels = 2; |
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO; |
||||||
|
break; |
||||||
|
} |
||||||
|
} else { |
||||||
|
switch (desiredChannels) { |
||||||
|
case 1: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO; |
||||||
|
break; |
||||||
|
case 2: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; |
||||||
|
break; |
||||||
|
case 3: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; |
||||||
|
break; |
||||||
|
case 4: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD; |
||||||
|
break; |
||||||
|
case 5: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; |
||||||
|
break; |
||||||
|
case 6: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; |
||||||
|
break; |
||||||
|
case 7: |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; |
||||||
|
break; |
||||||
|
case 8: |
||||||
|
if (Build.VERSION.SDK_INT >= 23) { |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; |
||||||
|
} else { |
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); |
||||||
|
desiredChannels = 6; |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; |
||||||
|
} |
||||||
|
break; |
||||||
|
default: |
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); |
||||||
|
desiredChannels = 2; |
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Log.v(TAG, "Speaker configuration (and order of channels):"); |
||||||
|
|
||||||
|
if ((channelConfig & 0x00000004) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000008) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000010) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000020) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000040) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000080) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000100) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000200) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000400) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00000800) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); |
||||||
|
} |
||||||
|
if ((channelConfig & 0x00001000) != 0) { |
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); |
||||||
|
} |
||||||
|
*/ |
||||||
|
} |
||||||
|
frameSize = (sampleSize * desiredChannels); |
||||||
|
|
||||||
|
// Let the user pick a larger buffer if they really want -- but ye
|
||||||
|
// gods they probably shouldn't, the minimums are horrifyingly high
|
||||||
|
// latency already
|
||||||
|
int minBufferSize; |
||||||
|
if (isCapture) { |
||||||
|
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); |
||||||
|
} else { |
||||||
|
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); |
||||||
|
} |
||||||
|
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); |
||||||
|
|
||||||
|
int[] results = new int[4]; |
||||||
|
|
||||||
|
if (isCapture) { |
||||||
|
if (mAudioRecord == null) { |
||||||
|
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, |
||||||
|
channelConfig, audioFormat, desiredFrames * frameSize); |
||||||
|
|
||||||
|
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
|
||||||
|
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { |
||||||
|
Log.e(TAG, "Failed during initialization of AudioRecord"); |
||||||
|
mAudioRecord.release(); |
||||||
|
mAudioRecord = null; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
mAudioRecord.startRecording(); |
||||||
|
} |
||||||
|
|
||||||
|
results[0] = mAudioRecord.getSampleRate(); |
||||||
|
results[1] = mAudioRecord.getAudioFormat(); |
||||||
|
results[2] = mAudioRecord.getChannelCount(); |
||||||
|
|
||||||
|
} else { |
||||||
|
if (mAudioTrack == null) { |
||||||
|
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); |
||||||
|
|
||||||
|
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
|
||||||
|
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
|
||||||
|
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
|
||||||
|
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { |
||||||
|
/* Try again, with safer values */ |
||||||
|
|
||||||
|
Log.e(TAG, "Failed during initialization of Audio Track"); |
||||||
|
mAudioTrack.release(); |
||||||
|
mAudioTrack = null; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
mAudioTrack.play(); |
||||||
|
} |
||||||
|
|
||||||
|
results[0] = mAudioTrack.getSampleRate(); |
||||||
|
results[1] = mAudioTrack.getAudioFormat(); |
||||||
|
results[2] = mAudioTrack.getChannelCount(); |
||||||
|
} |
||||||
|
results[3] = desiredFrames; |
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); |
||||||
|
|
||||||
|
return results; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { |
||||||
|
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void audioWriteFloatBuffer(float[] buffer) { |
||||||
|
if (mAudioTrack == null) { |
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) { |
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); |
||||||
|
if (result > 0) { |
||||||
|
i += result; |
||||||
|
} else if (result == 0) { |
||||||
|
try { |
||||||
|
Thread.sleep(1); |
||||||
|
} catch(InterruptedException e) { |
||||||
|
// Nom nom
|
||||||
|
} |
||||||
|
} else { |
||||||
|
Log.w(TAG, "SDL audio: error return from write(float)"); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void audioWriteShortBuffer(short[] buffer) { |
||||||
|
if (mAudioTrack == null) { |
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) { |
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i); |
||||||
|
if (result > 0) { |
||||||
|
i += result; |
||||||
|
} else if (result == 0) { |
||||||
|
try { |
||||||
|
Thread.sleep(1); |
||||||
|
} catch(InterruptedException e) { |
||||||
|
// Nom nom
|
||||||
|
} |
||||||
|
} else { |
||||||
|
Log.w(TAG, "SDL audio: error return from write(short)"); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void audioWriteByteBuffer(byte[] buffer) { |
||||||
|
if (mAudioTrack == null) { |
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length; ) { |
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i); |
||||||
|
if (result > 0) { |
||||||
|
i += result; |
||||||
|
} else if (result == 0) { |
||||||
|
try { |
||||||
|
Thread.sleep(1); |
||||||
|
} catch(InterruptedException e) { |
||||||
|
// Nom nom
|
||||||
|
} |
||||||
|
} else { |
||||||
|
Log.w(TAG, "SDL audio: error return from write(byte)"); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { |
||||||
|
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames); |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { |
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static int captureReadShortBuffer(short[] buffer, boolean blocking) { |
||||||
|
if (Build.VERSION.SDK_INT < 23) { |
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length); |
||||||
|
} else { |
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { |
||||||
|
if (Build.VERSION.SDK_INT < 23) { |
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length); |
||||||
|
} else { |
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static void audioClose() { |
||||||
|
if (mAudioTrack != null) { |
||||||
|
mAudioTrack.stop(); |
||||||
|
mAudioTrack.release(); |
||||||
|
mAudioTrack = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static void captureClose() { |
||||||
|
if (mAudioRecord != null) { |
||||||
|
mAudioRecord.stop(); |
||||||
|
mAudioRecord.release(); |
||||||
|
mAudioRecord = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */ |
||||||
|
public static void audioSetThreadPriority(boolean iscapture, int device_id) { |
||||||
|
try { |
||||||
|
|
||||||
|
/* Set thread name */ |
||||||
|
if (iscapture) { |
||||||
|
Thread.currentThread().setName("SDLAudioC" + device_id); |
||||||
|
} else { |
||||||
|
Thread.currentThread().setName("SDLAudioP" + device_id); |
||||||
|
} |
||||||
|
|
||||||
|
/* Set thread priority */ |
||||||
|
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); |
||||||
|
|
||||||
|
} catch (Exception e) { |
||||||
|
Log.v(TAG, "modify thread properties failed " + e.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static native int nativeSetupJNI(); |
||||||
|
} |
||||||
@ -0,0 +1,792 @@ |
|||||||
|
package org.libsdl.app; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.os.Build; |
||||||
|
import android.os.VibrationEffect; |
||||||
|
import android.os.Vibrator; |
||||||
|
import android.util.Log; |
||||||
|
import android.view.InputDevice; |
||||||
|
import android.view.KeyEvent; |
||||||
|
import android.view.MotionEvent; |
||||||
|
import android.view.View; |
||||||
|
|
||||||
|
|
||||||
|
public class SDLControllerManager |
||||||
|
{ |
||||||
|
|
||||||
|
public static native int nativeSetupJNI(); |
||||||
|
|
||||||
|
public static native int nativeAddJoystick(int device_id, String name, String desc, |
||||||
|
int vendor_id, int product_id, |
||||||
|
boolean is_accelerometer, int button_mask, |
||||||
|
int naxes, int nhats, int nballs); |
||||||
|
public static native int nativeRemoveJoystick(int device_id); |
||||||
|
public static native int nativeAddHaptic(int device_id, String name); |
||||||
|
public static native int nativeRemoveHaptic(int device_id); |
||||||
|
public static native int onNativePadDown(int device_id, int keycode); |
||||||
|
public static native int onNativePadUp(int device_id, int keycode); |
||||||
|
public static native void onNativeJoy(int device_id, int axis, |
||||||
|
float value); |
||||||
|
public static native void onNativeHat(int device_id, int hat_id, |
||||||
|
int x, int y); |
||||||
|
|
||||||
|
protected static SDLJoystickHandler mJoystickHandler; |
||||||
|
protected static SDLHapticHandler mHapticHandler; |
||||||
|
|
||||||
|
private static final String TAG = "SDLControllerManager"; |
||||||
|
|
||||||
|
public static void initialize() { |
||||||
|
if (mJoystickHandler == null) { |
||||||
|
if (Build.VERSION.SDK_INT >= 19) { |
||||||
|
mJoystickHandler = new SDLJoystickHandler_API19(); |
||||||
|
} else { |
||||||
|
mJoystickHandler = new SDLJoystickHandler_API16(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (mHapticHandler == null) { |
||||||
|
if (Build.VERSION.SDK_INT >= 26) { |
||||||
|
mHapticHandler = new SDLHapticHandler_API26(); |
||||||
|
} else { |
||||||
|
mHapticHandler = new SDLHapticHandler(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
|
||||||
|
public static boolean handleJoystickMotionEvent(MotionEvent event) { |
||||||
|
return mJoystickHandler.handleMotionEvent(event); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void pollInputDevices() { |
||||||
|
mJoystickHandler.pollInputDevices(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void pollHapticDevices() { |
||||||
|
mHapticHandler.pollHapticDevices(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void hapticRun(int device_id, float intensity, int length) { |
||||||
|
mHapticHandler.run(device_id, intensity, length); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is called by SDL using JNI. |
||||||
|
*/ |
||||||
|
public static void hapticStop(int device_id) |
||||||
|
{ |
||||||
|
mHapticHandler.stop(device_id); |
||||||
|
} |
||||||
|
|
||||||
|
// Check if a given device is considered a possible SDL joystick
|
||||||
|
public static boolean isDeviceSDLJoystick(int deviceId) { |
||||||
|
InputDevice device = InputDevice.getDevice(deviceId); |
||||||
|
// We cannot use InputDevice.isVirtual before API 16, so let's accept
|
||||||
|
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
|
||||||
|
if ((device == null) || (deviceId < 0)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
int sources = device.getSources(); |
||||||
|
|
||||||
|
/* This is called for every button press, so let's not spam the logs */ |
||||||
|
/* |
||||||
|
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { |
||||||
|
Log.v(TAG, "Input device " + device.getName() + " has class joystick."); |
||||||
|
} |
||||||
|
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) { |
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a dpad."); |
||||||
|
} |
||||||
|
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { |
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a gamepad."); |
||||||
|
} |
||||||
|
*/ |
||||||
|
|
||||||
|
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 || |
||||||
|
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || |
||||||
|
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
class SDLJoystickHandler { |
||||||
|
|
||||||
|
/** |
||||||
|
* Handles given MotionEvent. |
||||||
|
* @param event the event to be handled. |
||||||
|
* @return if given event was processed. |
||||||
|
*/ |
||||||
|
public boolean handleMotionEvent(MotionEvent event) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Handles adding and removing of input devices. |
||||||
|
*/ |
||||||
|
public void pollInputDevices() { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* Actual joystick functionality available for API >= 12 devices */ |
||||||
|
class SDLJoystickHandler_API16 extends SDLJoystickHandler { |
||||||
|
|
||||||
|
static class SDLJoystick { |
||||||
|
public int device_id; |
||||||
|
public String name; |
||||||
|
public String desc; |
||||||
|
public ArrayList<InputDevice.MotionRange> axes; |
||||||
|
public ArrayList<InputDevice.MotionRange> hats; |
||||||
|
} |
||||||
|
static class RangeComparator implements Comparator<InputDevice.MotionRange> { |
||||||
|
@Override |
||||||
|
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { |
||||||
|
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
|
||||||
|
int arg0Axis = arg0.getAxis(); |
||||||
|
int arg1Axis = arg1.getAxis(); |
||||||
|
if (arg0Axis == MotionEvent.AXIS_GAS) { |
||||||
|
arg0Axis = MotionEvent.AXIS_BRAKE; |
||||||
|
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) { |
||||||
|
arg0Axis = MotionEvent.AXIS_GAS; |
||||||
|
} |
||||||
|
if (arg1Axis == MotionEvent.AXIS_GAS) { |
||||||
|
arg1Axis = MotionEvent.AXIS_BRAKE; |
||||||
|
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) { |
||||||
|
arg1Axis = MotionEvent.AXIS_GAS; |
||||||
|
} |
||||||
|
|
||||||
|
return arg0Axis - arg1Axis; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private final ArrayList<SDLJoystick> mJoysticks; |
||||||
|
|
||||||
|
public SDLJoystickHandler_API16() { |
||||||
|
|
||||||
|
mJoysticks = new ArrayList<SDLJoystick>(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void pollInputDevices() { |
||||||
|
int[] deviceIds = InputDevice.getDeviceIds(); |
||||||
|
|
||||||
|
for (int device_id : deviceIds) { |
||||||
|
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) { |
||||||
|
SDLJoystick joystick = getJoystick(device_id); |
||||||
|
if (joystick == null) { |
||||||
|
InputDevice joystickDevice = InputDevice.getDevice(device_id); |
||||||
|
joystick = new SDLJoystick(); |
||||||
|
joystick.device_id = device_id; |
||||||
|
joystick.name = joystickDevice.getName(); |
||||||
|
joystick.desc = getJoystickDescriptor(joystickDevice); |
||||||
|
joystick.axes = new ArrayList<InputDevice.MotionRange>(); |
||||||
|
joystick.hats = new ArrayList<InputDevice.MotionRange>(); |
||||||
|
|
||||||
|
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges(); |
||||||
|
Collections.sort(ranges, new RangeComparator()); |
||||||
|
for (InputDevice.MotionRange range : ranges) { |
||||||
|
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { |
||||||
|
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { |
||||||
|
joystick.hats.add(range); |
||||||
|
} else { |
||||||
|
joystick.axes.add(range); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mJoysticks.add(joystick); |
||||||
|
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, |
||||||
|
getVendorId(joystickDevice), getProductId(joystickDevice), false, |
||||||
|
getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* Check removed devices */ |
||||||
|
ArrayList<Integer> removedDevices = null; |
||||||
|
for (SDLJoystick joystick : mJoysticks) { |
||||||
|
int device_id = joystick.device_id; |
||||||
|
int i; |
||||||
|
for (i = 0; i < deviceIds.length; i++) { |
||||||
|
if (device_id == deviceIds[i]) break; |
||||||
|
} |
||||||
|
if (i == deviceIds.length) { |
||||||
|
if (removedDevices == null) { |
||||||
|
removedDevices = new ArrayList<Integer>(); |
||||||
|
} |
||||||
|
removedDevices.add(device_id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (removedDevices != null) { |
||||||
|
for (int device_id : removedDevices) { |
||||||
|
SDLControllerManager.nativeRemoveJoystick(device_id); |
||||||
|
for (int i = 0; i < mJoysticks.size(); i++) { |
||||||
|
if (mJoysticks.get(i).device_id == device_id) { |
||||||
|
mJoysticks.remove(i); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected SDLJoystick getJoystick(int device_id) { |
||||||
|
for (SDLJoystick joystick : mJoysticks) { |
||||||
|
if (joystick.device_id == device_id) { |
||||||
|
return joystick; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean handleMotionEvent(MotionEvent event) { |
||||||
|
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) { |
||||||
|
int actionPointerIndex = event.getActionIndex(); |
||||||
|
int action = event.getActionMasked(); |
||||||
|
if (action == MotionEvent.ACTION_MOVE) { |
||||||
|
SDLJoystick joystick = getJoystick(event.getDeviceId()); |
||||||
|
if (joystick != null) { |
||||||
|
for (int i = 0; i < joystick.axes.size(); i++) { |
||||||
|
InputDevice.MotionRange range = joystick.axes.get(i); |
||||||
|
/* Normalize the value to -1...1 */ |
||||||
|
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; |
||||||
|
SDLControllerManager.onNativeJoy(joystick.device_id, i, value); |
||||||
|
} |
||||||
|
for (int i = 0; i < joystick.hats.size() / 2; i++) { |
||||||
|
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex)); |
||||||
|
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex)); |
||||||
|
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public String getJoystickDescriptor(InputDevice joystickDevice) { |
||||||
|
String desc = joystickDevice.getDescriptor(); |
||||||
|
|
||||||
|
if (desc != null && !desc.isEmpty()) { |
||||||
|
return desc; |
||||||
|
} |
||||||
|
|
||||||
|
return joystickDevice.getName(); |
||||||
|
} |
||||||
|
public int getProductId(InputDevice joystickDevice) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
public int getVendorId(InputDevice joystickDevice) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
public int getButtonMask(InputDevice joystickDevice) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 { |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getProductId(InputDevice joystickDevice) { |
||||||
|
return joystickDevice.getProductId(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVendorId(InputDevice joystickDevice) { |
||||||
|
return joystickDevice.getVendorId(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getButtonMask(InputDevice joystickDevice) { |
||||||
|
int button_mask = 0; |
||||||
|
int[] keys = new int[] { |
||||||
|
KeyEvent.KEYCODE_BUTTON_A, |
||||||
|
KeyEvent.KEYCODE_BUTTON_B, |
||||||
|
KeyEvent.KEYCODE_BUTTON_X, |
||||||
|
KeyEvent.KEYCODE_BUTTON_Y, |
||||||
|
KeyEvent.KEYCODE_BACK, |
||||||
|
KeyEvent.KEYCODE_BUTTON_MODE, |
||||||
|
KeyEvent.KEYCODE_BUTTON_START, |
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBL, |
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBR, |
||||||
|
KeyEvent.KEYCODE_BUTTON_L1, |
||||||
|
KeyEvent.KEYCODE_BUTTON_R1, |
||||||
|
KeyEvent.KEYCODE_DPAD_UP, |
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN, |
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT, |
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT, |
||||||
|
KeyEvent.KEYCODE_BUTTON_SELECT, |
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER, |
||||||
|
|
||||||
|
// These don't map into any SDL controller buttons directly
|
||||||
|
KeyEvent.KEYCODE_BUTTON_L2, |
||||||
|
KeyEvent.KEYCODE_BUTTON_R2, |
||||||
|
KeyEvent.KEYCODE_BUTTON_C, |
||||||
|
KeyEvent.KEYCODE_BUTTON_Z, |
||||||
|
KeyEvent.KEYCODE_BUTTON_1, |
||||||
|
KeyEvent.KEYCODE_BUTTON_2, |
||||||
|
KeyEvent.KEYCODE_BUTTON_3, |
||||||
|
KeyEvent.KEYCODE_BUTTON_4, |
||||||
|
KeyEvent.KEYCODE_BUTTON_5, |
||||||
|
KeyEvent.KEYCODE_BUTTON_6, |
||||||
|
KeyEvent.KEYCODE_BUTTON_7, |
||||||
|
KeyEvent.KEYCODE_BUTTON_8, |
||||||
|
KeyEvent.KEYCODE_BUTTON_9, |
||||||
|
KeyEvent.KEYCODE_BUTTON_10, |
||||||
|
KeyEvent.KEYCODE_BUTTON_11, |
||||||
|
KeyEvent.KEYCODE_BUTTON_12, |
||||||
|
KeyEvent.KEYCODE_BUTTON_13, |
||||||
|
KeyEvent.KEYCODE_BUTTON_14, |
||||||
|
KeyEvent.KEYCODE_BUTTON_15, |
||||||
|
KeyEvent.KEYCODE_BUTTON_16, |
||||||
|
}; |
||||||
|
int[] masks = new int[] { |
||||||
|
(1 << 0), // A -> A
|
||||||
|
(1 << 1), // B -> B
|
||||||
|
(1 << 2), // X -> X
|
||||||
|
(1 << 3), // Y -> Y
|
||||||
|
(1 << 4), // BACK -> BACK
|
||||||
|
(1 << 5), // MODE -> GUIDE
|
||||||
|
(1 << 6), // START -> START
|
||||||
|
(1 << 7), // THUMBL -> LEFTSTICK
|
||||||
|
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||||
|
(1 << 9), // L1 -> LEFTSHOULDER
|
||||||
|
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||||
|
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||||
|
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||||
|
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||||
|
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||||
|
(1 << 4), // SELECT -> BACK
|
||||||
|
(1 << 0), // DPAD_CENTER -> A
|
||||||
|
(1 << 15), // L2 -> ??
|
||||||
|
(1 << 16), // R2 -> ??
|
||||||
|
(1 << 17), // C -> ??
|
||||||
|
(1 << 18), // Z -> ??
|
||||||
|
(1 << 20), // 1 -> ??
|
||||||
|
(1 << 21), // 2 -> ??
|
||||||
|
(1 << 22), // 3 -> ??
|
||||||
|
(1 << 23), // 4 -> ??
|
||||||
|
(1 << 24), // 5 -> ??
|
||||||
|
(1 << 25), // 6 -> ??
|
||||||
|
(1 << 26), // 7 -> ??
|
||||||
|
(1 << 27), // 8 -> ??
|
||||||
|
(1 << 28), // 9 -> ??
|
||||||
|
(1 << 29), // 10 -> ??
|
||||||
|
(1 << 30), // 11 -> ??
|
||||||
|
(1 << 31), // 12 -> ??
|
||||||
|
// We're out of room...
|
||||||
|
0xFFFFFFFF, // 13 -> ??
|
||||||
|
0xFFFFFFFF, // 14 -> ??
|
||||||
|
0xFFFFFFFF, // 15 -> ??
|
||||||
|
0xFFFFFFFF, // 16 -> ??
|
||||||
|
}; |
||||||
|
boolean[] has_keys = joystickDevice.hasKeys(keys); |
||||||
|
for (int i = 0; i < keys.length; ++i) { |
||||||
|
if (has_keys[i]) { |
||||||
|
button_mask |= masks[i]; |
||||||
|
} |
||||||
|
} |
||||||
|
return button_mask; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SDLHapticHandler_API26 extends SDLHapticHandler { |
||||||
|
@Override |
||||||
|
public void run(int device_id, float intensity, int length) { |
||||||
|
SDLHaptic haptic = getHaptic(device_id); |
||||||
|
if (haptic != null) { |
||||||
|
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length); |
||||||
|
if (intensity == 0.0f) { |
||||||
|
stop(device_id); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
int vibeValue = Math.round(intensity * 255); |
||||||
|
|
||||||
|
if (vibeValue > 255) { |
||||||
|
vibeValue = 255; |
||||||
|
} |
||||||
|
if (vibeValue < 1) { |
||||||
|
stop(device_id); |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue)); |
||||||
|
} |
||||||
|
catch (Exception e) { |
||||||
|
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||||
|
// something went horribly wrong with the Android 8.0 APIs.
|
||||||
|
haptic.vib.vibrate(length); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SDLHapticHandler { |
||||||
|
|
||||||
|
static class SDLHaptic { |
||||||
|
public int device_id; |
||||||
|
public String name; |
||||||
|
public Vibrator vib; |
||||||
|
} |
||||||
|
|
||||||
|
private final ArrayList<SDLHaptic> mHaptics; |
||||||
|
|
||||||
|
public SDLHapticHandler() { |
||||||
|
mHaptics = new ArrayList<SDLHaptic>(); |
||||||
|
} |
||||||
|
|
||||||
|
public void run(int device_id, float intensity, int length) { |
||||||
|
SDLHaptic haptic = getHaptic(device_id); |
||||||
|
if (haptic != null) { |
||||||
|
haptic.vib.vibrate(length); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void stop(int device_id) { |
||||||
|
SDLHaptic haptic = getHaptic(device_id); |
||||||
|
if (haptic != null) { |
||||||
|
haptic.vib.cancel(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void pollHapticDevices() { |
||||||
|
|
||||||
|
final int deviceId_VIBRATOR_SERVICE = 999999; |
||||||
|
boolean hasVibratorService = false; |
||||||
|
|
||||||
|
int[] deviceIds = InputDevice.getDeviceIds(); |
||||||
|
// It helps processing the device ids in reverse order
|
||||||
|
// For example, in the case of the XBox 360 wireless dongle,
|
||||||
|
// so the first controller seen by SDL matches what the receiver
|
||||||
|
// considers to be the first controller
|
||||||
|
|
||||||
|
for (int i = deviceIds.length - 1; i > -1; i--) { |
||||||
|
SDLHaptic haptic = getHaptic(deviceIds[i]); |
||||||
|
if (haptic == null) { |
||||||
|
InputDevice device = InputDevice.getDevice(deviceIds[i]); |
||||||
|
Vibrator vib = device.getVibrator(); |
||||||
|
if (vib.hasVibrator()) { |
||||||
|
haptic = new SDLHaptic(); |
||||||
|
haptic.device_id = deviceIds[i]; |
||||||
|
haptic.name = device.getName(); |
||||||
|
haptic.vib = vib; |
||||||
|
mHaptics.add(haptic); |
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* Check VIBRATOR_SERVICE */ |
||||||
|
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE); |
||||||
|
if (vib != null) { |
||||||
|
hasVibratorService = vib.hasVibrator(); |
||||||
|
|
||||||
|
if (hasVibratorService) { |
||||||
|
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE); |
||||||
|
if (haptic == null) { |
||||||
|
haptic = new SDLHaptic(); |
||||||
|
haptic.device_id = deviceId_VIBRATOR_SERVICE; |
||||||
|
haptic.name = "VIBRATOR_SERVICE"; |
||||||
|
haptic.vib = vib; |
||||||
|
mHaptics.add(haptic); |
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* Check removed devices */ |
||||||
|
ArrayList<Integer> removedDevices = null; |
||||||
|
for (SDLHaptic haptic : mHaptics) { |
||||||
|
int device_id = haptic.device_id; |
||||||
|
int i; |
||||||
|
for (i = 0; i < deviceIds.length; i++) { |
||||||
|
if (device_id == deviceIds[i]) break; |
||||||
|
} |
||||||
|
|
||||||
|
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) { |
||||||
|
if (i == deviceIds.length) { |
||||||
|
if (removedDevices == null) { |
||||||
|
removedDevices = new ArrayList<Integer>(); |
||||||
|
} |
||||||
|
removedDevices.add(device_id); |
||||||
|
} |
||||||
|
} // else: don't remove the vibrator if it is still present
|
||||||
|
} |
||||||
|
|
||||||
|
if (removedDevices != null) { |
||||||
|
for (int device_id : removedDevices) { |
||||||
|
SDLControllerManager.nativeRemoveHaptic(device_id); |
||||||
|
for (int i = 0; i < mHaptics.size(); i++) { |
||||||
|
if (mHaptics.get(i).device_id == device_id) { |
||||||
|
mHaptics.remove(i); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected SDLHaptic getHaptic(int device_id) { |
||||||
|
for (SDLHaptic haptic : mHaptics) { |
||||||
|
if (haptic.device_id == device_id) { |
||||||
|
return haptic; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener { |
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
@Override |
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) { |
||||||
|
float x, y; |
||||||
|
int action; |
||||||
|
|
||||||
|
switch ( event.getSource() ) { |
||||||
|
case InputDevice.SOURCE_JOYSTICK: |
||||||
|
case InputDevice.SOURCE_GAMEPAD: |
||||||
|
case InputDevice.SOURCE_DPAD: |
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event); |
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE: |
||||||
|
action = event.getActionMasked(); |
||||||
|
switch (action) { |
||||||
|
case MotionEvent.ACTION_SCROLL: |
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); |
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false); |
||||||
|
return true; |
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE: |
||||||
|
x = event.getX(0); |
||||||
|
y = event.getY(0); |
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false); |
||||||
|
return true; |
||||||
|
|
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean supportsRelativeMouse() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean inRelativeMode() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public void reclaimRelativeMouseModeIfNeeded() |
||||||
|
{ |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
public float getEventX(MotionEvent event) { |
||||||
|
return event.getX(0); |
||||||
|
} |
||||||
|
|
||||||
|
public float getEventY(MotionEvent event) { |
||||||
|
return event.getY(0); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 { |
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
|
||||||
|
private boolean mRelativeModeEnabled; |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) { |
||||||
|
|
||||||
|
// Handle relative mouse mode
|
||||||
|
if (mRelativeModeEnabled) { |
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE) { |
||||||
|
int action = event.getActionMasked(); |
||||||
|
if (action == MotionEvent.ACTION_HOVER_MOVE) { |
||||||
|
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); |
||||||
|
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Event was not managed, call SDLGenericMotionListener_API12 method
|
||||||
|
return super.onGenericMotion(v, event); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsRelativeMouse() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean inRelativeMode() { |
||||||
|
return mRelativeModeEnabled; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) { |
||||||
|
mRelativeModeEnabled = enabled; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getEventX(MotionEvent event) { |
||||||
|
if (mRelativeModeEnabled) { |
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); |
||||||
|
} else { |
||||||
|
return event.getX(0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getEventY(MotionEvent event) { |
||||||
|
if (mRelativeModeEnabled) { |
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); |
||||||
|
} else { |
||||||
|
return event.getY(0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { |
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
private boolean mRelativeModeEnabled; |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) { |
||||||
|
float x, y; |
||||||
|
int action; |
||||||
|
|
||||||
|
switch ( event.getSource() ) { |
||||||
|
case InputDevice.SOURCE_JOYSTICK: |
||||||
|
case InputDevice.SOURCE_GAMEPAD: |
||||||
|
case InputDevice.SOURCE_DPAD: |
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event); |
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE: |
||||||
|
// DeX desktop mouse cursor is a separate non-standard input type.
|
||||||
|
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN: |
||||||
|
action = event.getActionMasked(); |
||||||
|
switch (action) { |
||||||
|
case MotionEvent.ACTION_SCROLL: |
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); |
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false); |
||||||
|
return true; |
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE: |
||||||
|
x = event.getX(0); |
||||||
|
y = event.getY(0); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false); |
||||||
|
return true; |
||||||
|
|
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE_RELATIVE: |
||||||
|
action = event.getActionMasked(); |
||||||
|
switch (action) { |
||||||
|
case MotionEvent.ACTION_SCROLL: |
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); |
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false); |
||||||
|
return true; |
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE: |
||||||
|
x = event.getX(0); |
||||||
|
y = event.getY(0); |
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true); |
||||||
|
return true; |
||||||
|
|
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsRelativeMouse() { |
||||||
|
return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean inRelativeMode() { |
||||||
|
return mRelativeModeEnabled; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) { |
||||||
|
if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) { |
||||||
|
if (enabled) { |
||||||
|
SDLActivity.getContentView().requestPointerCapture(); |
||||||
|
} else { |
||||||
|
SDLActivity.getContentView().releasePointerCapture(); |
||||||
|
} |
||||||
|
mRelativeModeEnabled = enabled; |
||||||
|
return true; |
||||||
|
} else { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reclaimRelativeMouseModeIfNeeded() |
||||||
|
{ |
||||||
|
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { |
||||||
|
SDLActivity.getContentView().requestPointerCapture(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getEventX(MotionEvent event) { |
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getX(0); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getEventY(MotionEvent event) { |
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getY(0); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
<resources> |
||||||
|
<string name="app_name">Game</string> |
||||||
|
</resources> |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules. |
||||||
|
|
||||||
|
buildscript { |
||||||
|
repositories { |
||||||
|
jcenter() |
||||||
|
google() |
||||||
|
} |
||||||
|
dependencies { |
||||||
|
classpath 'com.android.tools.build:gradle:3.2.0' |
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong |
||||||
|
// in the individual module build.gradle files |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
allprojects { |
||||||
|
repositories { |
||||||
|
jcenter() |
||||||
|
google() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
task clean(type: Delete) { |
||||||
|
delete rootProject.buildDir |
||||||
|
} |
||||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||||
|
#Mon Oct 23 13:51:26 PDT 2017 |
||||||
|
distributionBase=GRADLE_USER_HOME |
||||||
|
distributionPath=wrapper/dists |
||||||
|
zipStoreBase=GRADLE_USER_HOME |
||||||
|
zipStorePath=wrapper/dists |
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
############################################################################## |
||||||
|
## |
||||||
|
## Gradle start up script for UN*X |
||||||
|
## |
||||||
|
############################################################################## |
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||||
|
DEFAULT_JVM_OPTS="" |
||||||
|
|
||||||
|
APP_NAME="Gradle" |
||||||
|
APP_BASE_NAME=`basename "$0"` |
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value. |
||||||
|
MAX_FD="maximum" |
||||||
|
|
||||||
|
warn ( ) { |
||||||
|
echo "$*" |
||||||
|
} |
||||||
|
|
||||||
|
die ( ) { |
||||||
|
echo |
||||||
|
echo "$*" |
||||||
|
echo |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false'). |
||||||
|
cygwin=false |
||||||
|
msys=false |
||||||
|
darwin=false |
||||||
|
case "`uname`" in |
||||||
|
CYGWIN* ) |
||||||
|
cygwin=true |
||||||
|
;; |
||||||
|
Darwin* ) |
||||||
|
darwin=true |
||||||
|
;; |
||||||
|
MINGW* ) |
||||||
|
msys=true |
||||||
|
;; |
||||||
|
esac |
||||||
|
|
||||||
|
# Attempt to set APP_HOME |
||||||
|
# Resolve links: $0 may be a link |
||||||
|
PRG="$0" |
||||||
|
# Need this for relative symlinks. |
||||||
|
while [ -h "$PRG" ] ; do |
||||||
|
ls=`ls -ld "$PRG"` |
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'` |
||||||
|
if expr "$link" : '/.*' > /dev/null; then |
||||||
|
PRG="$link" |
||||||
|
else |
||||||
|
PRG=`dirname "$PRG"`"/$link" |
||||||
|
fi |
||||||
|
done |
||||||
|
SAVED="`pwd`" |
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null |
||||||
|
APP_HOME="`pwd -P`" |
||||||
|
cd "$SAVED" >/dev/null |
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM. |
||||||
|
if [ -n "$JAVA_HOME" ] ; then |
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
||||||
|
# IBM's JDK on AIX uses strange locations for the executables |
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java" |
||||||
|
else |
||||||
|
JAVACMD="$JAVA_HOME/bin/java" |
||||||
|
fi |
||||||
|
if [ ! -x "$JAVACMD" ] ; then |
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the |
||||||
|
location of your Java installation." |
||||||
|
fi |
||||||
|
else |
||||||
|
JAVACMD="java" |
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the |
||||||
|
location of your Java installation." |
||||||
|
fi |
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can. |
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then |
||||||
|
MAX_FD_LIMIT=`ulimit -H -n` |
||||||
|
if [ $? -eq 0 ] ; then |
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then |
||||||
|
MAX_FD="$MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
ulimit -n $MAX_FD |
||||||
|
if [ $? -ne 0 ] ; then |
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD" |
||||||
|
fi |
||||||
|
else |
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
fi |
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock |
||||||
|
if $darwin; then |
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" |
||||||
|
fi |
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java |
||||||
|
if $cygwin ; then |
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"` |
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"` |
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath |
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` |
||||||
|
SEP="" |
||||||
|
for dir in $ROOTDIRSRAW ; do |
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir" |
||||||
|
SEP="|" |
||||||
|
done |
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))" |
||||||
|
# Add a user-defined pattern to the cygpath arguments |
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then |
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" |
||||||
|
fi |
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
||||||
|
i=0 |
||||||
|
for arg in "$@" ; do |
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` |
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option |
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition |
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` |
||||||
|
else |
||||||
|
eval `echo args$i`="\"$arg\"" |
||||||
|
fi |
||||||
|
i=$((i+1)) |
||||||
|
done |
||||||
|
case $i in |
||||||
|
(0) set -- ;; |
||||||
|
(1) set -- "$args0" ;; |
||||||
|
(2) set -- "$args0" "$args1" ;; |
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;; |
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; |
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; |
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; |
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; |
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; |
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; |
||||||
|
esac |
||||||
|
fi |
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules |
||||||
|
function splitJvmOpts() { |
||||||
|
JVM_OPTS=("$@") |
||||||
|
} |
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS |
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" |
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
@if "%DEBUG%" == "" @echo off |
||||||
|
@rem ########################################################################## |
||||||
|
@rem |
||||||
|
@rem Gradle startup script for Windows |
||||||
|
@rem |
||||||
|
@rem ########################################################################## |
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell |
||||||
|
if "%OS%"=="Windows_NT" setlocal |
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||||
|
set DEFAULT_JVM_OPTS= |
||||||
|
|
||||||
|
set DIRNAME=%~dp0 |
||||||
|
if "%DIRNAME%" == "" set DIRNAME=. |
||||||
|
set APP_BASE_NAME=%~n0 |
||||||
|
set APP_HOME=%DIRNAME% |
||||||
|
|
||||||
|
@rem Find java.exe |
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome |
||||||
|
|
||||||
|
set JAVA_EXE=java.exe |
||||||
|
%JAVA_EXE% -version >NUL 2>&1 |
||||||
|
if "%ERRORLEVEL%" == "0" goto init |
||||||
|
|
||||||
|
echo. |
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||||
|
echo. |
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the |
||||||
|
echo location of your Java installation. |
||||||
|
|
||||||
|
goto fail |
||||||
|
|
||||||
|
:findJavaFromJavaHome |
||||||
|
set JAVA_HOME=%JAVA_HOME:"=% |
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init |
||||||
|
|
||||||
|
echo. |
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% |
||||||
|
echo. |
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the |
||||||
|
echo location of your Java installation. |
||||||
|
|
||||||
|
goto fail |
||||||
|
|
||||||
|
:init |
||||||
|
@rem Get command-line arguments, handling Windowz variants |
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args |
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args |
||||||
|
|
||||||
|
:win9xME_args |
||||||
|
@rem Slurp the command line arguments. |
||||||
|
set CMD_LINE_ARGS= |
||||||
|
set _SKIP=2 |
||||||
|
|
||||||
|
:win9xME_args_slurp |
||||||
|
if "x%~1" == "x" goto execute |
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%* |
||||||
|
goto execute |
||||||
|
|
||||||
|
:4NT_args |
||||||
|
@rem Get arguments from the 4NT Shell from JP Software |
||||||
|
set CMD_LINE_ARGS=%$ |
||||||
|
|
||||||
|
:execute |
||||||
|
@rem Setup the command line |
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
||||||
|
|
||||||
|
@rem Execute Gradle |
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% |
||||||
|
|
||||||
|
:end |
||||||
|
@rem End local scope for the variables with windows NT shell |
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd |
||||||
|
|
||||||
|
:fail |
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of |
||||||
|
rem the _cmd.exe /c_ return code! |
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 |
||||||
|
exit /b 1 |
||||||
|
|
||||||
|
:mainEnd |
||||||
|
if "%OS%"=="Windows_NT" endlocal |
||||||
|
|
||||||
|
:omega |
||||||
Loading…
Reference in new issue