mirror of
https://github.com/izzy2lost/Ghostship.git
synced 2026-06-19 01:17:03 -07:00
android port working
This commit is contained in:
+1
-1
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+77
@@ -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.**
|
||||
@@ -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
Reference in New Issue
Block a user