android port working

This commit is contained in:
izzy2lost
2026-01-27 20:51:17 -05:00
parent 4d1ec3a2e3
commit 1538ac8f64
67 changed files with 10611 additions and 18 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
url = https://github.com/HarbourMasters/Torch.git
[submodule "libultraship"]
path = libultraship
url = https://github.com/Kenix3/libultraship
url = https://github.com/izzy2lost/libultraship.git
+43 -13
View File
@@ -131,7 +131,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON)
################################################################################
# Global configuration types
################################################################################
if (CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
if (CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch" OR CMAKE_SYSTEM_NAME STREQUAL "Android")
set(CMAKE_C_FLAGS_DEBUG "-g -ffast-math -DDEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "-g -ffast-math -DDEBUG")
set(CMAKE_C_FLAGS_RELEASE "-O3 -ffast-math -DNDEBUG")
@@ -163,21 +163,13 @@ add_compile_definitions(
)
# Find necessary libraries
if (UNIX AND NOT APPLE)
if (UNIX AND NOT APPLE AND NOT ANDROID)
find_package(OpenGL REQUIRED)
endif()
configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/properties.h.in ${CMAKE_CURRENT_SOURCE_DIR}/properties.h @ONLY)
# Include directories
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}/include
${DIRECTORIES_TO_INCLUDE}
)
# Include directories are set per-target to avoid leaking into dependencies.
# Collect source files to build the executable
file(GLOB_RECURSE ALL_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
@@ -259,6 +251,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "iOS")
MACOSX_BUNDLE_INFO_PLIST ${IOS_DIR}/plist.in
RESOURCE "${IMAGE_FILES};${STORYBOARD_FILE};${ICON_FILES}"
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android")
add_library(${PROJECT_NAME} SHARED ${ALL_FILES})
else()
add_executable(${PROJECT_NAME} ${ALL_FILES})
endif()
@@ -302,6 +296,12 @@ add_subdirectory(libultraship ${CMAKE_CURRENT_SOURCE_DIR}/libultraship)
add_dependencies(${PROJECT_NAME} libultraship)
target_link_libraries(${PROJECT_NAME} PRIVATE libultraship)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}/include
${DIRECTORIES_TO_INCLUDE}
${CMAKE_CURRENT_SOURCE_DIR}/libultraship/include
${CMAKE_CURRENT_SOURCE_DIR}/libultraship/include/libultraship
)
@@ -366,6 +366,14 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(ADDITIONAL_LIBRARY_DEPENDENCIES
"$<$<BOOL:${USE_NETWORKING}>:SDL2_net::SDL2_net-static>"
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android")
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
set(ADDITIONAL_LIBRARY_DEPENDENCIES
SDL2::SDL2
Threads::Threads
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
find_package(SDL2)
set(ADDITIONAL_LIBRARY_DEPENDENCIES
@@ -582,6 +590,28 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
-Wno-stringop-overflow
>
)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
target_compile_options(${PROJECT_NAME} PRIVATE
-O2
-Wno-error
-Wno-format-security
-Wno-multichar
-Wno-return-type
-Wno-narrowing
-Wno-switch-outside-range
$<$<COMPILE_LANGUAGE:C>:
-Werror-implicit-function-declaration
-Wno-incompatible-pointer-types
-Wno-discarded-array-qualifiers
-Wno-discarded-qualifiers
-Wno-int-conversion
-Wno-builtin-declaration-mismatch
-Wno-switch-unreachable
-Wno-stringop-overflow
-Wno-implicit-int
>
)
else()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
set(CPU_OPTION -msse2 -mfpmath=sse)
@@ -624,7 +654,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets/" "$<TARGET_FILE_DIR:Ghostship>/assets/"
)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch" AND NOT CMAKE_SYSTEM_NAME STREQUAL "Android" AND NOT CMAKE_SYSTEM_NAME STREQUAL "iOS")
include(ExternalProject)
ExternalProject_Add(TorchExternal
PREFIX TorchExternal
@@ -708,4 +738,4 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
endif()
set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_SOURCE_DIR}/cmake/configure-packaging.cmake)
include(cmake/packaging.cmake)
include(cmake/packaging.cmake)
+34
View File
@@ -0,0 +1,34 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
app/release/
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
+156
View File
@@ -0,0 +1,156 @@
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY');
def buildAsApplication = !buildAsLibrary
if (buildAsApplication) {
apply plugin: 'com.android.application'
}
else {
apply plugin: 'com.android.library'
}
// Load keystore properties
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace("com.ghostship.android")
buildFeatures {
buildConfig true
}
// Signing configuration
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
storeFile file(keystoreProperties['GHOSTSHIP_STORE_FILE'])
storePassword keystoreProperties['GHOSTSHIP_STORE_PASSWORD']
keyAlias keystoreProperties['GHOSTSHIP_KEY_ALIAS']
keyPassword keystoreProperties['GHOSTSHIP_KEY_PASSWORD']
}
}
}
ndkVersion "29.0.14206865"
compileSdkVersion 36
defaultConfig {
if (buildAsApplication) {
applicationId "com.ghostship.android"
}
minSdkVersion 24
targetSdkVersion 36
versionCode 1
versionName "1.0.1"
externalNativeBuild {
cmake {
arguments "-DSDL_SHARED=ON", "-DANDROID_STL=c++_static", "-DHAVE_LD_VERSION_SCRIPT=OFF",'-DUSE_OPENGLES=ON'
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
// Optimize APK size
vectorDrawables {
useSupportLibrary true
}
}
// ✅ Universal APK: disables split APKs per ABI
splits {
abi {
enable false
}
}
buildTypes {
debug {
debuggable true
jniDebuggable true
minifyEnabled false
shrinkResources false
}
release {
debuggable false
jniDebuggable false
// Disable minification temporarily to isolate the issue
minifyEnabled false
shrinkResources false
// Keep debug symbols for crash analysis
ndk {
debugSymbolLevel 'SYMBOL_TABLE'
}
// Sign the release build
if (keystorePropertiesFile.exists()) {
signingConfig signingConfigs.release
}
}
}
applicationVariants.all { variant ->
tasks["merge${variant.name.capitalize()}Assets"]
.dependsOn("externalNativeBuild${variant.name.capitalize()}")
}
if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) {
sourceSets.main {
jniLibs.srcDir 'libs'
}
externalNativeBuild {
cmake {
path '../../CMakeLists.txt'
version "3.31.5"
}
}
}
lintOptions {
abortOnError false
checkReleaseBuilds false
}
// Additional optimizations
packagingOptions {
// Exclude unnecessary files to reduce APK size
excludes += [
'META-INF/DEPENDENCIES',
'META-INF/LICENSE',
'META-INF/LICENSE.txt',
'META-INF/NOTICE',
'META-INF/NOTICE.txt',
'META-INF/*.kotlin_module'
]
// Pick first occurrence of duplicate files
pickFirsts += ['**/libc++_shared.so', '**/libjsc.so']
}
if (buildAsLibrary) {
libraryVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith(".aar")) {
def fileName = "org.libsdl.app.aar";
output.outputFile = new File(outputFile.parent, fileName);
}
}
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.core:core:1.7.0' // Use the latest version
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
}
task wrapper(type: Wrapper) {
gradleVersion = '8.9'
}
task prepareKotlinBuildScriptModel {
}
+77
View File
@@ -0,0 +1,77 @@
# Ghostship Android ProGuard Rules
# Keep SDL classes - critical for native integration
-keep class org.libsdl.app.** { *; }
# Keep all native methods and JNI callbacks
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep MainActivity and its native methods
-keep class com.ghostship.android.MainActivity {
public static java.lang.String getSaveDir();
public static void waitForSetupFromNative();
public native void attachController();
public native void detachController();
public native void setButton(int, boolean);
public native void setCameraState(int, float);
public native void setAxis(int, short);
}
# Keep DialogActivity
-keep class com.ghostship.android.DialogActivity { *; }
# Keep ControllerButtons constants
-keep class com.ghostship.android.ControllerButtons { *; }
# Keep DocumentFile for SAF
-keep class androidx.documentfile.provider.DocumentFile { *; }
# Keep all classes that might be accessed via JNI
-keep class * {
public static <methods>;
public static <fields>;
}
# Keep enums - often used by native code
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep Serializable classes
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# Less aggressive optimization for native apps
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*,!code/removal/advanced,!code/removal/simple
-optimizationpasses 3
-allowaccessmodification
-dontpreverify
# Don't remove logging completely - keep error logs for crash reports
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
}
# Keep line numbers and source file for crash reports
-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature
# Rename source file attribute
-renamesourcefileattribute SourceFile
# Don't warn about missing classes that might be platform-specific
-dontwarn javax.**
-dontwarn java.awt.**
-dontwarn org.slf4j.**
+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ghostship.android"
android:versionCode="1"
android:versionName="1.0.1"
android:installLocation="auto">
<!-- OpenGL ES 3.0 -->
<uses-feature android:glEsVersion="0x00030000"/>
<!-- Touchscreen support -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- Game controller support -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- External mouse input events -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false" />
<!-- Gyroscope support -->
<uses-feature
android:name="android.hardware.sensor.gyroscope"
android:required="false"/>
<!-- Audio recording support -->
<!-- if you want to capture audio, uncomment this. -->
<!-- <uses-feature
android:name="android.hardware.microphone"
android:required="false" /> -->
<!-- Allow downloading to the external storage on Android 5.1 and older -->
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> -->
<!-- Allow access to Bluetooth devices -->
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> -->
<!-- Allow access to the vibrator -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- if you want to capture audio, uncomment this. -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
<!-- Create a Java class extending SDLActivity and place it in a
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
in the XML below.
An example Java class can be found in README-android.md
-->
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true"
android:appCategory="game"
android:requestLegacyExternalStorage="true" >
<!-- Example of setting SDL hints from AndroidManifest.xml:
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
-->
<meta-data android:name="SDL_ENV.SDL_IOS_ORIENTATIONS" android:value="LandscapeLeft LandscapeRight"/>
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:alwaysRetainTaskState="true"
android:launchMode="singleInstance"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:exported="true"
android:screenOrientation="landscape"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Let Android know that we can handle some USB devices and should receive this event -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<!-- Drop file event -->
<!--
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
-->
</activity>
<activity
android:name=".DialogActivity"
android:screenOrientation="portrait"
android:theme="@style/RoundedDialog"
android:exported="false" />
</application>
</manifest>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
Place mods in this folder
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

@@ -0,0 +1,87 @@
package com.ghostship.android;
import android.content.Context;
import android.content.res.AssetManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AssetCopyUtil {
public static void copyAssetsToExternal(Context context, String assetsFolderPath, String externalFolderPath) throws IOException {
AssetManager assetManager = context.getAssets();
String[] assetFiles = assetManager.list(assetsFolderPath);
for (String assetFile : assetFiles) {
String assetPath = assetsFolderPath + File.separator + assetFile;
String externalPath = externalFolderPath + File.separator + assetFile;
if (assetManager.list(assetPath).length > 0) {
// It's a directory
// Check if the directory exists in the external storage
File externalDir = new File(externalPath);
if (!externalDir.exists()) {
externalDir.mkdirs(); // Create the directory if it doesn't exist
}
// Recursively copy contents of the directory
copyAssetsToExternal(context, assetPath, externalPath);
} else {
// It's a file
File externalFile = new File(externalPath);
if (!externalFile.exists()) {
// Check if the file exists in the external storage
InputStream in = null;
OutputStream out = null;
try {
in = assetManager.open(assetPath);
out = new FileOutputStream(externalPath);
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
}
}
public static void copyDirectory(File sourceDir, File targetDir) throws IOException {
if (!targetDir.exists()) {
targetDir.mkdirs();
}
for (File file : sourceDir.listFiles()) {
File dest = new File(targetDir, file.getName());
if (file.isDirectory()) {
copyDirectory(file, dest);
} else {
copyFile(file, dest);
}
}
}
public static void copyFile(File source, File dest) throws IOException {
try (InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(dest)) {
byte[] buf = new byte[64*1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
}
}
}
@@ -0,0 +1,26 @@
package com.ghostship.android;
public class ControllerButtons {
// Xbox Buttons
public static final int BUTTON_A = 0;
public static final int BUTTON_B = 1;
public static final int BUTTON_X = 2;
public static final int BUTTON_Y = 3;
public static final int BUTTON_LB = 9; // Left Bumper
public static final int BUTTON_RB = -5; // Right Bumper
public static final int BUTTON_BACK = 4;
public static final int BUTTON_START = 6;
public static final int BUTTON_MENU = 111; // Escape/Menu button (KeyEvent.KEYCODE_ESCAPE)
public static final int BUTTON_DPAD_UP = 11;
public static final int BUTTON_DPAD_DOWN = 12;
public static final int BUTTON_DPAD_LEFT = 13;
public static final int BUTTON_DPAD_RIGHT = 14;
// Xbox Axis (Joysticks and Triggers)
public static final int AXIS_LX = 0; // Left stick X
public static final int AXIS_LY = 1; // Left stick Y
public static final int AXIS_RX = 2; // Right stick X
public static final int AXIS_RY = 3; // Right stick Y
public static final int AXIS_LT = -2; // Left Trigger (negative axis)
public static final int AXIS_RT = -4; // Right Trigger (negative axis)
}
@@ -0,0 +1,103 @@
package com.ghostship.android;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
public class DialogActivity extends Activity {
public static final String EXTRA_TITLE = "title";
public static final String EXTRA_MESSAGE = "message";
public static final String EXTRA_POSITIVE_BUTTON = "positive_button";
public static final String EXTRA_NEGATIVE_BUTTON = "negative_button";
public static final String EXTRA_CANCELABLE = "cancelable";
public static final String EXTRA_DIALOG_TYPE = "dialog_type";
public static final int DIALOG_TYPE_FOLDER_PROMPT = 1;
public static final int DIALOG_TYPE_FILE_NOT_FOUND = 2;
public static final int DIALOG_TYPE_COPY_COMPLETE = 3;
public static final int DIALOG_TYPE_FILE_READY = 4;
public static final int RESULT_POSITIVE = RESULT_OK;
public static final int RESULT_NEGATIVE = RESULT_CANCELED;
public static final int RESULT_FOLDER_PICKER = 100;
public static final int RESULT_TORCH_DOWNLOAD = 101;
public static final int RESULT_FILE_PICKER = 102;
public static final int RESULT_RESTART = 103;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String title = intent.getStringExtra(EXTRA_TITLE);
String message = intent.getStringExtra(EXTRA_MESSAGE);
String positiveButton = intent.getStringExtra(EXTRA_POSITIVE_BUTTON);
String negativeButton = intent.getStringExtra(EXTRA_NEGATIVE_BUTTON);
boolean cancelable = intent.getBooleanExtra(EXTRA_CANCELABLE, true);
int dialogType = intent.getIntExtra(EXTRA_DIALOG_TYPE, 0);
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.RoundedDialog);
builder.setTitle(title);
builder.setMessage(message);
builder.setCancelable(cancelable);
// Set up buttons based on dialog type
switch (dialogType) {
case DIALOG_TYPE_FOLDER_PROMPT:
builder.setPositiveButton("Select Folder", (d, w) -> {
setResult(RESULT_FOLDER_PICKER);
finish();
});
break;
case DIALOG_TYPE_FILE_NOT_FOUND:
builder.setPositiveButton("Download Torch App", (d, w) -> {
android.util.Log.i("DialogActivity", "Torch download button pressed");
setResult(RESULT_TORCH_DOWNLOAD);
finish();
});
builder.setNegativeButton("Select sm64.o2r File", (d, w) -> {
android.util.Log.i("DialogActivity", "File picker button pressed");
setResult(RESULT_FILE_PICKER);
finish();
});
break;
case DIALOG_TYPE_COPY_COMPLETE:
builder.setPositiveButton("Restart", (d, w) -> {
setResult(RESULT_RESTART);
finish();
});
builder.setNegativeButton("Later", (d, w) -> {
setResult(RESULT_NEGATIVE);
finish();
});
break;
case DIALOG_TYPE_FILE_READY:
builder.setPositiveButton("Restart", (d, w) -> {
setResult(RESULT_RESTART);
finish();
});
break;
default:
builder.setPositiveButton(positiveButton != null ? positiveButton : "OK", (d, w) -> {
setResult(RESULT_POSITIVE);
finish();
});
if (negativeButton != null) {
builder.setNegativeButton(negativeButton, (d, w) -> {
setResult(RESULT_NEGATIVE);
finish();
});
}
break;
}
AlertDialog dialog = builder.create();
dialog.setOnDismissListener(d -> finish());
dialog.show();
}
}
File diff suppressed because it is too large Load Diff
@@ -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();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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 /* Android 5.0 (LOLLIPOP) */) {
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 /* Android 5.0 (LOLLIPOP) */) {
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 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getProductName();
}
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
public String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
}
@Override
public int sendOutputReport(byte[] report) {
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (r != report.length) {
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
}
return r;
}
@Override
public boolean getFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceFeatureReport(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -0,0 +1,90 @@
package org.libsdl.app;
import android.content.Context;
import java.lang.Class;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
public static void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
public static void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
public static void setContext(Context context) {
SDLAudioManager.setContext(context);
mContext = context;
}
public static Context getContext() {
return mContext;
}
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
public static void loadLibrary(String libraryName, Context context) 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 = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.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, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Context mContext;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More