mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-29 15:23:01 -04:00
Customizable data directory & migration (#1059)
* Customizable data directory & migration * Add file/dir rename fast-path * Write data_location.json to base path on Windows; fix UTF-8 custom paths * Build fix * Another build fix * Android data directory selection * Fix CMake target ref
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -22,6 +26,13 @@ import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
"com.android.externalstorage.documents";
|
||||
|
||||
private long folderDialogUserdata = 0;
|
||||
|
||||
private static native void nativeFolderDialogResult(long userdata, String path, String error);
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
@@ -147,9 +158,154 @@ public class DuskActivity extends SDLActivity {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
|
||||
finishFolderDialog(resultCode, data);
|
||||
return;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public boolean showFolderDialog(long userdata) {
|
||||
if (userdata == 0 || folderDialogUserdata != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
folderDialogUserdata = userdata;
|
||||
runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open folder dialog.", e);
|
||||
finishFolderDialog(Activity.RESULT_CANCELED, null);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void finishFolderDialog(int resultCode, Intent data) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
if (userdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String path = getRealPathForUri(data.getData());
|
||||
if (path != null && !path.isEmpty()) {
|
||||
nativeFolderDialogResult(userdata, path, null);
|
||||
} else {
|
||||
nativeFolderDialogResult(
|
||||
userdata, null, "Selected folder is not available as a filesystem path");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nativeFolderDialogResult(userdata, null, null);
|
||||
}
|
||||
|
||||
private String getRealPathForUri(Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
if (!"content".equals(scheme) ||
|
||||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to resolve URI: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExternalStorageDocumentId(Uri uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
|
||||
return DocumentsContract.getTreeDocumentId(uri);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
|
||||
private static boolean isTreeDocumentUri(Uri uri) {
|
||||
List<String> segments = uri.getPathSegments();
|
||||
return segments.size() >= 2 && "tree".equals(segments.get(0));
|
||||
}
|
||||
|
||||
private String getExternalStoragePathForDocumentId(String documentId) {
|
||||
if (documentId == null || documentId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (documentId.startsWith("raw:")) {
|
||||
return documentId.substring("raw:".length());
|
||||
}
|
||||
|
||||
String[] parts = documentId.split(":", 2);
|
||||
String volumeId = parts[0];
|
||||
String relativePath = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
File root = getExternalStorageRoot(volumeId);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relativePath.isEmpty()
|
||||
? root.getAbsolutePath()
|
||||
: new File(root, relativePath).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getExternalStorageRoot(String volumeId) {
|
||||
if ("primary".equalsIgnoreCase(volumeId)) {
|
||||
return Environment.getExternalStorageDirectory();
|
||||
}
|
||||
if ("home".equalsIgnoreCase(volumeId)) {
|
||||
return new File(
|
||||
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
|
||||
}
|
||||
|
||||
File[] externalFilesDirs = getExternalFilesDirs(null);
|
||||
if (externalFilesDirs != null) {
|
||||
for (File externalFilesDir : externalFilesDirs) {
|
||||
File root = getStorageRootForExternalFilesDir(externalFilesDir);
|
||||
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File fallback = new File("/storage", volumeId);
|
||||
return fallback.exists() ? fallback : null;
|
||||
}
|
||||
|
||||
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
|
||||
if (externalFilesDir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = externalFilesDir.getAbsolutePath();
|
||||
int androidDir = path.indexOf("/Android/");
|
||||
if (androidDir <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(path.substring(0, androidDir));
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
|
||||
+55
-1
@@ -14,15 +14,22 @@ import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
|
||||
|
||||
private static final String ROOT_ID = "dusk";
|
||||
private static final String ROOT_DOCUMENT_ID = "root";
|
||||
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
|
||||
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
@@ -46,13 +53,19 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
ensureUserDirectories();
|
||||
if (!isCustomDataPathEnabled()) {
|
||||
ensureUserDirectories();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
if (isCustomDataPathEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final File root = getRootDirectory();
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
@@ -222,6 +235,11 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
}
|
||||
|
||||
private File getRootDirectory() throws FileNotFoundException {
|
||||
if (isCustomDataPathEnabled()) {
|
||||
throw new FileNotFoundException(
|
||||
"Dusk DocumentsProvider is disabled while a custom data path is configured");
|
||||
}
|
||||
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("Dusklight files directory is unavailable");
|
||||
@@ -273,6 +291,42 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
new File(root, "EUR/Card A").mkdirs();
|
||||
}
|
||||
|
||||
private boolean isCustomDataPathEnabled() {
|
||||
if (getContext() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File filesDir = getContext().getFilesDir();
|
||||
if (filesDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
|
||||
if (!descriptor.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(readText(descriptor));
|
||||
return "custom".equals(json.optString("mode", "default"));
|
||||
} catch (IOException | JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readText(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user