6 Commits
Author SHA1 Message Date
izzy2lostandGitHub 5ad3f98ee6 Add files via upload 2025-06-12 22:45:10 -04:00
Robert Kirkman d11c66eea9 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
    - add Android 5 & 6 support
2025-02-19 18:58:13 -06:00
WaterdishandGitHub d6c503f0e9 Update README.md 2024-12-06 21:15:54 -08:00
WaterdishandGitHub e9a5064e59 Update README.md 2024-11-25 21:06:20 -08:00
Waterdish 821dd700ff Fix free look not working with controllers 2024-11-23 22:58:07 -08:00
Waterdish ef7780ffaa Better c-button fix 2024-11-23 20:07:55 -08:00
23 changed files with 412 additions and 223 deletions
+74
View File
@@ -0,0 +1,74 @@
name: generate-android-apk
on:
workflow_dispatch:
jobs:
generate-2ship-otr:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ninja-build cmake g++ gcc libzip-dev zipcmp zipmerge ziptool
- name: Install latest SDL
run: |
if [ ! -d "SDL2-2.28.5" ]; then
wget https://github.com/libsdl-org/SDL/releases/download/release-2.28.5/SDL2-2.28.5.tar.gz
tar -xzf SDL2-2.28.5.tar.gz
fi
cd SDL2-2.28.5
./configure --enable-hidapi-libusb
make -j 10
sudo make install
sudo cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/
- name: Install latest tinyxml2
run: |
sudo apt-get remove libtinyxml2-dev
if [ ! -d "tinyxml2-10.0.0" ]; then
wget https://github.com/leethomason/tinyxml2/archive/refs/tags/10.0.0.tar.gz
tar -xzf 10.0.0.tar.gz
fi
cd tinyxml2-10.0.0
mkdir -p build
cd build
cmake ..
make
sudo make install
- name: Generate 2ship.o2r
run: |
cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release
cmake --build build-cmake --config Release --target Generate2ShipOtr -j3
- uses: actions/upload-artifact@v4
with:
name: 2ship.o2r
path: 2ship.o2r
retention-days: 1
build-android:
needs: generate-2ship-otr
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: Download 2ship.o2r
uses: actions/download-artifact@v4
with:
name: 2ship.o2r
path: Android/app/src/main/assets
- name: Build 2Ship APK
run: |
cd Android/
./gradlew assembleDebug -P elfBuildType=RelWithDebInfo
mv app/build/outputs/apk/debug/app-debug.apk ../2ship.apk
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: 2ship-apk
path: 2ship.apk
+29 -2
View File
@@ -1,7 +1,8 @@
name: generate-builds name: generate-builds
on: on:
push: # push:
pull_request: # pull_request:
workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -380,3 +381,29 @@ jobs:
with: with:
name: 2ship-windows name: 2ship-windows
path: 2ship-windows path: 2ship-windows
build-android:
needs: generate-2ship-otr
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: Download 2ship.o2r
uses: actions/download-artifact@v4
with:
name: 2ship.o2r
path: Android/app/src/main/assets
- name: Build 2Ship
run: |
cd Android/
./gradlew assembleDebug -P elfBuildType=RelWithDebInfo
mv app/build/outputs/apk/debug/app-debug.apk ../2ship.apk
- name: Create release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: 2ship.apk
+3
View File
@@ -67,3 +67,6 @@ _packages/
/mm/src/boot/build.c /mm/src/boot/build.c
/mm/windows/properties.h /mm/windows/properties.h
/clang-format.exe /clang-format.exe
# IntelliJ / Android Studio
.idea
+1 -1
View File
@@ -1,6 +1,6 @@
[submodule "libultraship"] [submodule "libultraship"]
path = libultraship path = libultraship
url = https://github.com/Waterdish/libultraship.git url = https://github.com/robertkirkman/libultraship.git
[submodule "OTRExporter"] [submodule "OTRExporter"]
path = OTRExporter path = OTRExporter
url = https://github.com/Waterdish/OTRExporter.git url = https://github.com/Waterdish/OTRExporter.git
+23 -25
View File
@@ -1,4 +1,4 @@
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY')
def buildAsApplication = !buildAsLibrary def buildAsApplication = !buildAsLibrary
if (buildAsApplication) { if (buildAsApplication) {
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
@@ -8,25 +8,21 @@ else {
} }
android { android {
ndkPath "/home/waterdish/Android/Sdk/ndk/26.0.10792818" // Point to your own NDK ndkVersion '27.2.12479018'
compileSdkVersion 31 compileSdkVersion 34
defaultConfig { defaultConfig {
if (buildAsApplication) { if (buildAsApplication) {
applicationId "com.dishii.mm" applicationId "com.dishii.mm"
} }
minSdkVersion 24 minSdkVersion 21
targetSdkVersion 31 //noinspection OldTargetApi
targetSdkVersion 34
versionCode 4 versionCode 4
versionName "1.1.1" versionName "1.1.1"
externalNativeBuild { externalNativeBuild {
//ndkBuild {
// arguments "APP_PLATFORM=android-23"
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
//}
cmake { cmake {
arguments "-DSDL_SHARED=ON", "-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 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
//abiFilters 'arm64-v8a'
} }
} }
} }
@@ -34,9 +30,17 @@ android {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
} }
} }
applicationVariants.all { variant -> buildFeatures {
buildConfig = true
}
namespace 'com.dishii.mm'
lint {
abortOnError false
}
applicationVariants.configureEach { variant ->
tasks["merge${variant.name.capitalize()}Assets"] tasks["merge${variant.name.capitalize()}Assets"]
.dependsOn("externalNativeBuild${variant.name.capitalize()}") .dependsOn("externalNativeBuild${variant.name.capitalize()}")
} }
@@ -45,26 +49,20 @@ android {
jniLibs.srcDir 'libs' jniLibs.srcDir 'libs'
} }
externalNativeBuild { externalNativeBuild {
//ndkBuild {
// path 'jni/Android.mk'
//}
cmake { cmake {
path '../../CMakeLists.txt' path '../../CMakeLists.txt'
version "3.25.1" version "3.31.5"
} }
} }
} }
lintOptions {
abortOnError false
}
if (buildAsLibrary) { if (buildAsLibrary) {
libraryVariants.all { variant -> libraryVariants.all { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
def outputFile = output.outputFile def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith(".aar")) { if (outputFile != null && outputFile.name.endsWith(".aar")) {
def fileName = "org.libsdl.app.aar"; def fileName = "com.dishii.mm.aar"
output.outputFile = new File(outputFile.parent, fileName); output.outputFile = new File(outputFile.parent, fileName);
} }
} }
@@ -74,13 +72,13 @@ android {
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.core:core:1.7.0' // Use the latest version implementation 'androidx.core:core:1.13.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
} }
task wrapper(type: Wrapper) { tasks.register('wrapper', Wrapper) {
gradleVersion = '7.0.3' 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
+3 -4
View File
@@ -3,7 +3,6 @@
com.gamemaker.game com.gamemaker.game
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dishii.mm"
android:versionCode="4" android:versionCode="4"
android:versionName="1.1.1" android:versionName="1.1.1"
android:installLocation="auto"> android:installLocation="auto">
@@ -54,8 +53,8 @@
<!-- Allow access to the vibrator --> <!-- Allow access to the vibrator -->
<uses-permission android:name="android.permission.VIBRATE" /> <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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- if you want to capture audio, uncomment this. --> <!-- if you want to capture audio, uncomment this. -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> --> <!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
@@ -73,7 +72,8 @@
android:allowBackup="true" android:allowBackup="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:appCategory="game" > android:appCategory="game"
android:requestLegacyExternalStorage="true">
<!-- Example of setting SDL hints from AndroidManifest.xml: <!-- Example of setting SDL hints from AndroidManifest.xml:
@@ -84,7 +84,6 @@
<activity android:name="MainActivity" <activity android:name="MainActivity"
android:label="@string/app_name"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
+1
View File
@@ -1,2 +1,3 @@
# Extractor Assets Copied by CMake during buildtime # Extractor Assets Copied by CMake during buildtime
assets/ assets/
mods/
@@ -2,6 +2,8 @@ package com.dishii.mm;
import android.content.Context; import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -9,53 +11,70 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
public class AssetCopyUtil { public class AssetCopyUtil {
// based on https://stackoverflow.com/a/8366081/11708026
public static void copyAssetsToExternal(Context context, String assetsFolderPath, String externalFolderPath) throws IOException { // This is desirable because it bulk copies all assets indiscriminately, simplifying the code in MainActivity
AssetManager assetManager = context.getAssets(); // the side effect is that, some people will see strange, unidentifiable files appearing in their destination folder.
String[] assetFiles = assetManager.list(assetsFolderPath); // 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.
for (String assetFile : assetFiles) { public static void copyAssetsToExternal(Context context, String externalFolderPath) {
String assetPath = assetsFolderPath + File.separator + assetFile; externalFolderPath = externalFolderPath + "/";
String externalPath = externalFolderPath + File.separator + assetFile; copyFileOrDir(context, "", externalFolderPath); // copy all files in assets folder to the destination
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 private static void copyFileOrDir(Context context, String srcpath, String destpath) {
copyAssetsToExternal(context, assetPath, externalPath); AssetManager assetManager = context.getAssets();
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 { } else {
// It's a file String fullPath = destpath + srcpath;
File externalFile = new File(externalPath); Log.i(tag, "path=" + fullPath);
if (!externalFile.exists()) { File dir = new File(fullPath);
// Check if the file exists in the external storage 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; InputStream in = null;
OutputStream out = null; OutputStream out = null;
String newFileName = null;
String tag = "AssetCopyUtil";
try { try {
in = assetManager.open(assetPath); Log.i(tag, "copyFile() " + filename);
out = new FileOutputStream(externalPath); in = assetManager.open(filename);
newFileName = destpath + filename;
out = new FileOutputStream(newFileName);
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int read; int read;
while ((read = in.read(buffer)) != -1) { while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read); out.write(buffer, 0, read);
} }
} finally {
if (in != null) {
in.close(); in.close();
} in = null;
if (out != null) { out.flush();
out.close(); out.close();
} out = null;
} } catch (Exception e) {
} Log.e(tag, "Exception in copyFile() of " + newFileName);
} Log.e(tag, "Exception in copyFile() " + e.toString());
} }
} }
} }
@@ -6,20 +6,12 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; 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 android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.util.Log;
import android.provider.Settings;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@@ -28,28 +20,48 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
//This class is the main SDLActivity and just sets up a bunch of default files and the input overlay import java.io.File;
public class MainActivity extends SDLActivity{ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
private static final int STORAGE_PERMISSION_REQUEST_CODE = 1; //This class is the main SDLActivity and just sets up a bunch of default files and the input overlay
public class MainActivity extends SDLActivity {
private static final int FILE_HANDLER_REQUEST_CODE = 0;
private static final int SPECIAL_STORAGE_PERMISSION_REQUEST_CODE = 1;
SharedPreferences preferences; 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",
"2ship"
};
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
preferences = getSharedPreferences("com.dishii.mm.prefs",Context.MODE_PRIVATE); preferences = getSharedPreferences("com.dishii.mm.prefs", Context.MODE_PRIVATE);
doVersionCheck();
setupControllerOverlay(); setupControllerOverlay();
// Check if storage permissions are granted
if (hasStoragePermission()) {
doVersionCheck();
setupFiles();
} else {
requestStoragePermission();
}
attachController(); attachController();
} }
@@ -58,116 +70,108 @@ public class MainActivity extends SDLActivity{
int storedVersion = preferences.getInt("appVersion", 1); int storedVersion = preferences.getInt("appVersion", 1);
if (currentVersion > storedVersion) { 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.mm 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(); preferences.edit().putInt("appVersion", currentVersion).apply();
} }
} }
private void deleteOutdatedAssets(){ // called from native code through JNI where necessary
File externalSohFile = new File(getExternalFilesDir(null), "2ship.o2r"); public String getExternalAssetsPath() {
externalSohFile.delete(); // the original location, /storage/emulated/0/Android/data/com.dishii.mm/files,
File externalOotFile = new File(getExternalFilesDir(null), "mm.o2r"); // can be the fallback if the user denies the permission
externalOotFile.delete(); String externalAssetsPath = getExternalFilesDir(null).getAbsolutePath();
File externalAssetsFolder = new File(getExternalFilesDir(null), "assets");
deleteRecursive(externalAssetsFolder);
if (!permissionPopupWasDeclined) {
requestSpecialExternalStoragePermission();
} }
private void deleteRecursive(File fileOrDirectory) { while(permissionPopupIsOpen) {
if (fileOrDirectory.isDirectory()) { // Do nothing until a permission is chosen
for (File child : fileOrDirectory.listFiles()) { try {
deleteRecursive(child); Thread.sleep(250);
} catch (InterruptedException e) {
// do nothing
} }
} }
fileOrDirectory.delete();
if (hasSpecialExternalStoragePermission) {
// /storage/emulated/0/com.dishii.mm, also mounted at /sdcard/com.dishii.mm
externalAssetsPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + getApplicationContext().getPackageName();
} }
if (!hasInstalledExternalAssetFiles) {
setupFiles(externalAssetsPath);
// 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;
} }
// Request storage permission return externalAssetsPath;
private void requestStoragePermission() { }
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, // Request the special external storage permission
Manifest.permission.WRITE_EXTERNAL_STORAGE}, private void requestSpecialExternalStoragePermission() {
STORAGE_PERMISSION_REQUEST_CODE); // 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;
}
requestPermissions(new String[]{"android.permission.WRITE_EXTERNAL_STORAGE"}, SPECIAL_STORAGE_PERMISSION_REQUEST_CODE);
permissionPopupIsOpen = true;
return;
}
// 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 // Handle permission request result
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, 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) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setupFiles(); hasSpecialExternalStoragePermission = true;
} else { } else {
// Permission denied, handle accordingly (e.g., show a message) permissionPopupWasDeclined = true;
} }
permissionPopupIsOpen = false;
} }
} }
private void setupFiles(){ private void setupFiles(String externalAssetsPath) {
//Copy assets folder for rom extraction AssetCopyUtil.copyAssetsToExternal(this, externalAssetsPath);
File externalAssetsDir = new File(getExternalFilesDir(null), "assets"); hasInstalledExternalAssetFiles = true;
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 2ship.o2r
File externalSohOtrFile = new File(getExternalFilesDir(null), "2ship.o2r");
if (!externalSohOtrFile.exists()) {
try {
InputStream in = getAssets().open("2ship.o2r");
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();
}
}
File configFile = new File(getExternalFilesDir(null), "2ship2harkinian.json");
if (!configFile.exists()) {
try {
InputStream in = getAssets().open("2ship2harkinian.json");
OutputStream out = new FileOutputStream(configFile);
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 native void nativeHandleSelectedFile(String filePath); private native void nativeHandleSelectedFile(String filePath);
@@ -175,10 +179,11 @@ public class MainActivity extends SDLActivity{
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, 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(); Uri selectedFileUri = data.getData();
String fileName = "MM.z64"; String fileName = "MM.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); File destinationFile = new File(destinationDirectory, fileName);
if (destinationDirectory != null) { if (destinationDirectory != null) {
@@ -200,8 +205,20 @@ public class MainActivity extends SDLActivity{
} }
nativeHandleSelectedFile(destinationFile.getPath()); 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() { public void openFilePicker() {
// Create an Intent to open the file picker dialog // Create an Intent to open the file picker dialog
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
@@ -211,14 +228,8 @@ public class MainActivity extends SDLActivity{
startActivityForResult(intent, 0); 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 attachController();
public native void detachController();
// Native method for setting button state // Native method for setting button state
public native void setButton(int button, boolean value); public native void setButton(int button, boolean value);
public native void setCameraState(int axis, float value); public native void setCameraState(int axis, float value);
@@ -284,10 +295,10 @@ public class MainActivity extends SDLActivity{
addTouchListener(buttonX, ControllerButtons.BUTTON_X); // SDL Button 2 (X) addTouchListener(buttonX, ControllerButtons.BUTTON_X); // SDL Button 2 (X)
addTouchListener(buttonY, ControllerButtons.BUTTON_Y); // SDL Button 3 (Y) addTouchListener(buttonY, ControllerButtons.BUTTON_Y); // SDL Button 3 (Y)
addTouchListener(buttonDpadUp, ControllerButtons.BUTTON_DPAD_UP); // SDL Button 10 (D-Pad Up) setupCButtons(buttonDpadUp, ControllerButtons.AXIS_RY, 1); // SDL Button 10 (D-Pad Up)
addTouchListener(buttonDpadDown, ControllerButtons.BUTTON_DPAD_DOWN); // SDL Button 11 (D-Pad Down) setupCButtons(buttonDpadDown, ControllerButtons.AXIS_RY , -1); // SDL Button 11 (D-Pad Down)
addTouchListener(buttonDpadLeft, ControllerButtons.BUTTON_DPAD_LEFT); // SDL Button 12 (D-Pad Left) setupCButtons(buttonDpadLeft, ControllerButtons.AXIS_RX, 1); // SDL Button 12 (D-Pad Left)
addTouchListener(buttonDpadRight, ControllerButtons.BUTTON_DPAD_RIGHT); // SDL Button 13 (D-Pad Right) setupCButtons(buttonDpadRight, ControllerButtons.AXIS_RX, -1); // SDL Button 13 (D-Pad Right)
addTouchListener(buttonLB, ControllerButtons.BUTTON_LB); // SDL Button 4 (LB) addTouchListener(buttonLB, ControllerButtons.BUTTON_LB); // SDL Button 4 (LB)
addTouchListener(buttonRB, ControllerButtons.BUTTON_RB); // SDL Button 5 (RB) addTouchListener(buttonRB, ControllerButtons.BUTTON_RB); // SDL Button 5 (RB)
@@ -309,14 +320,19 @@ public class MainActivity extends SDLActivity{
private void setupToggleButton(Button button, ViewGroup uiGroup){ private void setupToggleButton(Button button, ViewGroup uiGroup){
boolean isHidden = preferences.getBoolean("controlsVisible", false); // Default to 'false' (visible) boolean isHidden = preferences.getBoolean("controlsVisible", false); // Default to 'false' (visible)
uiGroup.setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); // Set the initial visibility based on the saved state uiGroup.setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); // Set the initial visibility based on the saved state
/*if(isHidden){
detachController();
}*/
button.setOnClickListener(new View.OnClickListener() { button.setOnClickListener(new View.OnClickListener() {
boolean isHidden = false; boolean isHidden = false;
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (isHidden) { if (isHidden) {
uiGroup.setVisibility(View.VISIBLE); // Show UI elements uiGroup.setVisibility(View.VISIBLE); // Show UI elements
//attachController();
} else { } else {
uiGroup.setVisibility(View.INVISIBLE); // Hide UI elements uiGroup.setVisibility(View.INVISIBLE); // Hide UI elements
//detachController();
} }
preferences.edit().putBoolean("controlsVisible", !isHidden).apply(); preferences.edit().putBoolean("controlsVisible", !isHidden).apply();
isHidden = !isHidden; // Toggle state isHidden = !isHidden; // Toggle state
@@ -347,6 +363,28 @@ public class MainActivity extends SDLActivity{
}); });
} }
private void setupCButtons(Button button, int buttonNum, int direction) {
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setAxis(buttonNum, direction<0 ? Short.MAX_VALUE : Short.MIN_VALUE);
button.setPressed(true);
return true;
case MotionEvent.ACTION_UP:
setAxis(buttonNum, (short) 0);
button.setPressed(false);
return true;
case MotionEvent.ACTION_CANCEL:
setAxis(buttonNum, (short) 0);
return true;
}
return false;
}
});
}
boolean TouchAreaEnabled = true; boolean TouchAreaEnabled = true;
void DisableTouchArea(){ void DisableTouchArea(){
@@ -60,8 +60,8 @@ import java.util.Locale;
public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
private static final String TAG = "SDL"; private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 2; private static final int SDL_MAJOR_VERSION = 2;
private static final int SDL_MINOR_VERSION = 30; private static final int SDL_MINOR_VERSION = 32;
private static final int SDL_MICRO_VERSION = 5; private static final int SDL_MICRO_VERSION = 0;
/* /*
// Display InputType.SOURCE/CLASS of events and devices // 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_POSITION
| InputDevice.SOURCE_CLASS_TRACKBALL); | 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; 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"; if (s == FLAG_TAINTED) src += " FLAG_TAINTED";
s2 &= ~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); Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src);
} }
@@ -274,7 +274,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
// "SDL2_mixer", // "SDL2_mixer",
// "SDL2_net", // "SDL2_net",
// "SDL2_ttf", // "SDL2_ttf",
"2ship" "main"
}; };
} }
@@ -790,6 +790,9 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
SDLActivity.mFullscreenModeActive = false; 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 { } else {
Log.e(TAG, "error handling message, getContext() returned no Activity"); Log.e(TAG, "error handling message, getContext() returned no Activity");
+2 -2
View File
@@ -6,7 +6,7 @@ buildscript {
google() google()
} }
dependencies { 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@@ -20,6 +20,6 @@ allprojects {
} }
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
+5
View File
@@ -18,3 +18,8 @@ org.gradle.jvmargs=-Xmx1536m
# org.gradle.parallel=true # org.gradle.parallel=true
android.useAndroidX=true android.useAndroidX=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
# set to Release or RelWithDebInfo to avoid slow mm.o2r generation
elfBuildType=Debug
+1 -1
View File
@@ -1,6 +1,6 @@
#Thu Nov 11 18:20:34 PST 2021 #Thu Nov 11 18:20:34 PST 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+2
View File
@@ -148,6 +148,8 @@ file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/filelists/" DESTINATION "${CMAKE_
file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/ActorList_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols") file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/ActorList_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols")
file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/ObjectList_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols") file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/ObjectList_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols")
file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/SymbolMap_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols") file(COPY "${CMAKE_SOURCE_DIR}/OTRExporter/CFG/SymbolMap_MM.txt" DESTINATION "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/assets/extractor/symbols")
file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/mods")
file(TOUCH "${CMAKE_SOURCE_DIR}/Android/app/src/main/assets/mods/custom_mod_files_go_here.txt")
endif() endif()
+1 -3
View File
@@ -4,8 +4,6 @@ A port of 2 Ship 2 Harkinian to Android. <br>
Original Repository: https://github.com/HarbourMasters/2ship2harkinian <br> Original Repository: https://github.com/HarbourMasters/2ship2harkinian <br>
<br> <br>
NOTE: Controller only. No touch controls yet except for in the enhancements menu. <br>
Supported (probably): Android 7+ (OpenGL ES 3.0+ required) <br> Supported (probably): Android 7+ (OpenGL ES 3.0+ required) <br>
Tested On: Android 14 <br> Tested On: Android 14 <br>
@@ -32,7 +30,7 @@ Q: The GUI scaling is too big/too small. <br>
A: There is no GUI scaling option implemented yet. This will come in a future update. <br><br> A: There is no GUI scaling option implemented yet. This will come in a future update. <br><br>
Q: Gyro Aim? <br> Q: Gyro Aim? <br>
A: It will come once it works in the base 2 Ship 2 Harkinian. <br> <br> A: It works. You just need to press any controller button when it asks for input. It will default to your phone's gyro if the controller doesn't support it. <br> <br>
Q: My controller is not doing anything. <br> Q: My controller is not doing anything. <br>
A: Close the Enhancements Menu. If the Enhancements Menu is not open, open it with the Android back button and check if it is detected in Settings->Controller->Controller Mapping. If it is, press refresh. <br><br> A: Close the Enhancements Menu. If the Enhancements Menu is not open, open it with the Android back button and check if it is detected in Settings->Controller->Controller Mapping. If it is, press refresh. <br><br>
+2 -2
View File
@@ -101,8 +101,8 @@ bool Camera_FreeLook(Camera* camera) {
#ifdef __ANDROID__ #ifdef __ANDROID__
if(Ship::Mobile::IsUsingTouchscreenControls()) { if(Ship::Mobile::IsUsingTouchscreenControls()) {
yawDiff = -Ship::Mobile::GetCameraYaw()*10.0f; yawDiff += -Ship::Mobile::GetCameraYaw()*10.0f;
pitchDiff = Ship::Mobile::GetCameraPitch()*10.0f; pitchDiff += Ship::Mobile::GetCameraPitch()*10.0f;
} }
#endif #endif
+3 -9
View File
@@ -79,7 +79,7 @@ enum class ButtonId : int {
#ifdef __ANDROID__ #ifdef __ANDROID__
const char* javaRomPath = NULL; static char javaRomPath[4096] = { 0 };
bool fileDialogOpen = false; bool fileDialogOpen = false;
//function to be called from C //function to be called from C
@@ -92,7 +92,7 @@ void openFilePickerFromC(JNIEnv* env, jobject javaObject) {
// Define the native method to handle the selected file path // Define the native method to handle the selected file path
extern "C" void JNICALL Java_com_dishii_mm_MainActivity_nativeHandleSelectedFile(JNIEnv* env, jobject obj, jstring filePath) { extern "C" void JNICALL Java_com_dishii_mm_MainActivity_nativeHandleSelectedFile(JNIEnv* env, jobject obj, jstring filePath) {
const char* filePathStr = env->GetStringUTFChars(filePath, 0); const char* filePathStr = env->GetStringUTFChars(filePath, 0);
javaRomPath = strdup(filePathStr); // save filepath to string snprintf(javaRomPath, sizeof(javaRomPath), "%s", filePathStr);
fileDialogOpen = false; fileDialogOpen = false;
env->ReleaseStringUTFChars(filePath, filePathStr); env->ReleaseStringUTFChars(filePath, filePathStr);
} }
@@ -322,7 +322,7 @@ bool Extractor::GetRomPathFromBox() {
//Do nothing until it's chosen //Do nothing until it's chosen
SDL_Delay(250); SDL_Delay(250);
} }
SDL_Log("%s",javaRomPath); SDL_Log("javaRomPath: %s", javaRomPath);
selection.push_back(javaRomPath); selection.push_back(javaRomPath);
#endif #endif
if (selection.empty()) { if (selection.empty()) {
@@ -332,12 +332,6 @@ bool Extractor::GetRomPathFromBox() {
mCurrentRomPath = selection[0]; mCurrentRomPath = selection[0];
#endif #endif
mCurRomSize = GetCurRomSize(); mCurRomSize = GetCurRomSize();
#ifdef __ANDROID__
if (javaRomPath) {
free((void*)javaRomPath);
javaRomPath = NULL;
}
#endif
return true; return true;
} }

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