You've already forked Shipwright-Android
mirror of
https://github.com/izzy2lost/Shipwright-Android.git
synced 2026-03-26 16:51:07 -07:00
special external storage permission
- bump SDL to 2.32.0
- bump NDK to r27c
- bump AGP to 8.8.0
- bump Gradle to 8.10.2
- bump CMake to 3.31.5
- bump targetSdkVersion to 34
- add GitHub Actions Release support (Debug)
- add Special External Storage Permission support
- fix "assets not found" error on first launch
- drop Android 4 support
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
name: generate-release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y ninja-build
|
||||
- name: Build SoH
|
||||
run: |
|
||||
./gradlew assembleDebug -P elfBuildType=RelWithDebInfo
|
||||
mv app/build/outputs/apk/debug/app-debug.apk soh.apk
|
||||
- name: Create release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: soh.apk
|
||||
@@ -32,3 +32,6 @@ google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "app/jni/src/libultraship"]
|
||||
path = app/jni/src/libultraship
|
||||
url = https://github.com/Waterdish/libultraship.git
|
||||
url = https://github.com/robertkirkman/libultraship.git
|
||||
|
||||
+24
-27
@@ -1,4 +1,4 @@
|
||||
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY');
|
||||
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY')
|
||||
def buildAsApplication = !buildAsLibrary
|
||||
if (buildAsApplication) {
|
||||
apply plugin: 'com.android.application'
|
||||
@@ -8,25 +8,21 @@ else {
|
||||
}
|
||||
|
||||
android {
|
||||
ndkPath "/home/waterdish/Android/Sdk/ndk/26.0.10792818" // Point to your own NDK
|
||||
compileSdkVersion 31
|
||||
ndkVersion '27.2.12479018'
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
if (buildAsApplication) {
|
||||
applicationId "com.dishii.soh"
|
||||
}
|
||||
minSdkVersion 18
|
||||
targetSdkVersion 31
|
||||
minSdkVersion 21
|
||||
//noinspection OldTargetApi
|
||||
targetSdkVersion 34
|
||||
versionCode 6
|
||||
versionName "1.3.0"
|
||||
externalNativeBuild {
|
||||
//ndkBuild {
|
||||
// arguments "APP_PLATFORM=android-23"
|
||||
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
//}
|
||||
cmake {
|
||||
arguments "-DANDROID_APP_PLATFORM=android-23", "-DANDROID_STL=c++_static", "-DHAVE_LD_VERSION_SCRIPT=OFF",'-DUSE_OPENGLES=ON'
|
||||
arguments "-DANDROID_APPNAME=${applicationId}", "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static", "-DHAVE_LD_VERSION_SCRIPT=OFF", "-DUSE_OPENGLES=ON", "-DCMAKE_BUILD_TYPE=$elfBuildType"
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
//abiFilters 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +32,14 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
applicationVariants.all { variant ->
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
namespace 'com.dishii.soh'
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
applicationVariants.configureEach { variant ->
|
||||
tasks["merge${variant.name.capitalize()}Assets"]
|
||||
.dependsOn("externalNativeBuild${variant.name.capitalize()}")
|
||||
}
|
||||
@@ -45,27 +48,21 @@ android {
|
||||
jniLibs.srcDir 'libs'
|
||||
}
|
||||
externalNativeBuild {
|
||||
//ndkBuild {
|
||||
// path 'jni/Android.mk'
|
||||
//}
|
||||
cmake {
|
||||
path 'jni/CMakeLists.txt'
|
||||
version "3.25.1"
|
||||
version "3.31.5"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
|
||||
if (buildAsLibrary) {
|
||||
libraryVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def outputFile = output.outputFile
|
||||
if (outputFile != null && outputFile.name.endsWith(".aar")) {
|
||||
def fileName = "org.libsdl.app.aar";
|
||||
output.outputFile = new File(outputFile.parent, fileName);
|
||||
def fileName = "com.dishii.soh.aar"
|
||||
output.outputFile = new File(outputFile.parent, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,13 +71,13 @@ android {
|
||||
|
||||
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.core:core:1.13.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
}
|
||||
|
||||
task wrapper(type: Wrapper) {
|
||||
gradleVersion = '7.3'
|
||||
tasks.register('wrapper', Wrapper) {
|
||||
gradleVersion = '8.10.2'
|
||||
}
|
||||
|
||||
task prepareKotlinBuildScriptModel {
|
||||
tasks.register('prepareKotlinBuildScriptModel') {
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+1
-1
Submodule app/jni/src/libultraship updated: 03e683e1d1...94f648a98e
@@ -95,7 +95,7 @@ enum class ButtonId : int {
|
||||
};
|
||||
|
||||
#ifdef __ANDROID__
|
||||
const char* javaRomPath = NULL;
|
||||
static char javaRomPath[4096] = { 0 };
|
||||
bool fileDialogOpen = false;
|
||||
|
||||
//function to be called from C
|
||||
@@ -108,7 +108,7 @@ void openFilePickerFromC(JNIEnv* env, jobject javaObject) {
|
||||
// Define the native method to handle the selected file path
|
||||
extern "C" void JNICALL Java_com_dishii_soh_MainActivity_nativeHandleSelectedFile(JNIEnv* env, jobject obj, jstring filePath) {
|
||||
const char* filePathStr = env->GetStringUTFChars(filePath, 0);
|
||||
javaRomPath = strdup(filePathStr); // save filepath to string
|
||||
snprintf(javaRomPath, sizeof(javaRomPath), "%s", filePathStr);
|
||||
fileDialogOpen = false;
|
||||
env->ReleaseStringUTFChars(filePath, filePathStr);
|
||||
}
|
||||
@@ -248,7 +248,7 @@ void Extractor::GetRoms(std::vector<std::string>& roms) {
|
||||
// if (h != nullptr) {
|
||||
// CloseHandle(h);
|
||||
//}
|
||||
#elif unix && !defined(__ANDROID__)
|
||||
#elif unix
|
||||
// Open the directory of the app.
|
||||
DIR* d = opendir(mSearchPath.c_str());
|
||||
struct dirent* dir;
|
||||
@@ -272,33 +272,6 @@ void Extractor::GetRoms(std::vector<std::string>& roms) {
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
#elif defined(__ANDROID__)
|
||||
const char* androidAssetPath = SDL_AndroidGetExternalStoragePath();
|
||||
if (androidAssetPath == NULL) {
|
||||
printf("Error accessing Android assets directory: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Use androidAssetPath for file operations
|
||||
// Example: List files in the directory
|
||||
DIR* dir;
|
||||
struct dirent* entry;
|
||||
|
||||
if ((dir = opendir(androidAssetPath)) != NULL) {
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_type == DT_REG) {
|
||||
char* filename = entry->d_name;
|
||||
// Check file extension and process accordingly
|
||||
if (strstr(filename, ".n64") || strstr(filename, ".z64") || strstr(filename, ".v64")) {
|
||||
std::string fullPath = std::string(androidAssetPath) + "/" + filename;
|
||||
roms.push_back(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
} else {
|
||||
printf("Error opening directory: %s\n", androidAssetPath);
|
||||
}
|
||||
#else
|
||||
for (const auto& file : std::filesystem::directory_iterator(mSearchPath)) {
|
||||
if (file.is_directory())
|
||||
@@ -358,29 +331,18 @@ bool Extractor::GetRomPathFromBox() {
|
||||
//Do nothing until a file is chosen
|
||||
SDL_Delay(250);
|
||||
}
|
||||
SDL_Log("%s",javaRomPath);
|
||||
SDL_Log("javaRomPath: %s", javaRomPath);
|
||||
|
||||
selection.push_back(javaRomPath);
|
||||
|
||||
if (selection.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mCurrentRomPath = selection[0];
|
||||
|
||||
if (javaRomPath) {
|
||||
free((void*)javaRomPath);
|
||||
javaRomPath = NULL;
|
||||
}
|
||||
|
||||
#else
|
||||
auto selection = pfd::open_file("Select a file", mSearchPath, { "N64 Roms", "*.z64 *.n64 *.v64" }).result();
|
||||
#endif
|
||||
|
||||
if (selection.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mCurrentRomPath = selection[0];
|
||||
#endif
|
||||
mCurRomSize = GetCurRomSize();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
com.gamemaker.game
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.dishii.soh"
|
||||
android:versionCode="6"
|
||||
android:versionName="1.3.0"
|
||||
android:installLocation="auto">
|
||||
@@ -54,8 +53,8 @@
|
||||
<!-- Allow access to the vibrator -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- if you want to capture audio, uncomment this. -->
|
||||
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
|
||||
@@ -73,7 +72,8 @@
|
||||
android:allowBackup="true"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
||||
android:hardwareAccelerated="true"
|
||||
android:appCategory="game" >
|
||||
android:appCategory="game"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
|
||||
<!-- Example of setting SDL hints from AndroidManifest.xml:
|
||||
@@ -84,7 +84,6 @@
|
||||
|
||||
|
||||
<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"
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.dishii.soh;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -9,53 +11,70 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class AssetCopyUtil {
|
||||
// based on https://stackoverflow.com/a/8366081/11708026
|
||||
// This is desirable because it bulk copies all assets indiscriminately, simplifying the code in MainActivity
|
||||
// the side effect is that, some people will see strange, unidentifiable files appearing in their destination folder.
|
||||
// those extra files do not come from the app itself. They originate from something inside the Android ROM
|
||||
// of the device they are using, and can be observed to vary between different models of devices.
|
||||
public static void copyAssetsToExternal(Context context, String externalFolderPath) {
|
||||
externalFolderPath = externalFolderPath + "/";
|
||||
copyFileOrDir(context, "", externalFolderPath); // copy all files in assets folder to the destination
|
||||
}
|
||||
|
||||
public static void copyAssetsToExternal(Context context, String assetsFolderPath, String externalFolderPath) throws IOException {
|
||||
private static void copyFileOrDir(Context context, String srcpath, String destpath) {
|
||||
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);
|
||||
String assets[] = null;
|
||||
String tag = "AssetCopyUtil";
|
||||
try {
|
||||
Log.i(tag, "copyFileOrDir() " + srcpath);
|
||||
assets = assetManager.list(srcpath);
|
||||
if (assets.length == 0) {
|
||||
copyFile(context, srcpath, destpath);
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
String fullPath = destpath + srcpath;
|
||||
Log.i(tag, "path=" + fullPath);
|
||||
File dir = new File(fullPath);
|
||||
if (!dir.exists())
|
||||
if (!dir.mkdirs())
|
||||
Log.i(tag, "could not create dir " + fullPath);
|
||||
for (int i = 0; i < assets.length; ++i) {
|
||||
String p;
|
||||
if (srcpath.isEmpty())
|
||||
p = "";
|
||||
else
|
||||
p = srcpath + "/";
|
||||
copyFileOrDir(context,p + assets[i], destpath);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Log.e(tag, "I/O Exception", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFile(Context context, String filename, String destpath) {
|
||||
AssetManager assetManager = context.getAssets();
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
String newFileName = null;
|
||||
String tag = "AssetCopyUtil";
|
||||
try {
|
||||
Log.i(tag, "copyFile() " + filename);
|
||||
in = assetManager.open(filename);
|
||||
newFileName = destpath + filename;
|
||||
out = new FileOutputStream(newFileName);
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
in.close();
|
||||
in = null;
|
||||
out.flush();
|
||||
out.close();
|
||||
out = null;
|
||||
} catch (Exception e) {
|
||||
Log.e(tag, "Exception in copyFile() of " + newFileName);
|
||||
Log.e(tag, "Exception in copyFile() " + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,11 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.provider.Settings;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -25,29 +18,49 @@ import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
//This class is the main SDLActivity and just sets up a bunch of default files
|
||||
public class MainActivity extends SDLActivity{
|
||||
public class MainActivity extends SDLActivity {
|
||||
|
||||
private static final int STORAGE_PERMISSION_REQUEST_CODE = 1;
|
||||
private static final int FILE_HANDLER_REQUEST_CODE = 0;
|
||||
private static final int SPECIAL_STORAGE_PERMISSION_REQUEST_CODE = 1;
|
||||
|
||||
SharedPreferences preferences;
|
||||
|
||||
private boolean hasSpecialExternalStoragePermission = false;
|
||||
|
||||
private boolean permissionPopupIsOpen = false;
|
||||
|
||||
// this is a case where I feel like it is actually clearer to have a double negative boolean,
|
||||
// than to have one named "permissionPopupChargeIsAvailable = true", but you can let me know
|
||||
// if you would prefer to reorganize this.
|
||||
private boolean permissionPopupWasDeclined = false;
|
||||
|
||||
private boolean hasInstalledExternalAssetFiles = false;
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
return new String[] {
|
||||
"SDL2",
|
||||
"soh"
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
preferences = getSharedPreferences("com.dishii.soh.prefs",Context.MODE_PRIVATE);
|
||||
preferences = getSharedPreferences("com.dishii.soh.prefs", Context.MODE_PRIVATE);
|
||||
|
||||
doVersionCheck();
|
||||
|
||||
setupControllerOverlay();
|
||||
// Check if storage permissions are granted
|
||||
if (hasStoragePermission()) {
|
||||
doVersionCheck();
|
||||
setupFiles();
|
||||
} else {
|
||||
requestStoragePermission();
|
||||
}
|
||||
attachController();
|
||||
}
|
||||
|
||||
@@ -56,99 +69,108 @@ public class MainActivity extends SDLActivity{
|
||||
int storedVersion = preferences.getInt("appVersion", 1);
|
||||
|
||||
if (currentVersion > storedVersion) {
|
||||
deleteOutdatedAssets();
|
||||
// I tend to just copy all assets on every app launch, overwriting the old ones,
|
||||
// so that I don't have to change appVersion every time I need to make sure that the newest assets
|
||||
// are guaranteed to always be present. Also, when /storage/emulated/0/com.dishii.soh is used, the assets
|
||||
// could be from a whole different version of the app and that wouldn't be detected (at least by this
|
||||
// particular SharedPreferences), so my way always overwrites those.
|
||||
// My way is also very slow to load at every startup, which is ok for
|
||||
// apps that have only a few external assets, but for apps like this one that have
|
||||
// a lot of external assets, the slowness is pretty severe. Let me know if that is not desirable
|
||||
// and you would prefer it to work differently.
|
||||
//deleteOutdatedAssets();
|
||||
preferences.edit().putInt("appVersion", currentVersion).apply();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteOutdatedAssets(){
|
||||
File externalSohFile = new File(getExternalFilesDir(null), "soh.otr");
|
||||
externalSohFile.delete();
|
||||
File externalOotFile = new File(getExternalFilesDir(null), "oot.otr");
|
||||
externalOotFile.delete();
|
||||
File externalOotMqFile = new File(getExternalFilesDir(null), "oot-mq.otr");
|
||||
externalOotMqFile.delete();
|
||||
File externalAssetsFolder = new File(getExternalFilesDir(null), "assets");
|
||||
deleteRecursive(externalAssetsFolder);
|
||||
// called from native code through JNI where necessary
|
||||
public String getExternalAssetsPath() {
|
||||
// the original location, /storage/emulated/0/Android/data/com.dishii.soh/files,
|
||||
// can be the fallback if the user denies the permission
|
||||
String externalAssetsPath = getExternalFilesDir(null).getAbsolutePath();
|
||||
|
||||
}
|
||||
if (!permissionPopupWasDeclined) {
|
||||
requestSpecialExternalStoragePermission();
|
||||
}
|
||||
|
||||
private void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory()) {
|
||||
for (File child : fileOrDirectory.listFiles()) {
|
||||
deleteRecursive(child);
|
||||
while(permissionPopupIsOpen) {
|
||||
// Do nothing until a permission is chosen
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
fileOrDirectory.delete();
|
||||
|
||||
if (hasSpecialExternalStoragePermission) {
|
||||
// /storage/emulated/0/com.dishii.soh, also mounted at /sdcard/com.dishii.soh
|
||||
externalAssetsPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + getApplicationContext().getPackageName();
|
||||
}
|
||||
|
||||
if (!hasInstalledExternalAssetFiles) {
|
||||
setupFiles(externalAssetsPath);
|
||||
}
|
||||
|
||||
return externalAssetsPath;
|
||||
}
|
||||
|
||||
// Request the special external storage permission
|
||||
private void requestSpecialExternalStoragePermission() {
|
||||
// Android 5 or older
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
hasSpecialExternalStoragePermission = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Android 10 or older
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
if (checkSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE") == PackageManager.PERMISSION_GRANTED) {
|
||||
hasSpecialExternalStoragePermission = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if storage permission is granted
|
||||
private boolean hasStoragePermission() {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
requestPermissions(new String[]{"android.permission.WRITE_EXTERNAL_STORAGE"}, SPECIAL_STORAGE_PERMISSION_REQUEST_CODE);
|
||||
permissionPopupIsOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Request storage permission
|
||||
private void requestStoragePermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
STORAGE_PERMISSION_REQUEST_CODE);
|
||||
// Android 11 or newer
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
hasSpecialExternalStoragePermission = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.addCategory("android.intent.category.DEFAULT");
|
||||
intent.setData(Uri.parse(String.format("package:%s", getApplicationContext().getPackageName())));
|
||||
startActivityForResult(intent, SPECIAL_STORAGE_PERMISSION_REQUEST_CODE);
|
||||
} catch (Exception e) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
startActivityForResult(intent, SPECIAL_STORAGE_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
permissionPopupIsOpen = true;
|
||||
}
|
||||
|
||||
// Handle permission request result
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) {
|
||||
if (requestCode == SPECIAL_STORAGE_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
setupFiles();
|
||||
hasSpecialExternalStoragePermission = true;
|
||||
} else {
|
||||
// Permission denied, handle accordingly (e.g., show a message)
|
||||
permissionPopupWasDeclined = true;
|
||||
}
|
||||
permissionPopupIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFiles(){
|
||||
//Copy assets folder for rom extraction
|
||||
File externalAssetsDir = new File(getExternalFilesDir(null), "assets");
|
||||
if (!externalAssetsDir.exists()) {
|
||||
try {
|
||||
externalAssetsDir.mkdirs();
|
||||
AssetCopyUtil.copyAssetsToExternal(this, "assets", externalAssetsDir.getAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
//Create empty mods folder
|
||||
File externalModsDir = new File(getExternalFilesDir(null), "mods");
|
||||
externalModsDir.mkdirs();
|
||||
|
||||
//Copy soh.otr
|
||||
File externalSohOtrFile = new File(getExternalFilesDir(null), "soh.otr");
|
||||
if (!externalSohOtrFile.exists()) {
|
||||
try {
|
||||
InputStream in = getAssets().open("soh.otr");
|
||||
OutputStream out = new FileOutputStream(externalSohOtrFile);
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
in.close();
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFiles(String externalAssetsPath) {
|
||||
AssetCopyUtil.copyAssetsToExternal(this, externalAssetsPath);
|
||||
hasInstalledExternalAssetFiles = true;
|
||||
}
|
||||
|
||||
private native void nativeHandleSelectedFile(String filePath);
|
||||
@@ -156,10 +178,11 @@ public class MainActivity extends SDLActivity{
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == 0 && resultCode == RESULT_OK) {
|
||||
|
||||
if (requestCode == FILE_HANDLER_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
Uri selectedFileUri = data.getData();
|
||||
String fileName = "ZELOOTD.z64";
|
||||
File destinationDirectory = getExternalFilesDir(null); // The second argument can specify a subdirectory, or you can pass null to use the root directory.
|
||||
String destinationDirectory = getExternalAssetsPath();
|
||||
File destinationFile = new File(destinationDirectory, fileName);
|
||||
|
||||
if (destinationDirectory != null) {
|
||||
@@ -181,8 +204,20 @@ public class MainActivity extends SDLActivity{
|
||||
}
|
||||
nativeHandleSelectedFile(destinationFile.getPath());
|
||||
}
|
||||
|
||||
if (requestCode == SPECIAL_STORAGE_PERMISSION_REQUEST_CODE) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R /* Android 11 or newer */) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
hasSpecialExternalStoragePermission = true;
|
||||
} else {
|
||||
permissionPopupWasDeclined = true;
|
||||
}
|
||||
}
|
||||
permissionPopupIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// called from native code through JNI where necessary
|
||||
public void openFilePicker() {
|
||||
// Create an Intent to open the file picker dialog
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
@@ -192,14 +227,6 @@ public class MainActivity extends SDLActivity{
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
// Check if external storage is available and writable
|
||||
private boolean isExternalStorageWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public native void attachController();
|
||||
public native void detachController();
|
||||
// Native method for setting button state
|
||||
|
||||
@@ -273,9 +273,11 @@ public class HIDDeviceManager {
|
||||
final int XB1_IFACE_SUBCLASS = 71;
|
||||
final int XB1_IFACE_PROTOCOL = 208;
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x03f0, // HP
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0b05, // ASUS
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
@@ -284,7 +286,7 @@ public class HIDDeviceManager {
|
||||
0x24c6, // PowerA
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
//0x3537, // Gamesir
|
||||
0x3537, // GameSir
|
||||
};
|
||||
|
||||
if (usbInterface.getId() == 0 &&
|
||||
@@ -358,6 +360,12 @@ public class HIDDeviceManager {
|
||||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||
@@ -583,7 +591,13 @@ public class HIDDeviceManager {
|
||||
} else {
|
||||
flags = 0;
|
||||
}
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
|
||||
if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) {
|
||||
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
|
||||
} else {
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||
HIDDeviceOpenResult(deviceID, false);
|
||||
|
||||
@@ -38,6 +38,10 @@ public class SDL {
|
||||
}
|
||||
|
||||
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.");
|
||||
@@ -53,10 +57,10 @@ public class SDL {
|
||||
// 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");
|
||||
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.
|
||||
@@ -66,7 +70,7 @@ public class SDL {
|
||||
|
||||
// Actually load the library!
|
||||
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||
loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
|
||||
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
|
||||
}
|
||||
catch (final Throwable e) {
|
||||
// Fall back
|
||||
|
||||
@@ -60,8 +60,8 @@ import java.util.Locale;
|
||||
public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
|
||||
private static final String TAG = "SDL";
|
||||
private static final int SDL_MAJOR_VERSION = 2;
|
||||
private static final int SDL_MINOR_VERSION = 28;
|
||||
private static final int SDL_MICRO_VERSION = 1;
|
||||
private static final int SDL_MINOR_VERSION = 32;
|
||||
private static final int SDL_MICRO_VERSION = 0;
|
||||
/*
|
||||
// Display InputType.SOURCE/CLASS of events and devices
|
||||
//
|
||||
@@ -89,7 +89,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
| InputDevice.SOURCE_CLASS_POSITION
|
||||
| InputDevice.SOURCE_CLASS_TRACKBALL);
|
||||
|
||||
if (s2 != 0) cls += "Some_Unkown";
|
||||
if (s2 != 0) cls += "Some_Unknown";
|
||||
|
||||
s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class;
|
||||
|
||||
@@ -163,7 +163,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
if (s == FLAG_TAINTED) src += " FLAG_TAINTED";
|
||||
s2 &= ~FLAG_TAINTED;
|
||||
|
||||
if (s2 != 0) src += " Some_Unkown";
|
||||
if (s2 != 0) src += " Some_Unknown";
|
||||
|
||||
Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src);
|
||||
}
|
||||
@@ -274,14 +274,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
// "SDL2_mixer",
|
||||
// "SDL2_net",
|
||||
// "SDL2_ttf",
|
||||
"soh"
|
||||
"main"
|
||||
};
|
||||
}
|
||||
|
||||
// Load the .so
|
||||
public void loadLibraries() {
|
||||
for (String lib : getLibraries()) {
|
||||
SDL.loadLibrary(lib);
|
||||
SDL.loadLibrary(lib, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +790,9 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
SDLActivity.mFullscreenModeActive = false;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) {
|
||||
window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "error handling message, getContext() returned no Activity");
|
||||
@@ -995,8 +998,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
/* No valid hint, nothing is explicitly allowed */
|
||||
if (!is_portrait_allowed && !is_landscape_allowed) {
|
||||
if (resizable) {
|
||||
/* All orientations are allowed */
|
||||
req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
|
||||
/* All orientations are allowed, respecting user orientation lock setting */
|
||||
req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
|
||||
} else {
|
||||
/* Fixed window and nothing specified. Get orientation from w/h of created window */
|
||||
req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
@@ -1005,8 +1008,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
/* At least one orientation is allowed */
|
||||
if (resizable) {
|
||||
if (is_portrait_allowed && is_landscape_allowed) {
|
||||
/* hint allows both landscape and portrait, promote to full sensor */
|
||||
req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
|
||||
/* hint allows both landscape and portrait, promote to full user */
|
||||
req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
|
||||
} else {
|
||||
/* Use the only one allowed "orientation" */
|
||||
req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
|
||||
@@ -1345,23 +1348,6 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (isTextInputEvent(event)) {
|
||||
if (ic != null) {
|
||||
ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
} else {
|
||||
SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
}
|
||||
}
|
||||
onNativeKeyDown(keyCode);
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
onNativeKeyUp(keyCode);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
|
||||
// on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
|
||||
// they are ignored here because sending them as mouse input to SDL is messy
|
||||
@@ -1376,6 +1362,21 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (isTextInputEvent(event)) {
|
||||
if (ic != null) {
|
||||
ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
} else {
|
||||
SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
}
|
||||
}
|
||||
onNativeKeyDown(keyCode);
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
onNativeKeyUp(keyCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -546,13 +546,15 @@ class SDLHapticHandler {
|
||||
if (haptic == null) {
|
||||
InputDevice device = InputDevice.getDevice(deviceIds[i]);
|
||||
Vibrator vib = device.getVibrator();
|
||||
if (vib.hasVibrator()) {
|
||||
haptic = new SDLHaptic();
|
||||
haptic.device_id = deviceIds[i];
|
||||
haptic.name = device.getName();
|
||||
haptic.vib = vib;
|
||||
mHaptics.add(haptic);
|
||||
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||
if (vib != null) {
|
||||
if (vib.hasVibrator()) {
|
||||
haptic = new SDLHaptic();
|
||||
haptic.device_id = deviceIds[i];
|
||||
haptic.name = device.getName();
|
||||
haptic.vib = vib;
|
||||
mHaptics.add(haptic);
|
||||
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -18,3 +18,9 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
# org.gradle.parallel=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
# set to Release or RelWithDebInfo to avoid assertion failure on title screen
|
||||
# with the "Art Plus Link" player model
|
||||
elfBuildType=Debug
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
#Thu Nov 11 18:20:34 PST 2021
|
||||
#Wed Feb 12 06:17:56 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user