From 17a3fa114553fd8c879033736ab86e14f3191c6b Mon Sep 17 00:00:00 2001 From: Malik <31506138+msmalik681@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:09:34 +0000 Subject: [PATCH] Android Updates added file selection menu so you can copy .pak files from downloads directory to internal files directory. fixed issue with version.sh not being executable. added launcher activity so actions can be performed before the engine starts, like copying a .pak file. removed the custom application code to simplify but may add it back in the future. --- engine/android/app/build.gradle | 18 +- engine/android/app/debug.keystore | Bin 0 -> 2762 bytes .../android/app/src/main/AndroidManifest.xml | 24 +- .../java/org/openbor/engine/GameActivity.java | 119 +-------- .../org/openbor/engine/LauncherActivity.java | 241 ++++++++++++++++++ engine/android/build.sh | 1 + engine/sdl/menu.c | 12 - 7 files changed, 279 insertions(+), 136 deletions(-) create mode 100644 engine/android/app/debug.keystore create mode 100644 engine/android/app/src/main/java/org/openbor/engine/LauncherActivity.java diff --git a/engine/android/app/build.gradle b/engine/android/app/build.gradle index 7a815dcb2..c87f6d09c 100644 --- a/engine/android/app/build.gradle +++ b/engine/android/app/build.gradle @@ -24,9 +24,18 @@ android { } signingConfigs { + debug { + // ... your debug keystore details here ... + // For example, if you have a debug.keystore in your project root + storeFile file('debug.keystore') + storePassword 'android' // Default password for debug keystore + keyAlias 'androiddebugkey' // Default alias + keyPassword 'android' // Default password for alias + } + release { // only try to find keystore.properties when it's release build - if (project.gradle.startParameter.taskNames.any { it.toLowerCase().contains('release') }) { + /* def keystorePropertiesFile = rootProject.file("keystore.properties") def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) @@ -35,7 +44,7 @@ android { keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] - } + */ } } @@ -46,7 +55,10 @@ android { outputFileName = "OpenBOR.apk"; } } - + debug { + // Do NOT add 'signingConfig signingConfigs.release' here + // It should inherit the default debug signing or you define 'signingConfig signingConfigs.debug' + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' diff --git a/engine/android/app/debug.keystore b/engine/android/app/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..1915492644b94548bff2f5543966e289c9658e58 GIT binary patch literal 2762 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 @@ -22,24 +23,37 @@ - - - + + + android:name="org.openbor.engine.LauncherActivity"> + + + + + - + \ No newline at end of file diff --git a/engine/android/app/src/main/java/org/openbor/engine/GameActivity.java b/engine/android/app/src/main/java/org/openbor/engine/GameActivity.java index b1ca60468..d2c31fb37 100644 --- a/engine/android/app/src/main/java/org/openbor/engine/GameActivity.java +++ b/engine/android/app/src/main/java/org/openbor/engine/GameActivity.java @@ -22,39 +22,22 @@ import org.libsdl.app.SDLActivity; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.File; -import java.io.FileOutputStream; - import android.util.Log; import android.os.Bundle; import android.content.Context; import android.os.Build; -import android.content.pm.PackageManager; import android.content.pm.ApplicationInfo; import android.os.PowerManager; -import android.os.PowerManager.*; +import android.os.PowerManager.WakeLock; // Explicitly import WakeLock if you want to be specific, or keep PowerManager.* import android.view.View; import android.view.WindowManager; -import android.content.res.*; -import android.Manifest; -//msmalik681 added imports for new pak copy! -import android.os.Environment; -import android.widget.Toast; -//msmalik681 added import for permission check -import androidx.core.content.ContextCompat; -import androidx.core.app.ActivityCompat; +import android.view.WindowManager; import android.os.Vibrator; import android.os.VibrationEffect; -import android.view.*; -//needed to fix sdk 34+ crashing import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; -import android.os.Build; import org.jetbrains.annotations.Nullable; /** @@ -137,7 +120,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.v("OpenBOR", "onCreate called"); //msmalik681 copy pak for custom apk and notify is paks folder empty - CopyPak(); + // CopyPak(); //CRxTRDude - Added FLAG_KEEP_SCREEN_ON to prevent screen timeout. getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -151,102 +134,6 @@ protected void onCreate(Bundle savedInstanceState) { } } - /** - * Proceed in copying paks files, or just prepare the destination Paks directory depending - * on which type of app it is. - */ - public void CopyPak() - { - try { - Context ctx = getContext(); - Context appCtx = getApplicationContext(); - String toast = null; - - // if package name is literally "org.openbor.engine" then we have no need to copy any .pak files - if (appCtx.getPackageName().equals("org.openbor.engine")) - { - // Default output folder - File outFolderDefault = new File(Environment.getExternalStorageDirectory() + "/OpenBOR/Paks"); - - if (!outFolderDefault.isDirectory()) - { - outFolderDefault.mkdirs(); - toast = "Folder: (" + outFolderDefault + ") is empty!"; - Toast.makeText(appCtx, toast, Toast.LENGTH_LONG).show(); - } - else - { - String[] files = outFolderDefault.list(); - if (files.length == 0) - { - // directory is empty - toast = "Paks Folder: (" + outFolderDefault + ") is empty!"; - Toast.makeText(appCtx, toast, Toast.LENGTH_LONG).show(); - } - } - } - // otherwise it acts like a dedicated app (commercial title, standalone app) - // intend to work with pre-baked single .pak file at build time - else - { - String version = null; - // versionName is "android:versionName" in AndroidManifest.xml - version = appCtx.getPackageManager().getPackageInfo(appCtx.getPackageName(), 0).versionName; // get version number as string - // set local output folder (primary shared/external storage) - File outFolder = new File(ctx.getExternalFilesDir(null) + "/Paks"); - // set local output filename as version number - File outFile = new File(outFolder, version + ".pak"); - - // check if existing pak directory is actually directory, and pak file with matching version - // for this build is there, if not then delete all files residing in such - // directory (old pak files) preparing for updating new one - if (outFolder.isDirectory() && !outFile.exists()) // if local folder true and file does not match version empty folder - { - toast = "Updating please wait!"; - String[] children = outFolder.list(); - for (int i = 0; i < children.length; i++) - { - new File(outFolder, children[i]).delete(); - } - } - else - { - toast = "First time setup, please wait..."; - } - - if (!outFile.exists()) - { - Toast.makeText(appCtx, toast, Toast.LENGTH_LONG).show(); - outFolder.mkdirs(); - - //custom pak should be saved in "app\src\main\assets\bor.pak" - InputStream in = ctx.getAssets().open("bor.pak"); - FileOutputStream out = new FileOutputStream(outFile); - - copyFile(in, out); - in.close(); - in = null; - out.flush(); - out.close(); - out = null; - } - } - } catch (IOException e) { - // not handled - } catch (Exception e) { - // not handled - } - } - - private void copyFile(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) - { - out.write(buffer, 0, read); - } - } - @Override public void onLowMemory() { super.onLowMemory(); diff --git a/engine/android/app/src/main/java/org/openbor/engine/LauncherActivity.java b/engine/android/app/src/main/java/org/openbor/engine/LauncherActivity.java new file mode 100644 index 000000000..65a0cab5f --- /dev/null +++ b/engine/android/app/src/main/java/org/openbor/engine/LauncherActivity.java @@ -0,0 +1,241 @@ +package org.openbor.engine; // Make sure this matches your package name + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +// You might need to import AlertDialog and DialogInterface if you use showErrorAndRetryDialog +// import android.app.AlertDialog; +// import android.content.DialogInterface; + +public class LauncherActivity extends Activity { + + private static final String TAG = "LauncherActivity"; + private static final int PICK_PAK_FILE_REQUEST_CODE = 1; + private static final String DEST_SUB_FOLDER_NAME = "Paks"; // Subdirectory for copied .pak files + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Set your layout if you have one, or keep it simple for testing + // setContentView(R.layout.activity_launcher); + + // Immediately try to show the file picker when the activity is created + showPakSelectionDialog(); + } + + private void showPakSelectionDialog() { + Log.d(TAG, "Showing .pak file selection dialog..."); + Toast.makeText(this, "Select .pak file to copy!.", Toast.LENGTH_LONG).show(); + // ACTION_OPEN_DOCUMENT allows the user to pick a document that is "owned" by an app. + // This is suitable for files the user wants to grant you access to, like from Downloads. + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + + // Filter to show only files that can be opened. + intent.addCategory(Intent.CATEGORY_OPENABLE); + + // Specify the MIME type for .pak files or general binary files. + // A specific MIME type like "application/octet-stream" is often used for arbitrary binary data. + // If your .pak files have a known MIME type, use that. + intent.setType("*/*"); // Allow all file types for broader selection + String[] mimeTypes = {"application/octet-stream", "application/zip", "application/x-pak"}; // Common for custom data, zip + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + + + // OPTIONAL: Suggest a starting location (Android 11+ for this to be consistently respected) + // This tries to open the picker directly in the Downloads directory. + // It's a hint and might not work on all devices/Android versions. + try { + Uri downloadsUri = Uri.parse("content://com.android.providers.downloads.documents/document/downloads"); + if (downloadsUri != null) { + intent.putExtra(android.provider.DocumentsContract.EXTRA_INITIAL_URI, downloadsUri); + } + } catch (Exception e) { + Log.w(TAG, "Could not set initial URI to Downloads. " + e.getMessage()); + } + + + try { + startActivityForResult(intent, PICK_PAK_FILE_REQUEST_CODE); + } catch (Exception e) { + Log.e(TAG, "Could not launch file picker: " + e.getMessage(), e); + Toast.makeText(this, "Error opening file picker. Please ensure a file manager is installed.", Toast.LENGTH_LONG).show(); + // Decide what to do if the picker cannot be launched (e.g., finish activity or retry) + finish(); // For this simple example, we'll just exit + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == PICK_PAK_FILE_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { + Uri uri = data.getData(); + Log.d(TAG, "Selected file URI: " + uri.toString()); + + String fileName = getFileName(uri); // Helper method to get file name from URI + + if (fileName != null && fileName.toLowerCase().endsWith(".pak")) { + File destinationRootFolder = new File(getExternalFilesDir(null), DEST_SUB_FOLDER_NAME); + // Ensure the destination folder exists + if (!destinationRootFolder.exists()) { + if (!destinationRootFolder.mkdirs()) { + Log.e(TAG, "Failed to create destination folder: " + destinationRootFolder.getAbsolutePath()); + Toast.makeText(this, "Failed to create game data folder.", Toast.LENGTH_LONG).show(); + showErrorAndRetryDialog("Failed to prepare game data folder. Try again?"); + return; + } + } + + File destinationFile = new File(destinationRootFolder, fileName); + + // Check if the file already exists in the destination + if (destinationFile.exists()) { + Log.d(TAG, "File " + fileName + " already exists in Paks folder. Launching game."); + Toast.makeText(this, fileName + " already in game folder. Starting game.", Toast.LENGTH_LONG).show(); + startGameActivity(); + } else { + Log.d(TAG, "Selected file: " + fileName + " is a .pak file and needs to be copied. Copying..."); + copySelectedPakFile(uri, fileName); + } + } else { + Log.w(TAG, "Selected file is not a .pak file: " + (fileName != null ? fileName : "Unknown")); + Toast.makeText(this, "Please select a file with the .pak extension.", Toast.LENGTH_LONG).show(); + showPakSelectionDialog(); // Prompt user again + } + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.d(TAG, "File selection cancelled by user."); + Toast.makeText(this, "File selection cancelled.", Toast.LENGTH_SHORT).show(); + // If user cancels, we might want to prompt again or exit + startGameActivity(); // Continue to start engine + } else { + Log.e(TAG, "Unknown result from file picker: resultCode=" + resultCode); + Toast.makeText(this, "An error occurred during file selection.", Toast.LENGTH_LONG).show(); + showPakSelectionDialog(); // Re-prompt + } + } + } + + private void copySelectedPakFile(Uri pakFileUri, String fileName) { + File destinationRootFolder = new File(getExternalFilesDir(null), DEST_SUB_FOLDER_NAME); + File destinationFile = new File(destinationRootFolder, fileName); + + InputStream in = null; + OutputStream out = null; + try { + // !!! Crucial for ACTION_OPEN_DOCUMENT URIs !!! + // Grants your app persistent read access to the selected URI. + // This is the direct solution to the SecurityException you encountered earlier. + final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; + getContentResolver().takePersistableUriPermission(pakFileUri, takeFlags); + + in = getContentResolver().openInputStream(pakFileUri); + if (in == null) { + throw new IOException("Failed to open input stream for URI: " + pakFileUri.toString()); + } + out = new FileOutputStream(destinationFile); + copyFile(in, out); // Your utility copy method + Toast.makeText(this, "Copied " + fileName + " to game data folder!", Toast.LENGTH_LONG).show(); + Log.d(TAG, "Successfully copied: " + fileName + " to " + destinationFile.getAbsolutePath()); + startGameActivity(); // Launch the game after successful copy + } catch (IOException e) { + Log.e(TAG, "Failed to copy selected .pak file: " + fileName, e); + Toast.makeText(this, "Error copying " + fileName + ". Please try again.", Toast.LENGTH_LONG).show(); + // Clean up partially copied file + if (destinationFile.exists() && !destinationFile.delete()) { + Log.w(TAG, "Could not delete partially copied file: " + destinationFile.getAbsolutePath()); + } + showErrorAndRetryDialog("Failed to copy " + fileName + ". Would you like to try again?"); + } catch (SecurityException e) { + Log.e(TAG, "SecurityException while copying file: " + e.getMessage(), e); + Toast.makeText(this, "Permission denied to read selected file. Please ensure app has storage access if prompted, or choose an accessible file.", Toast.LENGTH_LONG).show(); + showErrorAndRetryDialog("Permission denied. Select another file?"); + } finally { + try { + if (in != null) in.close(); + if (out != null) { + out.flush(); // Ensure all buffered data is written + out.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error closing streams for " + fileName + ": " + e.getMessage()); + } + } + } + + // --- Utility Methods (Keep these as they were or adapt them) --- + + // This method needs to be implemented based on your actual game launch +private void startGameActivity() { + Log.d(TAG, "Attempting to start GameActivity..."); // <--- ADD THIS + Toast.makeText(this, "Starting OpenBOR!", Toast.LENGTH_SHORT).show(); + Intent gameIntent = new Intent(this, GameActivity.class); // Assuming GameActivity is your game's main activity + startActivity(gameIntent); + finish(); // Optional: close LauncherActivity if it's no longer needed +} + + // This is a placeholder; implement your actual dialog logic + private void showErrorAndRetryDialog(String message) { + Log.e(TAG, "Error: " + message); + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + // Example of a simple dialog, you'll need AlertDialog imports: + // new AlertDialog.Builder(this) + // .setTitle("Error") + // .setMessage(message) + // .setPositiveButton("Retry", new DialogInterface.OnClickListener() { + // public void onClick(DialogInterface dialog, int which) { + // showPakSelectionDialog(); // Try again + // } + // }) + // .setNegativeButton("Exit", new DialogInterface.OnClickListener() { + // public void onClick(DialogInterface dialog, int which) { + // finish(); // Exit the app + // } + // }) + // .setCancelable(false) // User must choose an option + // .show(); + showPakSelectionDialog(); // For this example, just re-prompt directly + } + + // Helper method to get the file name from a content URI + // This is a common pattern for ACTION_OPEN_DOCUMENT URIs. + private String getFileName(Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + try (android.database.Cursor cursor = getContentResolver().query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME); + if (nameIndex != -1) { + result = cursor.getString(nameIndex); + } + } + } catch (Exception e) { + Log.e(TAG, "Error getting file name from URI: " + uri.toString(), e); + } + } + if (result == null) { + result = uri.getLastPathSegment(); // Fallback + } + return result; + } + + // Utility method to copy an InputStream to an OutputStream + private void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } +} \ No newline at end of file diff --git a/engine/android/build.sh b/engine/android/build.sh index efed59c04..63e771cdc 100755 --- a/engine/android/build.sh +++ b/engine/android/build.sh @@ -72,6 +72,7 @@ fi cd $(dirname $(readlink -f $0)) cd ../ +chmod +x version.sh ./version.sh cd android ./gradlew clean diff --git a/engine/sdl/menu.c b/engine/sdl/menu.c index 896de42dd..c7babbb8e 100644 --- a/engine/sdl/menu.c +++ b/engine/sdl/menu.c @@ -517,19 +517,7 @@ static void drawMenu() s_screen* Image = NULL; putscreen(vscreen,bgscreen,0,0,NULL); - #ifdef ANDROID - char no_paks[MAX_FILENAME_LEN] = "No Mods In Paks Folder:\n"; - strcat(no_paks, paksDir); - char no_paks_2[MAX_FILENAME_LEN]; - strncpy(no_paks_2,no_paks,44); - no_paks_2[44]='\0'; - char *pak_out = no_paks + 44; - strcpy(no_paks,pak_out); - if(dListTotal < 1) printText((isWide ? 30 : 8), (isWide ? 33 : 24), RED, 0, 0, no_paks_2); - if(dListTotal < 1) printText((isWide ? 30 : 8), (isWide ? 43 : 34), RED, 0, 0, no_paks); - #else if(dListTotal < 1) printText((isWide ? 30 : 8), (isWide ? 33 : 24), RED, 0, 0, "No Mods In Paks Folder!"); - #endif for(list = 0; list < dListTotal; list++) { if(list < MAX_MODS_NUM) //Kratus (13-03-21) avoid engine "close" bug