mirror of https://git.sr.ht/~rabbits/uxn
36 changed files with 5815 additions and 0 deletions
@ -0,0 +1,3 @@
|
||||
[submodule "src/android/app/jni/SDL/SDL"] |
||||
path = src/android/app/jni/SDL/SDL |
||||
url = git://github.com/libsdl-org/SDL |
||||
@ -0,0 +1,38 @@
|
||||
apply plugin: 'com.android.application' |
||||
|
||||
android { |
||||
compileSdkVersion 31 |
||||
defaultConfig { |
||||
applicationId "org.rabbits.uxn" |
||||
minSdkVersion 16 |
||||
targetSdkVersion 31 |
||||
versionCode 1 |
||||
versionName "1.0" |
||||
externalNativeBuild { |
||||
ndkBuild { |
||||
arguments "APP_PLATFORM=android-16" |
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' |
||||
} |
||||
} |
||||
} |
||||
buildTypes { |
||||
release { |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' |
||||
} |
||||
} |
||||
sourceSets.main { |
||||
jniLibs.srcDir 'libs' |
||||
} |
||||
externalNativeBuild { |
||||
ndkBuild { |
||||
path 'jni/Android.mk' |
||||
} |
||||
} |
||||
lintOptions { |
||||
abortOnError false |
||||
} |
||||
} |
||||
dependencies { |
||||
implementation fileTree(include: ['*.jar'], dir: 'libs') |
||||
} |
||||
@ -0,0 +1 @@
|
||||
include $(call all-subdir-makefiles) |
||||
@ -0,0 +1,2 @@
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
|
||||
APP_PLATFORM=android-16
|
||||
@ -0,0 +1,123 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
###########################
|
||||
#
|
||||
# SDL shared library
|
||||
#
|
||||
###########################
|
||||
|
||||
include $(CLEAR_VARS) |
||||
|
||||
LOCAL_MODULE := SDL2
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
|
||||
|
||||
LOCAL_SRC_FILES := \
|
||||
$(subst $(LOCAL_PATH)/,, \
|
||||
$(wildcard $(LOCAL_PATH)/src/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/audio/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/audio/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/audio/dummy/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/audio/aaudio/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/audio/openslES/*.c) \
|
||||
$(LOCAL_PATH)/src/atomic/SDL_atomic.c.arm \
|
||||
$(LOCAL_PATH)/src/atomic/SDL_spinlock.c.arm \
|
||||
$(wildcard $(LOCAL_PATH)/src/core/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/dynapi/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/events/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/file/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/haptic/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/haptic/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/hidapi/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/hidapi/android/*.cpp) \
|
||||
$(wildcard $(LOCAL_PATH)/src/joystick/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/joystick/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/joystick/hidapi/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/joystick/virtual/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/loadso/dlopen/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/locale/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/locale/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/misc/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/misc/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/power/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/power/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/filesystem/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/sensor/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/sensor/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/render/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/render/*/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/stdlib/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/thread/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/thread/pthread/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/timer/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/video/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/video/android/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c) \
|
||||
$(wildcard $(LOCAL_PATH)/src/test/*.c))
|
||||
|
||||
LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES
|
||||
LOCAL_CFLAGS += \
|
||||
-Wall -Wextra \
|
||||
-Wdocumentation \
|
||||
-Wdocumentation-unknown-command \
|
||||
-Wmissing-prototypes \
|
||||
-Wunreachable-code-break \
|
||||
-Wunneeded-internal-declaration \
|
||||
-Wmissing-variable-declarations \
|
||||
-Wfloat-conversion \
|
||||
-Wshorten-64-to-32 \
|
||||
-Wunreachable-code-return \
|
||||
-Wshift-sign-overflow \
|
||||
-Wstrict-prototypes \
|
||||
-Wkeyword-macro \
|
||||
|
||||
|
||||
# Warnings we haven't fixed (yet)
|
||||
LOCAL_CFLAGS += -Wno-unused-parameter -Wno-sign-compare
|
||||
|
||||
LOCAL_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid
|
||||
|
||||
ifeq ($(NDK_DEBUG),1) |
||||
cmd-strip :=
|
||||
endif |
||||
|
||||
LOCAL_STATIC_LIBRARIES := cpufeatures
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY) |
||||
|
||||
###########################
|
||||
#
|
||||
# SDL static library
|
||||
#
|
||||
###########################
|
||||
|
||||
LOCAL_MODULE := SDL2_static
|
||||
|
||||
LOCAL_MODULE_FILENAME := libSDL2
|
||||
|
||||
LOCAL_LDLIBS :=
|
||||
LOCAL_EXPORT_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -llog -landroid
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY) |
||||
|
||||
###########################
|
||||
#
|
||||
# SDL main static library
|
||||
#
|
||||
###########################
|
||||
|
||||
include $(CLEAR_VARS) |
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
|
||||
LOCAL_MODULE := SDL2_main
|
||||
|
||||
LOCAL_MODULE_FILENAME := libSDL2main
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY) |
||||
|
||||
$(call import-module,android/cpufeatures) |
||||
@ -0,0 +1 @@
|
||||
Subproject commit a1e992b110b9adf3305a5ebb5514f0e970f7911e |
||||
@ -0,0 +1,20 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS) |
||||
|
||||
LOCAL_MODULE := main
|
||||
SDL_PATH := $(LOCAL_PATH)/../SDL
|
||||
UXN_DIR := $(LOCAL_PATH)/../../../../../
|
||||
LOCAL_C_INCLUDES := $(SDL_PATH)/include $(UXN_DIR)/src
|
||||
LOCAL_SRC_FILES := \
|
||||
$(UXN_DIR)/src/uxn-fast.c \
|
||||
$(UXN_DIR)/src/uxnemu.c \
|
||||
$(UXN_DIR)/src/devices/apu.c \
|
||||
$(UXN_DIR)/src/devices/file.c \
|
||||
$(UXN_DIR)/src/devices/mouse.c \
|
||||
$(UXN_DIR)/src/devices/ppu.c \
|
||||
$(UXN_DIR)/src/devices/ppu_aarch64.c
|
||||
LOCAL_SHARED_LIBRARIES := SDL2
|
||||
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY) |
||||
@ -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,108 @@
|
||||
<?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.rabbits.uxn" |
||||
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" /> --> |
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" /> |
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
||||
|
||||
<!-- Allow downloading to the external storage on Android 5.1 and older --> |
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> |
||||
|
||||
<!-- Allow access to Bluetooth devices --> |
||||
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM --> |
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> --> |
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> --> |
||||
|
||||
<!-- 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 "Uxn" 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="Uxn" |
||||
android:label="@string/app_name" |
||||
android:alwaysRetainTaskState="true" |
||||
android:launchMode="singleInstance" |
||||
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" |
||||
android:preferMinimalPostProcessing="true" |
||||
android:screenOrientation="portrait" |
||||
android:exported="true" |
||||
> |
||||
<intent-filter> |
||||
<action android:name="android.intent.action.MAIN" /> |
||||
<category android:name="android.intent.category.LAUNCHER" /> |
||||
</intent-filter> |
||||
<!-- Let Android know that we can handle some USB devices and should receive this event --> |
||||
<intent-filter> |
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> |
||||
</intent-filter> |
||||
<!-- Drop file event --> |
||||
<intent-filter android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:priority="50"> |
||||
<action android:name="android.intent.action.VIEW" /> |
||||
<category android:name="android.intent.category.DEFAULT" /> |
||||
<data android:mimeType="application/octet-stream" android:pathPattern=".*\\.rom" /> |
||||
</intent-filter> |
||||
<intent-filter android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:priority="50"> |
||||
<action android:name="android.intent.action.VIEW" /> |
||||
<category android:name="android.intent.category.DEFAULT" /> |
||||
<category android:name="android.intent.category.BROWSABLE" /> |
||||
<data android:mimeType="application/octet-stream" android:host="*" android:scheme="http" android:pathPattern=".*\\.rom" /> |
||||
</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,672 @@
|
||||
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.os.Build; |
||||
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; |
||||
|
||||
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); |
||||
} |
||||
} |
||||
|
||||
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
|
||||
0x1949, // Lab126, Inc.
|
||||
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 (Build.VERSION.SDK_INT <= 30 && |
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { |
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); |
||||
return; |
||||
} |
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) { |
||||
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); |
||||
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 initialize(boolean usb, boolean bluetooth) { |
||||
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); |
||||
|
||||
if (usb) { |
||||
initializeUSB(); |
||||
} |
||||
if (bluetooth) { |
||||
initializeBluetooth(); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
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 { |
||||
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||
int flags; |
||||
if (Build.VERSION.SDK_INT >= 31) { |
||||
flags = FLAG_MUTABLE; |
||||
} else { |
||||
flags = 0; |
||||
} |
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); |
||||
} 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,394 @@
|
||||
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; |
||||
} |
||||
} |
||||
|
||||
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */ |
||||
if (Build.VERSION.SDK_INT < 22) { |
||||
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,7 @@
|
||||
package org.rabbits.uxn; |
||||
|
||||
import org.libsdl.app.SDLActivity; |
||||
|
||||
public class Uxn extends SDLActivity |
||||
{ |
||||
} |
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<color name="colorPrimary">#3F51B5</color> |
||||
<color name="colorPrimaryDark">#303F9F</color> |
||||
<color name="colorAccent">#FF4081</color> |
||||
</resources> |
||||
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="app_name">Uxn</string> |
||||
</resources> |
||||
@ -0,0 +1,8 @@
|
||||
<resources> |
||||
|
||||
<!-- Base application theme. --> |
||||
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar"> |
||||
<!-- Customize your theme here. --> |
||||
</style> |
||||
|
||||
</resources> |
||||
@ -0,0 +1,25 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules. |
||||
|
||||
buildscript { |
||||
repositories { |
||||
mavenCentral() |
||||
google() |
||||
} |
||||
dependencies { |
||||
classpath 'com.android.tools.build:gradle:7.0.3' |
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong |
||||
// in the individual module build.gradle files |
||||
} |
||||
} |
||||
|
||||
allprojects { |
||||
repositories { |
||||
mavenCentral() |
||||
google() |
||||
} |
||||
} |
||||
|
||||
task clean(type: Delete) { |
||||
delete rootProject.buildDir |
||||
} |
||||
@ -0,0 +1,17 @@
|
||||
# Project-wide Gradle settings. |
||||
|
||||
# IDE (e.g. Android Studio) users: |
||||
# Gradle settings configured through the IDE *will override* |
||||
# any settings specified in this file. |
||||
|
||||
# For more details on how to configure your build environment visit |
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html |
||||
|
||||
# Specifies the JVM arguments used for the daemon process. |
||||
# The setting is particularly useful for tweaking memory settings. |
||||
org.gradle.jvmargs=-Xmx1536m |
||||
|
||||
# When configured, Gradle will run in incubating parallel mode. |
||||
# This option should only be used with decoupled projects. More details, visit |
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects |
||||
# org.gradle.parallel=true |
||||
Binary file not shown.
@ -0,0 +1,6 @@
|
||||
#Thu Nov 11 18:20:34 PST 2021 |
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip |
||||
distributionPath=wrapper/dists |
||||
zipStorePath=wrapper/dists |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
@ -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 "$@" |
||||
Loading…
Reference in new issue