Compare commits

...
Author SHA1 Message Date
Mike Klaas 5cd34a745c Harden android startup path 2026-04-04 08:18:51 -07:00
3 changed files with 75 additions and 23 deletions
@@ -13,18 +13,23 @@ import java.io.OutputStream;
public class FileUtils { public class FileUtils {
static boolean copyRecursively(ContentResolver contentResolver, DocumentFile src, File dest) { static boolean copyRecursively(ContentResolver contentResolver, DocumentFile src, File dest) {
if (dest == null || (!dest.exists() && !dest.mkdirs())) {
return false;
}
final DocumentFile[] documentFiles = src.listFiles(); final DocumentFile[] documentFiles = src.listFiles();
for (final DocumentFile documentFile : documentFiles) { for (final DocumentFile documentFile : documentFiles) {
final String name = documentFile.getName();
if (name == null || name.isEmpty()) {
return false;
}
if (documentFile.isFile()) { if (documentFile.isFile()) {
if (!copyFile(contentResolver, documentFile, new File(dest, documentFile.getName()))) { if (!copyFile(contentResolver, documentFile, new File(dest, name))) {
return false; return false;
} }
} else if (documentFile.isDirectory()) { } else if (documentFile.isDirectory()) {
final File subdirectory = new File(dest, documentFile.getName()); final File subdirectory = new File(dest, name);
if (!subdirectory.exists()) {
subdirectory.mkdir();
}
if (!copyRecursively(contentResolver, documentFile, subdirectory)) { if (!copyRecursively(contentResolver, documentFile, subdirectory)) {
return false; return false;
} }
@@ -34,18 +39,22 @@ public class FileUtils {
} }
private static boolean copyFile(ContentResolver contentResolver, DocumentFile src, File dest) { private static boolean copyFile(ContentResolver contentResolver, DocumentFile src, File dest) {
try { final File parent = dest.getParentFile();
final InputStream inputStream = contentResolver.openInputStream(src.getUri()); if (parent != null && !parent.exists() && !parent.mkdirs()) {
final OutputStream outputStream = new FileOutputStream(dest); return false;
}
try (final InputStream inputStream = contentResolver.openInputStream(src.getUri());
final OutputStream outputStream = new FileOutputStream(dest)) {
if (inputStream == null) {
return false;
}
final byte[] buffer = new byte[16384]; final byte[] buffer = new byte[16384];
int bytesRead; int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) { while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); outputStream.write(buffer, 0, bytesRead);
} }
inputStream.close();
outputStream.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return false; return false;
@@ -2,10 +2,12 @@ package com.alexbatalov.fallout2ce;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
@@ -17,18 +19,18 @@ public class ImportActivity extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); launchImportPicker();
startActivityForResult(intent, IMPORT_REQUEST_CODE);
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) { protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == IMPORT_REQUEST_CODE) { if (requestCode == IMPORT_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
final Uri treeUri = resultData.getData(); final Uri treeUri = resultData != null ? resultData.getData() : null;
if (treeUri != null) { if (treeUri != null) {
grantImportPermissions(treeUri, resultData);
final DocumentFile treeDocument = DocumentFile.fromTreeUri(this, treeUri); final DocumentFile treeDocument = DocumentFile.fromTreeUri(this, treeUri);
if (treeDocument != null) { if (treeDocument != null && treeDocument.isDirectory()) {
copyFiles(treeDocument); copyFiles(treeDocument);
return; return;
} }
@@ -42,20 +44,59 @@ public class ImportActivity extends Activity {
} }
private void copyFiles(DocumentFile treeDocument) { private void copyFiles(DocumentFile treeDocument) {
ProgressDialog dialog = createProgressDialog(); final ProgressDialog dialog = createProgressDialog();
dialog.show(); dialog.show();
new Thread(() -> { new Thread(() -> {
ContentResolver contentResolver = getContentResolver(); final ContentResolver contentResolver = getContentResolver();
File externalFilesDir = getExternalFilesDir(null); final File externalFilesDir = getExternalFilesDir(null);
FileUtils.copyRecursively(contentResolver, treeDocument, externalFilesDir); final boolean success = externalFilesDir != null
&& FileUtils.copyRecursively(contentResolver, treeDocument, externalFilesDir);
startMainActivity(); runOnUiThread(() -> {
dialog.dismiss(); dialog.dismiss();
finish();
if (success) {
startMainActivity();
} else {
Toast.makeText(this, R.string.import_failed, Toast.LENGTH_LONG).show();
}
finish();
});
}).start(); }).start();
} }
private void launchImportPicker() {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
try {
startActivityForResult(intent, IMPORT_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.import_picker_unavailable, Toast.LENGTH_LONG).show();
finish();
}
}
private void grantImportPermissions(Uri treeUri, Intent resultData) {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
if (resultData != null) {
flags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
try {
getContentResolver().takePersistableUriPermission(treeUri, flags);
} catch (SecurityException ignored) {
// Some providers do not offer persistable grants. One-shot access is enough for import.
}
}
private void startMainActivity() { private void startMainActivity() {
Intent intent = new Intent(this, MainActivity.class); Intent intent = new Intent(this, MainActivity.class);
startActivity(intent); startActivity(intent);
@@ -2,4 +2,6 @@
<string name="app_name">Fallout 2</string> <string name="app_name">Fallout 2</string>
<string name="loading_dialog_title">PLEASE STAND BY</string> <string name="loading_dialog_title">PLEASE STAND BY</string>
<string name="loading_dialog_message">Copying files…</string> <string name="loading_dialog_message">Copying files…</string>
<string name="import_failed">Failed to import game files. Please pick a valid Fallout 2 folder and try again.</string>
<string name="import_picker_unavailable">No compatible folder picker is available on this device.</string>
</resources> </resources>