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:
Robert Kirkman
2025-02-19 18:54:15 -06:00
parent b5a4986ab3
commit bde310703c
19 changed files with 318 additions and 267 deletions
+22
View File
@@ -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
+3
View File
@@ -32,3 +32,6 @@ google-services.json
# Android Profiling
*.hprof
# vscode
.vscode
+1 -1
View File
@@ -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
View File
@@ -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') {
}
Binary file not shown.
-5
View File
@@ -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
+6 -44
View File
@@ -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 -4
View File
@@ -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);
+9 -5
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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