4 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
22 changed files with 378 additions and 198 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,97 +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();
}
}
} }
private native void nativeHandleSelectedFile(String filePath); private native void nativeHandleSelectedFile(String filePath);
@@ -156,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) {
@@ -181,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);
@@ -192,13 +228,6 @@ 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(); public native void detachController();
// Native method for setting button state // Native method for setting button state
@@ -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>
+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;
} }
+14
View File
@@ -37,7 +37,21 @@ typedef enum FlashSlotFile {
((GET_NEWF(save, 0) == 'Z') && (GET_NEWF(save, 1) == 'E') && (GET_NEWF(save, 2) == 'L') && \ ((GET_NEWF(save, 0) == 'Z') && (GET_NEWF(save, 1) == 'E') && (GET_NEWF(save, 2) == 'L') && \
(GET_NEWF(save, 3) == 'D') && (GET_NEWF(save, 4) == 'A') && (GET_NEWF(save, 5) == '3')) (GET_NEWF(save, 3) == 'D') && (GET_NEWF(save, 4) == 'A') && (GET_NEWF(save, 5) == '3'))
#if !defined(__ANDROID__)
const std::filesystem::path savesFolderPath(Ship::Context::GetPathRelativeToAppDirectory("saves", appShortName)); const std::filesystem::path savesFolderPath(Ship::Context::GetPathRelativeToAppDirectory("saves", appShortName));
#else
// in Android, when targeting SurfaceFlinger (ANativeWindow, ART, calling JNI), to avoid problems, one should usually
// try to avoid using C++ features that run port-sensitive code in early initialization BEFORE main() (SDL_main())
// actually runs, like __attribute__((constructor)) and method-initialized global variables,
// because it is necessary to wait until after the Java Activity initializes JNI before calling certain methods.
// the Java Activity will call SDL_main() when it is ready to execute native code, and does not expect native application
// code to be running before that point.
std::filesystem::path savesFolderPath;
// call this function early in main() before any other functions that use savesFolderPath are called.
void SaveManager_Init(void) {
savesFolderPath = Ship::Context::GetPathRelativeToAppDirectory("saves", appShortName);
}
#endif
// Migrations // Migrations
// The idea here is that we can read in any version of the save as generic JSON, then apply migrations // The idea here is that we can read in any version of the save as generic JSON, then apply migrations

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