android support

mastering_effects
Nikita Krapivin 2021-10-29 16:00:03 -07:00 committed by cosmonaut
parent e63252b3ba
commit 601aebaadd
32 changed files with 3583 additions and 9 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.vs .vs
visualc/x64 visualc/x64
build/ build/
android/buildandroid/

68
android/Android.mk Normal file
View File

@ -0,0 +1,68 @@
# FAudioGMS Android.mk file
# PS: Expect hell
SAVED_LOCAL_PATH := $(call my-dir)
LOCAL_PATH := $(SAVED_LOCAL_PATH)
SDL_PATH := $(LOCAL_PATH)/../lib/SDL
FAUDIO_PATH := $(LOCAL_PATH)/../lib/FAudio
FAUDIOGMS_PATH := $(LOCAL_PATH)/..
# First we import SDL 2
include $(SDL_PATH)/Android.mk
# Then we compile FAudio as a static library
include $(CLEAR_VARS)
LOCAL_PATH := $(SAVED_LOCAL_PATH)
LOCAL_MODULE := FAudio_static
LOCAL_MODULE_FILENAME := libFAudio
LOCAL_SHARED_LIBRARIES := SDL2
LOCAL_C_INCLUDES := $(SDL_PATH)/include $(FAUDIO_PATH)/include $(FAUDIO_PATH)/src
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
LOCAL_LDLIBS :=
LOCAL_EXPORT_LDLIBS := -ldl -llog -landroid
LOCAL_SRC_FILES := \
$(FAUDIO_PATH)/src/F3DAudio.c \
$(FAUDIO_PATH)/src/FACT3D.c \
$(FAUDIO_PATH)/src/FACT.c \
$(FAUDIO_PATH)/src/FACT_internal.c \
$(FAUDIO_PATH)/src/FAPOBase.c \
$(FAUDIO_PATH)/src/FAPOFX.c \
$(FAUDIO_PATH)/src/FAPOFX_echo.c \
$(FAUDIO_PATH)/src/FAPOFX_eq.c \
$(FAUDIO_PATH)/src/FAPOFX_masteringlimiter.c \
$(FAUDIO_PATH)/src/FAPOFX_reverb.c \
$(FAUDIO_PATH)/src/FAudio.c \
$(FAUDIO_PATH)/src/FAudioFX_reverb.c \
$(FAUDIO_PATH)/src/FAudioFX_volumemeter.c \
$(FAUDIO_PATH)/src/FAudio_internal.c \
$(FAUDIO_PATH)/src/FAudio_internal_simd.c \
$(FAUDIO_PATH)/src/FAudio_operationset.c \
$(FAUDIO_PATH)/src/FAudio_platform_sdl2.c \
$(FAUDIO_PATH)/src/FAudio_platform_win32.c \
$(FAUDIO_PATH)/src/XNA_Song.c \
$(FAUDIO_PATH)/src/FAudio_gstreamer.c
include $(BUILD_STATIC_LIBRARY)
# And then we do our stuff...
include $(CLEAR_VARS)
LOCAL_PATH := $(SAVED_LOCAL_PATH)
LOCAL_MODULE := FAudioGMS
# Tell ndk-build we rely on these two fellas:
LOCAL_SHARED_LIBRARIES := SDL2 FAudio_static
LOCAL_C_INCLUDES := $(SDL_PATH)/include $(FAUDIO_PATH)/include $(FAUDIOGMS_PATH)/src
LOCAL_SRC_FILES := $(FAUDIOGMS_PATH)/src/FAudioGMS.c $(LOCAL_PATH)/FAudioGMS_JNI.c
include $(BUILD_SHARED_LIBRARY)

388
android/FAudioGMS_JNI.c Normal file
View File

@ -0,0 +1,388 @@
/* FAudioGMS - Game Maker FAudio bindings in C
*
* Copyright (c) 2021 Evan Hemsley
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Evan "cosmonaut" Hemsley <evan@moonside.games>
*
*/
/* These are the Native -> JNI conv wrappers, they must only be built for Android */
#ifdef __ANDROID__
#include <jni.h>
#include <FAudioGMS.h>
/*
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1Init
JNIEXPORT: export this function for JNI
jdouble: return type, a GameMaker function must always return something
JNICALL: JNI calling convention
Java_class_path_here_classNameHere_Function_Name_Here
classpath: org.screwyoyo.faudiogms
classname: FAudioGMSNative
function name: FAudioGMSNative_FAudioGMS_1Init
underscores must be escaped with _1
*/
/* replace this with -1.0 or NAN if you wish... */
/* ideally, a jdouble should map to a double */
#define NOTHING ((jdouble)0.0)
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1Init
(JNIEnv* jniEnv, jclass jniThis, jdouble _spatialDistanceScale, jdouble _timestep)
{
FAudioGMS_Init(_spatialDistanceScale, _timestep);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1StaticSound_1LoadWAV
(JNIEnv* jniEnv, jclass jniThis, jstring _filePath)
{
jboolean isCopy;
const char* filePath;
jdouble ret;
filePath = (*jniEnv)->GetStringUTFChars(jniEnv, _filePath, &isCopy);
ret = FAudioGMS_StaticSound_LoadWAV((char *)filePath);
(*jniEnv)->ReleaseStringUTFChars(jniEnv, _filePath, filePath);
return ret;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1StaticSound_1CreateSoundInstance
(JNIEnv* jniEnv, jclass jniThis, jdouble _staticSoundID)
{
return (jdouble)FAudioGMS_StaticSound_CreateSoundInstance(_staticSoundID);
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1StaticSound_1Destroy
(JNIEnv* jniEnv, jclass jniThis, jdouble _staticSoundID)
{
FAudioGMS_StaticSound_Destroy(_staticSoundID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1StreamingSound_1LoadOGG
(JNIEnv* jniEnv, jclass jniThis, jstring _filepath)
{
jboolean isCopy;
const char* filepath;
jdouble ret;
filepath = (*jniEnv)->GetStringUTFChars(jniEnv, _filepath, &isCopy);
ret = FAudioGMS_StreamingSound_LoadOGG((char *)filepath);
(*jniEnv)->ReleaseStringUTFChars(jniEnv, _filepath, filepath);
return ret;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Play
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _loop)
{
FAudioGMS_SoundInstance_Play(_soundInstanceID, _loop);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Pause
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
FAudioGMS_SoundInstance_Pause(_soundInstanceID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Stop
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
FAudioGMS_SoundInstance_Stop(_soundInstanceID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetPan
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _pan)
{
FAudioGMS_SoundInstance_SetPan(_soundInstanceID, _pan);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetPitch
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _pitch)
{
FAudioGMS_SoundInstance_SetPitch(_soundInstanceID, _pitch);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetVolume
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _volume)
{
FAudioGMS_SoundInstance_SetVolume(_soundInstanceID, _volume);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Set3DPosition
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _x, jdouble _y, jdouble _z)
{
FAudioGMS_SoundInstance_Set3DPosition(_soundInstanceID, _x, _y, _z);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Set3DVelocity
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _xVelocity, jdouble _yVelocity, jdouble _zVelocity)
{
FAudioGMS_SoundInstance_Set3DVelocity(_soundInstanceID, _xVelocity, _yVelocity, _zVelocity);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetTrackPositionInSeconds
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _trackPositionInSeconds)
{
FAudioGMS_SoundInstance_SetTrackPositionInSeconds(_soundInstanceID, _trackPositionInSeconds);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetVolumeOverTime
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _volume, jdouble _milliseconds)
{
FAudioGMS_SoundInstance_SetVolumeOverTime(_soundInstanceID, _volume, _milliseconds);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetLowPassFilter
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _lowPassFilter, jdouble _Q)
{
FAudioGMS_SoundInstance_SetLowPassFilter(_soundInstanceID, _lowPassFilter, _Q);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetHighPassFilter
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _highPassFilter, jdouble _Q)
{
FAudioGMS_SoundInstance_SetHighPassFilter(_soundInstanceID, _highPassFilter, _Q);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetBandPassFilter
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _bandPassFilter, jdouble _Q)
{
FAudioGMS_SoundInstance_SetBandPassFilter(_soundInstanceID, _bandPassFilter, _Q);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1GetPitch
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
return (jdouble)FAudioGMS_SoundInstance_GetPitch(_soundInstanceID);
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1GetVolume
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
return (jdouble)FAudioGMS_SoundInstance_GetVolume(_soundInstanceID);
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1GetTrackLengthInSeconds
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
return (jdouble)FAudioGMS_SoundInstance_GetTrackLengthInSeconds(_soundInstanceID);
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1GetTrackPositionInSeconds
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
return (jdouble)FAudioGMS_SoundInstance_GetTrackPositionInSeconds(_soundInstanceID);
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1Destroy
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
FAudioGMS_SoundInstance_Destroy(_soundInstanceID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1DestroyWhenFinished
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID)
{
FAudioGMS_SoundInstance_DestroyWhenFinished(_soundInstanceID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1EffectChain_1Create
(JNIEnv* jniEnv, jclass jniThis)
{
return (jdouble)FAudioGMS_EffectChain_Create();
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1EffectChain_1AddDefaultReverb
(JNIEnv* jniEnv, jclass jniThis, jdouble _effectChainID)
{
FAudioGMS_EffectChain_AddDefaultReverb(_effectChainID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1EffectChain_1AddReverb
(JNIEnv* jniEnv, jclass jniThis,
jdouble _effectChainID,
jdouble _wetDryMix,
jdouble _reflectionsDelay,
jdouble _reverbDelay,
jdouble _earlyDiffusion,
jdouble _lateDiffusion,
jdouble _lowEQGain,
jdouble _lowEQCutoff,
jdouble _highEQGain,
jdouble _highEQCutoff,
jdouble _reflectionsGain,
jdouble _reverbGain,
jdouble _decayTime,
jdouble _density,
jdouble _roomSize)
{
FAudioGMS_EffectChain_AddReverb(
_effectChainID,
_wetDryMix,
_reflectionsDelay,
_reverbDelay,
_earlyDiffusion,
_lateDiffusion,
_lowEQGain,
_lowEQCutoff,
_highEQGain,
_highEQCutoff,
_reflectionsGain,
_reverbGain,
_decayTime,
_density,
_roomSize
);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1EffectChain_1Destroy
(JNIEnv* jniEnv, jclass jniThis, jdouble _effectChainID)
{
FAudioGMS_EffectChain_Destroy(_effectChainID);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetEffectChain
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _effectChainID, jdouble _effectGain)
{
FAudioGMS_SoundInstance_SetEffectChain(_soundInstanceID, _effectChainID, _effectGain);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SoundInstance_1SetEffectGain
(JNIEnv* jniEnv, jclass jniThis, jdouble _soundInstanceID, jdouble _effectGain)
{
FAudioGMS_SoundInstance_SetEffectGain(_soundInstanceID, _effectGain);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SetListenerPosition
(JNIEnv* jniEnv, jclass jniThis, jdouble _x, jdouble _y, jdouble _z)
{
FAudioGMS_SetListenerPosition(_x, _y, _z);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1SetListenerVelocity
(JNIEnv* jniEnv, jclass jniThis, jdouble _xVelocity, jdouble _yVelocity, jdouble _zVelocity)
{
FAudioGMS_SetListenerVelocity(_xVelocity, _yVelocity, _zVelocity);
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1PauseAll
(JNIEnv* jniEnv, jclass jniThis)
{
FAudioGMS_PauseAll();
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1ResumeAll
(JNIEnv* jniEnv, jclass jniThis)
{
FAudioGMS_ResumeAll();
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1StopAll
(JNIEnv* jniEnv, jclass jniThis)
{
FAudioGMS_StopAll();
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1Update
(JNIEnv* jniEnv, jclass jniThis)
{
FAudioGMS_Update();
return NOTHING;
}
JNIEXPORT jdouble JNICALL
Java_org_screwyoyo_faudiogms_FAudioGMSNative_FAudioGMS_1Destroy
(JNIEnv* jniEnv, jclass jniThis)
{
FAudioGMS_Destroy();
return NOTHING;
}
#endif /* __ANDROID__ */
/* Do nothing for other platforms, because they, thankly, do not require JNI bindings... */

24
android/build.ndk.sh Normal file
View File

@ -0,0 +1,24 @@
#!/bin/sh
cd `dirname $0`
# rm -rf buildandroid
mkdir -p buildandroid
# Make sure you have ndk-build and Android Sdk stuff in your $PATH!
ndk-build \
NDK_PROJECT_PATH=null \
APP_BUILD_SCRIPT=Android.mk \
APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" \
APP_PLATFORM=android-16 \
APP_MODULES="SDL2 FAudio_static FAudioGMS" \
NDK_OUT=buildandroid/obj \
NDK_LIBS_OUT=buildandroid/lib
# Update gamemaker project folder..
cp -rf buildandroid/lib/* ../gamemaker/extensions/FAudioGMS/AndroidSource/libs
rm -rf buildandroid
# we're done here.

View File

@ -0,0 +1,140 @@
package ${YYAndroidPackageName}; /* this class will reside in Runner's package namespace */
import java.lang.String;
import android.util.Log;
import android.content.Intent;
import android.content.res.Configuration;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.app.Dialog;
import android.view.MotionEvent;
import org.screwyoyo.faudiogms.FAudioGMSNative;
import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDL;
import org.libsdl.app.SDLAudioManager;
import com.yoyogames.runner.RunnerJNILib;
import android.content.res.AssetManager;
public class FAudioGMSBridge extends FAudioGMSNative implements IExtensionBase
{
public SDLActivity sdl;
public boolean paused;
public FAudioGMSBridge()
{
super();
paused = false;
SDL.setContext(RunnerJNILib.GetApplicationContext());
sdl = new SDLActivity();
}
public void Init()
{
SDL.setContext(RunnerJNILib.GetApplicationContext());
sdl.onCreate(null);
}
public void onStart()
{
SDL.setContext(RunnerJNILib.GetApplicationContext());
sdl.onStart();
}
public void onRestart()
{
onStart();
}
public void onStop()
{
sdl.onStop();
}
public void onDestroy()
{
sdl.onDestroy();
}
public void onPause()
{
sdl.onPause();
if (!paused)
{
paused = true;
FAudioGMS_PauseAll();
}
}
public void onResume()
{
sdl.onResume();
if (paused)
{
paused = false;
FAudioGMS_ResumeAll();
}
}
public void onWindowFocusChanged(boolean hasFocus)
{
sdl.onWindowFocusChanged(hasFocus);
}
public void onConfigurationChanged(Configuration newConfig)
{
sdl.onConfigurationChanged(newConfig);
}
public void onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults)
{
sdl.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public Dialog onCreateDialog(int id)
{
return null;
}
public boolean onTouchEvent(final MotionEvent event)
{
return false;
}
public boolean onGenericMotionEvent(MotionEvent event)
{
return false;
}
public boolean dispatchKeyEvent(KeyEvent event)
{
return false;
}
public boolean dispatchGenericMotionEvent(MotionEvent event)
{
return false;
}
public boolean performClick()
{
return false;
}
public void onNewIntent(android.content.Intent newIntent)
{
}
public void onActivityResult(int requestCode, int resultCode, Intent data){}
public boolean onKeyLongPress(int keyCode, KeyEvent event){return false;}
public boolean onCreateOptionsMenu( Menu menu ){return false;}
public boolean onOptionsItemSelected( MenuItem item ){return false;}
public boolean onKeyDown( int keyCode, KeyEvent event )
{ return false;}
public boolean onKeyUp( int keyCode, KeyEvent event ){return false;}
}

View File

@ -0,0 +1,15 @@
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 28
}
repositories {
mavenCentral()
}
dependencies {
compile 'com.getkeepsafe.relinker:relinker:1.4.4'
}

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.screwyoyo.faudiogms">
</manifest>

View File

@ -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();
}

View File

@ -0,0 +1,649 @@
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();
}
}

View File

@ -0,0 +1,685 @@
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
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 (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 openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0));
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public int sendOutputReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendOutputReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public int sendFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public boolean getFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.getFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
}

View File

@ -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);
}
}
}
}
}
}

View File

@ -0,0 +1,83 @@
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() {
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;
}

View File

@ -0,0 +1,616 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.UiModeManager;
import android.content.ClipboardManager;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Hashtable;
import java.util.Locale;
/**
SDL Activity
*/
public class SDLActivity {
private static final String TAG = "yoyo";
public static boolean mIsResumedCalled, mHasFocus;
public static Locale mCurrentLocale;
// Handle the state of the native layer
public enum NativeState {
INIT, RESUMED, PAUSED
}
public static NativeState mNextNativeState;
public static NativeState mCurrentNativeState;
// Main components
public static SDLActivity mSingleton;
/**
* This method is called by SDL before loading the native shared libraries.
* It can be overridden to provide names of shared libraries to be loaded.
* The default implementation returns the defaults. It never returns null.
* An array returned by a new implementation must at least contain "SDL2".
* Also keep in mind that the order the libraries are loaded may matter.
* @return names of shared libraries to be loaded (e.g. "SDL2", "main").
*/
public String[] getLibraries() {
return new String[] {
"hidapi",
"SDL2",
"FAudioGMS"
};
}
// Load the .so
public void loadLibraries() {
for (String lib : getLibraries()) {
SDL.loadLibrary(lib);
}
}
public static void initialize() {
// The static nature of the singleton and Android quirkyness force us to initialize everything here
// Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
mSingleton = null;
mIsResumedCalled = false;
mHasFocus = true;
mNextNativeState = NativeState.INIT;
mCurrentNativeState = NativeState.INIT;
}
// Setup
public void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "Device: " + Build.DEVICE);
Log.v(TAG, "Model: " + Build.MODEL);
Log.v(TAG, "onCreate()");
try {
Thread.currentThread().setName("SDLActivity");
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
// Load shared libraries
loadLibraries();
// Set up JNI
SDL.setupJNI();
// Initialize state
SDL.initialize();
// So we can call stuff from static callbacks
mSingleton = this;
}
public void pauseNativeThread() {
mNextNativeState = NativeState.PAUSED;
mIsResumedCalled = false;
SDLActivity.handleNativeState();
}
public void resumeNativeThread() {
mNextNativeState = NativeState.RESUMED;
mIsResumedCalled = true;
SDLActivity.handleNativeState();
}
// Events
public void onPause() {
Log.v(TAG, "onPause()");
pauseNativeThread();
}
public void onResume() {
Log.v(TAG, "onResume()");
resumeNativeThread();
}
public void onStop() {
Log.v(TAG, "onStop()");
pauseNativeThread();
}
public void onStart() {
Log.v(TAG, "onStart()");
resumeNativeThread();
}
public void onWindowFocusChanged(boolean hasFocus) {
Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
mHasFocus = hasFocus;
if (hasFocus) {
mNextNativeState = NativeState.RESUMED;
SDLActivity.handleNativeState();
nativeFocusChanged(true);
} else {
nativeFocusChanged(false);
mNextNativeState = NativeState.PAUSED;
SDLActivity.handleNativeState();
}
}
public void onLowMemory() {
Log.v(TAG, "onLowMemory()");
SDLActivity.nativeLowMemory();
}
public void onConfigurationChanged(Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged()");
if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) {
mCurrentLocale = newConfig.locale;
SDLActivity.onNativeLocaleChanged();
}
}
public void onDestroy() {
Log.v(TAG, "onDestroy()");
SDLActivity.nativeSendQuit();
SDLActivity.nativeQuit();
}
// Called by JNI from SDL.
public static void manualBackButton() {
}
/* Transition to next state */
public static void handleNativeState() {
if (mNextNativeState == mCurrentNativeState) {
// Already in same state, discard.
return;
}
// Try a transition to init state
if (mNextNativeState == NativeState.INIT) {
mCurrentNativeState = mNextNativeState;
return;
}
// Try a transition to paused state
if (mNextNativeState == NativeState.PAUSED) {
nativePause();
mCurrentNativeState = mNextNativeState;
Log.i("yoyo", "SDL - PAUSE!");
return;
}
// Try a transition to resumed state
if (mNextNativeState == NativeState.RESUMED) {
nativeResume();
mCurrentNativeState = mNextNativeState;
Log.i("yoyo", "SDL - RESUME!");
return;
}
}
/**
* This method is called by SDL if SDL did not handle a message itself.
* This happens if a received message contains an unsupported command.
* Method can be overwritten to handle Messages in a different class.
* @param command the command of the message.
* @param param the parameter of the message. May be null.
* @return if the message was handled in overridden method.
*/
public boolean onUnhandledMessage(int command, Object param) {
return false;
}
/**
* A Handler class for Messages from native SDL applications.
* It uses current Activities as target (e.g. for the title).
* static to prevent implicit references to enclosing object.
*/
public static class SDLCommandHandler extends Handler {
@Override
public void handleMessage(Message msg) {
}
}
// Handler for the messages
Handler commandHandler = new SDLCommandHandler();
// Send a message from the SDLMain thread
boolean sendCommand(int command, Object data) {
Message msg = commandHandler.obtainMessage();
msg.arg1 = command;
msg.obj = data;
boolean result = commandHandler.sendMessage(msg);
return result;
}
// C functions we call
public static native int nativeSetupJNI();
public static native int nativeRunMain(String library, String function, Object arguments);
public static native void nativeLowMemory();
public static native void nativeSendQuit();
public static native void nativeQuit();
public static native void nativePause();
public static native void nativeResume();
public static native void nativeFocusChanged(boolean hasFocus);
public static native void onNativeDropFile(String filename);
public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate);
public static native void onNativeResize();
public static native void onNativeKeyDown(int keycode);
public static native void onNativeKeyUp(int keycode);
public static native boolean onNativeSoftReturnKey();
public static native void onNativeKeyboardFocusLost();
public static native void onNativeMouse(int button, int action, float x, float y, boolean relative);
public static native void onNativeTouch(int touchDevId, int pointerFingerId,
int action, float x,
float y, float p);
public static native void onNativeAccel(float x, float y, float z);
public static native void onNativeClipboardChanged();
public static native void onNativeSurfaceCreated();
public static native void onNativeSurfaceChanged();
public static native void onNativeSurfaceDestroyed();
public static native String nativeGetHint(String name);
public static native void nativeSetenv(String name, String value);
public static native void onNativeOrientationChanged(int orientation);
public static native void nativeAddTouch(int touchId, String name);
public static native void nativePermissionResult(int requestCode, boolean result);
public static native void onNativeLocaleChanged();
/**
* This method is called by SDL using JNI.
*/
public static boolean setActivityTitle(String title) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static void setWindowStyle(boolean fullscreen) {
}
/**
* This method is called by SDL using JNI.
* This is a static method for JNI convenience, it calls a non-static method
* so that is can be overridden
*/
public static void setOrientation(int w, int h, boolean resizable, String hint){
}
/**
* This can be overridden
*/
public void setOrientationBis(int w, int h, boolean resizable, String hint){
}
/**
* This method is called by SDL using JNI.
*/
public static void minimizeWindow() {
}
/**
* This method is called by SDL using JNI.
*/
public static boolean shouldMinimizeOnFocusLoss() {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean isScreenKeyboardShown(){
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean supportsRelativeMouse(){
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean sendMessage(int command, int param) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static Context getContext() {
return SDL.getContext();
}
/**
* This method is called by SDL using JNI.
*/
public static boolean isAndroidTV() {
return false;
}
public static double getDiagonal() {
return 0.0;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean isTablet() {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean isChromebook() {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean isDeXMode() {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static DisplayMetrics getDisplayDPI() {
return getContext().getResources().getDisplayMetrics();
}
/**
* This method is called by SDL using JNI.
*/
public static boolean getManifestEnvironmentVariables() {
Log.i("yoyo", "sdl envvars req!");
nativeSetenv("SDL_ANDROID_BLOCK_ON_PAUSE", "1");
nativeSetenv("SDL_ANDROID_BLOCK_ON_PAUSE_PAUSEAUDIO", "1");
Log.i("yoyo", "sdl envvars ok!");
return true;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean showTextInput(int x, int y, int w, int h) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static Surface getNativeSurface() {
return null;
}
// Input
/**
* This method is called by SDL using JNI.
*/
public static void initTouch() {
}
// Messagebox
/** Result of current messagebox. Also used for blocking the calling thread. */
public final int[] messageboxSelection = new int[1];
/**
* This method is called by SDL using JNI.
* Shows the messagebox from UI thread and block calling thread.
* buttonFlags, buttonIds and buttonTexts must have same length.
* @param buttonFlags array containing flags for every button.
* @param buttonIds array containing id for every button.
* @param buttonTexts array containing text for every button.
* @param colors null for default or array of length 5 containing colors.
* @return button id or -1.
*/
public int messageboxShowMessageBox(
final int flags,
final String title,
final String message,
final int[] buttonFlags,
final int[] buttonIds,
final String[] buttonTexts,
final int[] colors) {
return -1;
}
public void messageboxCreateAndShow(Bundle args) {
}
private final Runnable rehideSystemUi = new Runnable() {
@Override
public void run() {
}
};
public void onSystemUiVisibilityChange(int visibility) {
}
/**
* This method is called by SDL using JNI.
*/
public static boolean clipboardHasText() {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static String clipboardGetText() {
return "";
}
/**
* This method is called by SDL using JNI.
*/
public static void clipboardSetText(String string) {
}
/**
* This method is called by SDL using JNI.
*/
public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) {
return 0;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean setCustomCursor(int cursorID) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static boolean setSystemCursor(int cursorID) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static void requestPermission(String permission, int requestCode) {
if (Build.VERSION.SDK_INT < 23) {
nativePermissionResult(requestCode, true);
return;
}
Activity activity = (Activity)getContext();
if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(new String[]{permission}, requestCode);
} else {
nativePermissionResult(requestCode, true);
}
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
boolean result = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED);
nativePermissionResult(requestCode, result);
}
/**
* This method is called by SDL using JNI.
*/
public static int openURL(String url) {
return -1;
}
/**
* This method is called by SDL using JNI.
*/
public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset) {
return -1;
}
}
/**
Simple runnable to start the SDL application
*/
class SDLMain implements Runnable {
@Override
public void run() {
try {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
} catch (Exception e) {
Log.v("SDL", "modify thread properties failed " + e.toString());
}
}
}
class SDLInputConnection extends BaseInputConnection {
public SDLInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
public boolean sendKeyEvent(KeyEvent event) {
return false;
}
public boolean commitText(CharSequence text, int newCursorPosition) {
return false;
}
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return false;
}
public static native void nativeCommitText(String text, int newCursorPosition);
public native void nativeGenerateScancodeForUnichar(char c);
public native void nativeSetComposingText(String text, int newCursorPosition);
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return false;
}
}

View File

@ -0,0 +1,390 @@
package org.libsdl.app;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
public class SDLAudioManager
{
protected static final String TAG = "yoyo";
protected static AudioTrack mAudioTrack;
protected static AudioRecord mAudioRecord;
public static void initialize() {
mAudioTrack = null;
mAudioRecord = null;
}
// Audio
protected static String getAudioFormatString(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return "8-bit";
case AudioFormat.ENCODING_PCM_16BIT:
return "16-bit";
case AudioFormat.ENCODING_PCM_FLOAT:
return "float";
default:
return Integer.toString(audioFormat);
}
}
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
int channelConfig;
int sampleSize;
int frameSize;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
/* On older devices let's use known good settings */
if (Build.VERSION.SDK_INT < 21) {
if (desiredChannels > 2) {
desiredChannels = 2;
}
if (sampleRate < 8000) {
sampleRate = 8000;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
}
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
int minSDKVersion = (isCapture ? 23 : 21);
if (Build.VERSION.SDK_INT < minSDKVersion) {
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
}
}
switch (audioFormat)
{
case AudioFormat.ENCODING_PCM_8BIT:
sampleSize = 1;
break;
case AudioFormat.ENCODING_PCM_16BIT:
sampleSize = 2;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
sampleSize = 4;
break;
default:
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
sampleSize = 2;
break;
}
if (isCapture) {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_IN_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
}
} else {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 3:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 7:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
break;
case 8:
if (Build.VERSION.SDK_INT >= 23) {
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else {
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
desiredChannels = 6;
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
}
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
}
/*
Log.v(TAG, "Speaker configuration (and order of channels):");
if ((channelConfig & 0x00000004) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
}
if ((channelConfig & 0x00000008) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
}
if ((channelConfig & 0x00000010) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
}
if ((channelConfig & 0x00000020) != 0) {
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
}
if ((channelConfig & 0x00000040) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
}
if ((channelConfig & 0x00000080) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
}
if ((channelConfig & 0x00000100) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
}
if ((channelConfig & 0x00000200) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
}
if ((channelConfig & 0x00000400) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
}
if ((channelConfig & 0x00000800) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
}
if ((channelConfig & 0x00001000) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
}
*/
}
frameSize = (sampleSize * desiredChannels);
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
int minBufferSize;
if (isCapture) {
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
} else {
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
}
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
int[] results = new int[4];
if (isCapture) {
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize);
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Failed during initialization of AudioRecord");
mAudioRecord.release();
mAudioRecord = null;
return null;
}
mAudioRecord.startRecording();
}
results[0] = mAudioRecord.getSampleRate();
results[1] = mAudioRecord.getAudioFormat();
results[2] = mAudioRecord.getChannelCount();
} else {
if (mAudioTrack == null) {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
/* Try again, with safer values */
Log.e(TAG, "Failed during initialization of Audio Track");
mAudioTrack.release();
mAudioTrack = null;
return null;
}
mAudioTrack.play();
}
results[0] = mAudioTrack.getSampleRate();
results[1] = mAudioTrack.getAudioFormat();
results[2] = mAudioTrack.getChannelCount();
}
results[3] = desiredFrames;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
return results;
}
/**
* This method is called by SDL using JNI.
*/
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteFloatBuffer(float[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(float)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteShortBuffer(short[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(short)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteByteBuffer(byte[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length; ) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(byte)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
}
/** This method is called by SDL using JNI. */
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
/** This method is called by SDL using JNI. */
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static void audioClose() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
/** This method is called by SDL using JNI. */
public static void captureClose() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
/** This method is called by SDL using JNI. */
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
try {
/* Set thread name */
if (iscapture) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
public static native int nativeSetupJNI();
}

View File

@ -0,0 +1,92 @@
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);
private static final String TAG = "SDLControllerManager";
public static void initialize() {
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
public static boolean handleJoystickMotionEvent(MotionEvent event) {
return false;
}
/**
* This method is called by SDL using JNI.
*/
public static void pollInputDevices() {
}
/**
* This method is called by SDL using JNI.
*/
public static void pollHapticDevices() {
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticRun(int device_id, float intensity, int length) {
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticStop(int device_id){
}
// Check if a given device is considered a possible SDL joystick
public static boolean isDeviceSDLJoystick(int deviceId) {
return false;
}
}
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() {
}
}

View File

@ -0,0 +1,81 @@
package org.screwyoyo.faudiogms;
import java.lang.String;
public class FAudioGMSNative
{
public FAudioGMSNative()
{
super();
}
/* exactly as in FAudioGMS_JNI.c: */
public native double FAudioGMS_Init(double spatialDistanceScale, double timestep);
public native double FAudioGMS_StaticSound_LoadWAV(String filePath);
public native double FAudioGMS_StaticSound_CreateSoundInstance(double staticSoundID);
public native double FAudioGMS_StaticSound_Destroy(double staticSoundID);
public native double FAudioGMS_StreamingSound_LoadOGG(String filepath);
public native double FAudioGMS_SoundInstance_Play(double soundInstanceID, double loop);
public native double FAudioGMS_SoundInstance_Pause(double soundInstanceID);
public native double FAudioGMS_SoundInstance_Stop(double soundInstanceID);
public native double FAudioGMS_SoundInstance_SetPan(double soundInstanceID, double pan);
public native double FAudioGMS_SoundInstance_SetPitch(double soundInstanceID, double pitch);
public native double FAudioGMS_SoundInstance_SetVolume(double soundInstanceID, double volume);
public native double FAudioGMS_SoundInstance_Set3DPosition(double soundInstanceID, double x, double y, double z);
public native double FAudioGMS_SoundInstance_Set3DVelocity(double soundInstanceID, double xVelocity, double yVelocity, double zVelocity);
public native double FAudioGMS_SoundInstance_SetTrackPositionInSeconds(double soundInstanceID, double trackPositionInSeconds);
public native double FAudioGMS_SoundInstance_SetVolumeOverTime(double soundInstanceID, double volume, double milliseconds);
public native double FAudioGMS_SoundInstance_SetLowPassFilter(double soundInstanceID, double lowPassFilter, double Q);
public native double FAudioGMS_SoundInstance_SetHighPassFilter(double soundInstanceID, double highPassFilter, double Q);
public native double FAudioGMS_SoundInstance_SetBandPassFilter(double soundInstanceID, double bandPassFilter, double Q);
public native double FAudioGMS_SoundInstance_GetPitch(double soundInstanceID);
public native double FAudioGMS_SoundInstance_GetVolume(double soundInstanceID);
public native double FAudioGMS_SoundInstance_GetTrackLengthInSeconds(double soundInstanceID);
public native double FAudioGMS_SoundInstance_GetTrackPositionInSeconds(double soundInstanceID);
public native double FAudioGMS_SoundInstance_Destroy(double soundInstanceID);
public native double FAudioGMS_SoundInstance_DestroyWhenFinished(double soundInstanceID);
public native double FAudioGMS_EffectChain_Create();
public native double FAudioGMS_EffectChain_AddDefaultReverb(double effectChainID);
public native double FAudioGMS_EffectChain_AddReverb(
double effectChainID,
double wetDryMix,
double reflectionsDelay,
double reverbDelay,
double earlyDiffusion,
double lateDiffusion,
double lowEQGain,
double lowEQCutoff,
double highEQGain,
double highEQCutoff,
double reflectionsGain,
double reverbGain,
double decayTime,
double density,
double roomSize
);
public native double FAudioGMS_EffectChain_Destroy(double effectChainID);
/*
* NOTE: Any changes to the effect chain will NOT apply after this is set!
* You MUST call SetEffectChain again if you make changes to the effect chain parameters!
*/
public native double FAudioGMS_SoundInstance_SetEffectChain(double soundInstanceID, double effectChainID, double effectGain);
public native double FAudioGMS_SoundInstance_SetEffectGain(double soundInstanceID, double effectGain);
public native double FAudioGMS_SetListenerPosition(double x, double y, double z);
public native double FAudioGMS_SetListenerVelocity(double xVelocity, double yVelocity, double zVelocity);
public native double FAudioGMS_PauseAll(); /* mobile platforms, man... */
public native double FAudioGMS_ResumeAll(); /* same thing here */
public native double FAudioGMS_StopAll();
public native double FAudioGMS_Update();
public native double FAudioGMS_Destroy();
}

View File

@ -3,17 +3,17 @@
"options": [], "options": [],
"exportToGame": true, "exportToGame": true,
"supportedTargets": -1, "supportedTargets": -1,
"extensionVersion": "0.2.0", "extensionVersion": "0.3.0",
"packageId": "", "packageId": "",
"productId": "", "productId": "",
"author": "", "author": "",
"date": "2021-10-21T16:46:44.6241287-07:00", "date": "2021-10-22T04:46:44.6241287+05:00",
"license": "", "license": "",
"description": "", "description": "",
"helpfile": "", "helpfile": "",
"iosProps": false, "iosProps": false,
"tvosProps": false, "tvosProps": false,
"androidProps": false, "androidProps": true,
"installdir": "", "installdir": "",
"files": [ "files": [
{"filename":"FAudioGMS.dll","origname":"","init":"","final":"","kind":1,"uncompress":false,"functions":[ {"filename":"FAudioGMS.dll","origname":"","init":"","final":"","kind":1,"uncompress":false,"functions":[
@ -160,7 +160,8 @@
],"constants":[],"ProxyFiles":[ ],"constants":[],"ProxyFiles":[
{"TargetMask":7,"resourceVersion":"1.0","name":"libFAudioGMS.so","tags":[],"resourceType":"GMProxyFile",}, {"TargetMask":7,"resourceVersion":"1.0","name":"libFAudioGMS.so","tags":[],"resourceType":"GMProxyFile",},
{"TargetMask":7,"resourceVersion":"1.0","name":"libSDL2.so","tags":[],"resourceType":"GMProxyFile",}, {"TargetMask":7,"resourceVersion":"1.0","name":"libSDL2.so","tags":[],"resourceType":"GMProxyFile",},
],"copyToTargets":192,"order":[ {"TargetMask":3,"resourceVersion":"1.0","name":"FAudioGMSAndroidDummy.ext","tags":[],"resourceType":"GMProxyFile",},
],"copyToTargets":200,"order":[
{"name":"FAudioGMS_Init","path":"extensions/FAudioGMS/FAudioGMS.yy",}, {"name":"FAudioGMS_Init","path":"extensions/FAudioGMS/FAudioGMS.yy",},
{"name":"FAudioGMS_StaticSound_LoadWAV","path":"extensions/FAudioGMS/FAudioGMS.yy",}, {"name":"FAudioGMS_StaticSound_LoadWAV","path":"extensions/FAudioGMS/FAudioGMS.yy",},
{"name":"FAudioGMS_StaticSound_CreateSoundInstance","path":"extensions/FAudioGMS/FAudioGMS.yy",}, {"name":"FAudioGMS_StaticSound_CreateSoundInstance","path":"extensions/FAudioGMS/FAudioGMS.yy",},
@ -204,7 +205,7 @@
"tvosclassname": null, "tvosclassname": null,
"tvosdelegatename": null, "tvosdelegatename": null,
"iosdelegatename": "", "iosdelegatename": "",
"androidclassname": "", "androidclassname": "FAudioGMSBridge",
"sourcedir": "", "sourcedir": "",
"androidsourcedir": "", "androidsourcedir": "",
"macsourcedir": "", "macsourcedir": "",
@ -228,7 +229,7 @@
"tvosThirdPartyFrameworkEntries": [], "tvosThirdPartyFrameworkEntries": [],
"IncludedResources": [], "IncludedResources": [],
"androidPermissions": [], "androidPermissions": [],
"copyToTargets": 192, "copyToTargets": 200,
"iosCocoaPods": "", "iosCocoaPods": "",
"tvosCocoaPods": "", "tvosCocoaPods": "",
"iosCocoaPodDependencies": "", "iosCocoaPodDependencies": "",
@ -241,4 +242,4 @@
"name": "FAudioGMS", "name": "FAudioGMS",
"tags": [], "tags": [],
"resourceType": "GMExtension", "resourceType": "GMExtension",
} }

View File

@ -1,9 +1,16 @@
// working_directory on android is "assets/" which makes SDL freak out.
function GetPathPrepend()
{
if (os_type != os_android) return working_directory;
else return "";
}
// StaticSounds are usually short and intended to be played multiple times. // StaticSounds are usually short and intended to be played multiple times.
// All of the sound data lives in memory for as long as the StaticSound exists. // All of the sound data lives in memory for as long as the StaticSound exists.
// Playing a StaticSound returns a SoundInstance. // Playing a StaticSound returns a SoundInstance.
function LoadStaticSound(filename) function LoadStaticSound(filename)
{ {
var filePath = working_directory + "audio/static/" + filename; var filePath = GetPathPrepend() + "audio/static/" + filename;
var staticSoundID = FAudioGMS_StaticSound_LoadWAV(filePath); var staticSoundID = FAudioGMS_StaticSound_LoadWAV(filePath);
return new StaticSound(staticSoundID); return new StaticSound(staticSoundID);
} }
@ -69,7 +76,7 @@ function StaticSound(_staticSoundID) constructor
// Note that StreamingSounds are SoundInstances. // Note that StreamingSounds are SoundInstances.
function LoadStreamingSound(filename) function LoadStreamingSound(filename)
{ {
var filePath = working_directory + "audio/streaming/" + filename; var filePath = GetPathPrepend() + "audio/streaming/" + filename;
soundInstanceID = FAudioGMS_StreamingSound_LoadOGG(filePath); soundInstanceID = FAudioGMS_StreamingSound_LoadOGG(filePath);
return new SoundInstance(soundInstanceID); return new SoundInstance(soundInstanceID);
} }