Browse Source
* Kitty Gridlock now has its own project with custom icon and name * Redirect source/data file output to this project * Added script to generate multiple resolutions of icons * Update readme with Android infomaster
38 changed files with 5983 additions and 13 deletions
@ -0,0 +1,71 @@ |
|||
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); |
|||
def buildAsApplication = !buildAsLibrary |
|||
if (buildAsApplication) { |
|||
apply plugin: 'com.android.application' |
|||
} |
|||
else { |
|||
apply plugin: 'com.android.library' |
|||
} |
|||
|
|||
android { |
|||
compileSdkVersion 26 |
|||
defaultConfig { |
|||
if (buildAsApplication) { |
|||
applicationId "org.libsdl.app" |
|||
} |
|||
minSdkVersion 16 |
|||
targetSdkVersion 26 |
|||
versionCode 1 |
|||
versionName "1.0" |
|||
externalNativeBuild { |
|||
ndkBuild { |
|||
arguments "APP_PLATFORM=android-16" |
|||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' |
|||
} |
|||
// cmake { |
|||
// arguments "-DANDROID_APP_PLATFORM=android-16", "-DANDROID_STL=c++_static" |
|||
// // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' |
|||
// abiFilters 'arm64-v8a' |
|||
// } |
|||
} |
|||
} |
|||
buildTypes { |
|||
release { |
|||
minifyEnabled false |
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' |
|||
} |
|||
} |
|||
if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { |
|||
sourceSets.main { |
|||
jniLibs.srcDir 'libs' |
|||
} |
|||
externalNativeBuild { |
|||
ndkBuild { |
|||
path 'jni/Android.mk' |
|||
} |
|||
// cmake { |
|||
// path 'jni/CMakeLists.txt' |
|||
// } |
|||
} |
|||
|
|||
} |
|||
lintOptions { |
|||
abortOnError false |
|||
} |
|||
|
|||
if (buildAsLibrary) { |
|||
libraryVariants.all { variant -> |
|||
variant.outputs.each { output -> |
|||
def outputFile = output.outputFile |
|||
if (outputFile != null && outputFile.name.endsWith(".aar")) { |
|||
def fileName = "org.libsdl.app.aar"; |
|||
output.outputFile = new File(outputFile.parent, fileName); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation fileTree(include: ['*.jar'], dir: 'libs') |
|||
} |
@ -0,0 +1 @@ |
|||
include $(call all-subdir-makefiles) |
@ -0,0 +1,10 @@ |
|||
|
|||
# Uncomment this if you're using STL in your project
|
|||
# You can find more information here:
|
|||
# https://developer.android.com/ndk/guides/cpp-support
|
|||
# APP_STL := c++_shared
|
|||
|
|||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 |
|||
|
|||
# Min runtime API level
|
|||
APP_PLATFORM=android-16 |
@ -0,0 +1,20 @@ |
|||
cmake_minimum_required(VERSION 3.6) |
|||
|
|||
project(GAME) |
|||
|
|||
# armeabi-v7a requires cpufeatures library |
|||
# include(AndroidNdkModules) |
|||
# android_ndk_import_module_cpufeatures() |
|||
|
|||
|
|||
# SDL sources are in a subfolder named "SDL" |
|||
add_subdirectory(SDL) |
|||
|
|||
# Compilation of companion libraries |
|||
#add_subdirectory(SDL_image) |
|||
#add_subdirectory(SDL_mixer) |
|||
#add_subdirectory(SDL_ttf) |
|||
|
|||
# Your game and its CMakeLists.txt are in a subfolder named "src" |
|||
add_subdirectory(src) |
|||
|
@ -0,0 +1,21 @@ |
|||
LOCAL_PATH := $(call my-dir) |
|||
|
|||
include $(CLEAR_VARS) |
|||
|
|||
LOCAL_MODULE := main |
|||
|
|||
SDL_PATH := ../SDL |
|||
|
|||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include $(LOCAL_PATH)/../../../../Dependencies/gamelib/Dependencies/Handmade-Math |
|||
|
|||
# Work around "bug" in cakelisp where too many parens
|
|||
LOCAL_CFLAGS += -Wno-parentheses-equality |
|||
|
|||
# Add your application source files here...
|
|||
LOCAL_SRC_FILES := Main.cake.cpp Math.cake.cpp SDL.cake.cpp |
|||
|
|||
LOCAL_SHARED_LIBRARIES := SDL2 |
|||
|
|||
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog |
|||
|
|||
include $(BUILD_SHARED_LIBRARY) |
@ -0,0 +1,13 @@ |
|||
cmake_minimum_required(VERSION 3.6) |
|||
|
|||
project(MY_APP) |
|||
|
|||
find_library(SDL2 SDL2) |
|||
|
|||
add_library(main SHARED) |
|||
|
|||
target_sources(main PRIVATE YourSourceHere.c) |
|||
|
|||
target_link_libraries(main SDL2) |
|||
|
|||
|
@ -0,0 +1,17 @@ |
|||
# Add project specific ProGuard rules here. |
|||
# By default, the flags in this file are appended to flags specified |
|||
# in [sdk]/tools/proguard/proguard-android.txt |
|||
# You can edit the include path and order by changing the proguardFiles |
|||
# directive in build.gradle. |
|||
# |
|||
# For more details, see |
|||
# http://developer.android.com/guide/developing/tools/proguard.html |
|||
|
|||
# Add any project specific keep options here: |
|||
|
|||
# If your project uses WebView with JS, uncomment the following |
|||
# and specify the fully qualified class name to the JavaScript interface |
|||
# class: |
|||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
|||
# public *; |
|||
#} |
@ -0,0 +1,90 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Replace com.test.game with the identifier of your game below, e.g. |
|||
com.gamemaker.game |
|||
--> |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|||
package="org.libsdl.app" |
|||
android:versionCode="1" |
|||
android:versionName="1.0" |
|||
android:installLocation="auto"> |
|||
|
|||
<!-- OpenGL ES 2.0 --> |
|||
<uses-feature android:glEsVersion="0x00020000" /> |
|||
|
|||
<!-- Touchscreen support --> |
|||
<uses-feature |
|||
android:name="android.hardware.touchscreen" |
|||
android:required="false" /> |
|||
|
|||
<!-- Game controller support --> |
|||
<uses-feature |
|||
android:name="android.hardware.bluetooth" |
|||
android:required="false" /> |
|||
<uses-feature |
|||
android:name="android.hardware.gamepad" |
|||
android:required="false" /> |
|||
<uses-feature |
|||
android:name="android.hardware.usb.host" |
|||
android:required="false" /> |
|||
|
|||
<!-- External mouse input events --> |
|||
<uses-feature |
|||
android:name="android.hardware.type.pc" |
|||
android:required="false" /> |
|||
|
|||
<!-- Audio recording support --> |
|||
<!-- if you want to capture audio, uncomment this. --> |
|||
<!-- <uses-feature |
|||
android:name="android.hardware.microphone" |
|||
android:required="false" /> --> |
|||
|
|||
<!-- Allow writing to external storage --> |
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
|||
<!-- Allow access to Bluetooth devices --> |
|||
<uses-permission android:name="android.permission.BLUETOOTH" /> |
|||
<!-- Allow access to the vibrator --> |
|||
<uses-permission android:name="android.permission.VIBRATE" /> |
|||
|
|||
<!-- if you want to capture audio, uncomment this. --> |
|||
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> --> |
|||
|
|||
<!-- Create a Java class extending SDLActivity and place it in a |
|||
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java |
|||
|
|||
then replace "SDLActivity" with the name of your class (e.g. "MyGame") |
|||
in the XML below. |
|||
|
|||
An example Java class can be found in README-android.md |
|||
--> |
|||
<application android:label="@string/app_name" |
|||
android:icon="@mipmap/ic_launcher" |
|||
android:allowBackup="true" |
|||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" |
|||
android:hardwareAccelerated="true" > |
|||
|
|||
<!-- Example of setting SDL hints from AndroidManifest.xml: |
|||
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/> |
|||
--> |
|||
|
|||
<activity android:name="com.macoy.kitty_gridlock.KittyGridlock" |
|||
android:label="@string/app_name" |
|||
android:alwaysRetainTaskState="true" |
|||
android:launchMode="singleInstance" |
|||
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" |
|||
> |
|||
<intent-filter> |
|||
<action android:name="android.intent.action.MAIN" /> |
|||
<category android:name="android.intent.category.LAUNCHER" /> |
|||
</intent-filter> |
|||
<!-- Drop file event --> |
|||
<!-- |
|||
<intent-filter> |
|||
<action android:name="android.intent.action.VIEW" /> |
|||
<category android:name="android.intent.category.DEFAULT" /> |
|||
<data android:mimeType="*/*" /> |
|||
</intent-filter> |
|||
--> |
|||
</activity> |
|||
</application> |
|||
|
|||
</manifest> |
@ -0,0 +1,9 @@ |
|||
package com.macoy.kitty_gridlock; |
|||
|
|||
import org.libsdl.app.SDLActivity; |
|||
|
|||
/** |
|||
* A sample wrapper class that just calls SDLActivity |
|||
*/ |
|||
|
|||
public class KittyGridlock extends SDLActivity { } |
@ -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,684 @@ |
|||
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; |
|||
|
|||
// Make sure we have the HIDAPI library loaded with the native functions
|
|||
try { |
|||
SDL.loadLibrary("hidapi"); |
|||
} catch (Throwable e) { |
|||
Log.w(TAG, "Couldn't load hidapi: " + e.toString()); |
|||
|
|||
AlertDialog.Builder builder = new AlertDialog.Builder(context); |
|||
builder.setCancelable(false); |
|||
builder.setTitle("SDL HIDAPI Error"); |
|||
builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage()); |
|||
builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() { |
|||
@Override |
|||
public void onClick(DialogInterface dialog, int which) { |
|||
try { |
|||
// If our context is an activity, exit rather than crashing when we can't
|
|||
// call our native functions.
|
|||
Activity activity = (Activity)context; |
|||
|
|||
activity.finish(); |
|||
} |
|||
catch (ClassCastException cce) { |
|||
// Context wasn't an activity, there's nothing we can do. Give up and return.
|
|||
} |
|||
} |
|||
}); |
|||
builder.show(); |
|||
|
|||
return; |
|||
} |
|||
|
|||
HIDDeviceRegisterCallback(); |
|||
|
|||
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); |
|||
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); |
|||
|
|||
// if (shouldClear) {
|
|||
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
|||
// spedit.clear();
|
|||
// spedit.commit();
|
|||
// }
|
|||
// else
|
|||
{ |
|||
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); |
|||
} |
|||
|
|||
initializeUSB(); |
|||
initializeBluetooth(); |
|||
} |
|||
|
|||
public Context getContext() { |
|||
return mContext; |
|||
} |
|||
|
|||
public int getDeviceIDForIdentifier(String identifier) { |
|||
SharedPreferences.Editor spedit = mSharedPreferences.edit(); |
|||
|
|||
int result = mSharedPreferences.getInt(identifier, 0); |
|||
if (result == 0) { |
|||
result = mNextDeviceId++; |
|||
spedit.putInt("next_device_id", mNextDeviceId); |
|||
} |
|||
|
|||
spedit.putInt(identifier, result); |
|||
spedit.commit(); |
|||
return result; |
|||
} |
|||
|
|||
private void initializeUSB() { |
|||
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); |
|||
|
|||
/* |
|||
// Logging
|
|||
for (UsbDevice device : mUsbManager.getDeviceList().values()) { |
|||
Log.i(TAG,"Path: " + device.getDeviceName()); |
|||
Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); |
|||
Log.i(TAG,"Product: " + device.getProductName()); |
|||
Log.i(TAG,"ID: " + device.getDeviceId()); |
|||
Log.i(TAG,"Class: " + device.getDeviceClass()); |
|||
Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); |
|||
Log.i(TAG,"Vendor ID " + device.getVendorId()); |
|||
Log.i(TAG,"Product ID: " + device.getProductId()); |
|||
Log.i(TAG,"Interface count: " + device.getInterfaceCount()); |
|||
Log.i(TAG,"---------------------------------------"); |
|||
|
|||
// Get interface details
|
|||
for (int index = 0; index < device.getInterfaceCount(); index++) { |
|||
UsbInterface mUsbInterface = device.getInterface(index); |
|||
Log.i(TAG," ***** *****"); |
|||
Log.i(TAG," Interface index: " + index); |
|||
Log.i(TAG," Interface ID: " + mUsbInterface.getId()); |
|||
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); |
|||
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); |
|||
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); |
|||
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); |
|||
|
|||
// Get endpoint details
|
|||
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) |
|||
{ |
|||
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); |
|||
Log.i(TAG," ++++ ++++ ++++"); |
|||
Log.i(TAG," Endpoint index: " + epi); |
|||
Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); |
|||
Log.i(TAG," Direction: " + mEndpoint.getDirection()); |
|||
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); |
|||
Log.i(TAG," Interval: " + mEndpoint.getInterval()); |
|||
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); |
|||
Log.i(TAG," Type: " + mEndpoint.getType()); |
|||
} |
|||
} |
|||
} |
|||
Log.i(TAG," No more devices connected."); |
|||
*/ |
|||
|
|||
// Register for USB broadcasts and permission completions
|
|||
IntentFilter filter = new IntentFilter(); |
|||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); |
|||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); |
|||
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); |
|||
mContext.registerReceiver(mUsbBroadcast, filter); |
|||
|
|||
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { |
|||
handleUsbDeviceAttached(usbDevice); |
|||
} |
|||
} |
|||
|
|||
UsbManager getUSBManager() { |
|||
return mUsbManager; |
|||
} |
|||
|
|||
private void shutdownUSB() { |
|||
try { |
|||
mContext.unregisterReceiver(mUsbBroadcast); |
|||
} catch (Exception e) { |
|||
// We may not have registered, that's okay
|
|||
} |
|||
} |
|||
|
|||
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { |
|||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { |
|||
return true; |
|||
} |
|||
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { |
|||
final int XB360_IFACE_SUBCLASS = 93; |
|||
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
|||
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
|||
final int[] SUPPORTED_VENDORS = { |
|||
0x0079, // GPD Win 2
|
|||
0x044f, // Thrustmaster
|
|||
0x045e, // Microsoft
|
|||
0x046d, // Logitech
|
|||
0x056e, // Elecom
|
|||
0x06a3, // Saitek
|
|||
0x0738, // Mad Catz
|
|||
0x07ff, // Mad Catz
|
|||
0x0e6f, // PDP
|
|||
0x0f0d, // Hori
|
|||
0x1038, // SteelSeries
|
|||
0x11c9, // Nacon
|
|||
0x12ab, // Unknown
|
|||
0x1430, // RedOctane
|
|||
0x146b, // BigBen
|
|||
0x1532, // Razer Sabertooth
|
|||
0x15e4, // Numark
|
|||
0x162e, // Joytech
|
|||
0x1689, // Razer Onza
|
|||
0x1bad, // Harmonix
|
|||
0x24c6, // PowerA
|
|||
}; |
|||
|
|||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && |
|||
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && |
|||
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || |
|||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { |
|||
int vendor_id = usbDevice.getVendorId(); |
|||
for (int supportedVid : SUPPORTED_VENDORS) { |
|||
if (vendor_id == supportedVid) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { |
|||
final int XB1_IFACE_SUBCLASS = 71; |
|||
final int XB1_IFACE_PROTOCOL = 208; |
|||
final int[] SUPPORTED_VENDORS = { |
|||
0x045e, // Microsoft
|
|||
0x0738, // Mad Catz
|
|||
0x0e6f, // PDP
|
|||
0x0f0d, // Hori
|
|||
0x1532, // Razer Wildcat
|
|||
0x24c6, // PowerA
|
|||
0x2e24, // Hyperkin
|
|||
}; |
|||
|
|||
if (usbInterface.getId() == 0 && |
|||
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && |
|||
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && |
|||
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { |
|||
int vendor_id = usbDevice.getVendorId(); |
|||
for (int supportedVid : SUPPORTED_VENDORS) { |
|||
if (vendor_id == supportedVid) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private void handleUsbDeviceAttached(UsbDevice usbDevice) { |
|||
connectHIDDeviceUSB(usbDevice); |
|||
} |
|||
|
|||
private void handleUsbDeviceDetached(UsbDevice usbDevice) { |
|||
List<Integer> devices = new ArrayList<Integer>(); |
|||
for (HIDDevice device : mDevicesById.values()) { |
|||
if (usbDevice.equals(device.getDevice())) { |
|||
devices.add(device.getId()); |
|||
} |
|||
} |
|||
for (int id : devices) { |
|||
HIDDevice device = mDevicesById.get(id); |
|||
mDevicesById.remove(id); |
|||
device.shutdown(); |
|||
HIDDeviceDisconnected(id); |
|||
} |
|||
} |
|||
|
|||
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { |
|||
for (HIDDevice device : mDevicesById.values()) { |
|||
if (usbDevice.equals(device.getDevice())) { |
|||
boolean opened = false; |
|||
if (permission_granted) { |
|||
opened = device.open(); |
|||
} |
|||
HIDDeviceOpenResult(device.getId(), opened); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void connectHIDDeviceUSB(UsbDevice usbDevice) { |
|||
synchronized (this) { |
|||
int interface_mask = 0; |
|||
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { |
|||
UsbInterface usbInterface = usbDevice.getInterface(interface_index); |
|||
if (isHIDDeviceInterface(usbDevice, usbInterface)) { |
|||
// Check to see if we've already added this interface
|
|||
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
|||
int interface_id = usbInterface.getId(); |
|||
if ((interface_mask & (1 << interface_id)) != 0) { |
|||
continue; |
|||
} |
|||
interface_mask |= (1 << interface_id); |
|||
|
|||
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); |
|||
int id = device.getId(); |
|||
mDevicesById.put(id, device); |
|||
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void initializeBluetooth() { |
|||
Log.d(TAG, "Initializing Bluetooth"); |
|||
|
|||
if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { |
|||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); |
|||
return; |
|||
} |
|||
|
|||
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); |
|||