android support

main
Nikita Krapivin 2021-10-29 16:00:03 -07:00 committed by cosmonaut
parent 30e4f01d37
commit 3f83d2a825
32 changed files with 3619 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();
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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