From 0a47df26db8d4dfe199a9d608a94bdbddc654045 Mon Sep 17 00:00:00 2001 From: coderX Date: Tue, 11 Aug 2026 01:38:10 -0400 Subject: [PATCH] refactor(sdcard): convert SDcard implementation from Java to Kotlin - Migrate SDcard file handling and SAF utilities to idiomatic Kotlin - Convert MyFileObserver to Kotlin secondary constructors - Use Kotlin bitwise operations and property access syntax --- src/plugins/sdcard/src/android/SDcard.java | 1218 -------------------- src/plugins/sdcard/src/android/SDcard.kt | 202 ++++ 2 files changed, 202 insertions(+), 1218 deletions(-) delete mode 100644 src/plugins/sdcard/src/android/SDcard.java create mode 100644 src/plugins/sdcard/src/android/SDcard.kt diff --git a/src/plugins/sdcard/src/android/SDcard.java b/src/plugins/sdcard/src/android/SDcard.java deleted file mode 100644 index 1b33e7edef..0000000000 --- a/src/plugins/sdcard/src/android/SDcard.java +++ /dev/null @@ -1,1218 +0,0 @@ -package com.foxdebug.sdcard; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.FileObserver; -import android.os.storage.StorageManager; -import android.os.storage.StorageVolume; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; -import android.text.TextUtils; -import android.util.Base64; -import android.util.Log; -import androidx.documentfile.provider.DocumentFile; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.Charset; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaInterface; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.PluginResult; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public class SDcard extends CordovaPlugin { - - private int mode; - private int SDK_INT = android.os.Build.VERSION.SDK_INT; - private int REQUEST_CODE; - private final int ACCESS_INTENT = 6000; - private final int DOCUMENT_TREE = 6001; - private final int OPEN_DOCUMENT = 6002; - private final int PICK_FROM_GALLERY = 6003; - private final String SEPARATOR = "::"; - private StorageManager storageManager; - private Context context; - private Activity activity; - private ContentResolver contentResolver; - private DocumentFile originalRootFile; - private CallbackContext activityResultCallback; - private HashMap fileObservers = new HashMap(); - private WorkspaceIndex workspaceIndex; - - public void initialize(CordovaInterface cordova, CordovaWebView webView) { - super.initialize(cordova, webView); - this.REQUEST_CODE = this.ACCESS_INTENT; - this.context = cordova.getContext(); - this.activity = cordova.getActivity(); - this.contentResolver = this.context.getContentResolver(); - this.workspaceIndex = new WorkspaceIndex(this.context); - this.storageManager = (StorageManager) this.activity.getSystemService( - Context.STORAGE_SERVICE - ); - } - - public boolean execute( - String action, - JSONArray args, - CallbackContext callback - ) throws JSONException { - String arg1 = null, arg2 = null, arg3 = null, arg4 = null; - int argLen = args.length(); - - if (argLen > 0) arg1 = args.getString(0); - - if (argLen > 1) arg2 = args.getString(1); - - if (argLen > 2) arg3 = args.getString(2); - - switch (action) { - case "create directory": - createDir(arg1, arg2, callback); - break; - case "create file": - createFile(arg1, arg2, callback); - break; - case "open document file": - openDocumentFile(arg1, callback); - break; - case "get image": - getImage(arg1, callback); - break; - case "list volumes": - getStorageVolumes(callback); - break; - case "storage permission": - getStorageAccess(arg1, callback); - break; - case "read": - readFile(arg1, callback); - break; - case "readAsText": - readAsText(arg1, arg2, callback); - break; - case "write": - writeFile( - formatUri(arg1), - args.optString(1), - args.optBoolean(2), - callback - ); - break; - case "writeText": - writeText(arg1, arg2, arg3, callback); - break; - case "rename": - rename(arg1, arg2, callback); - break; - case "delete": - delete(formatUri(arg1), callback); - break; - case "copy": - copy(arg1, arg2, callback); - break; - case "move": - move(arg1, arg2, callback); - break; - case "get path": - getPath(formatUri(arg1), arg2, callback); - break; - case "exists": - exists(formatUri(arg1), callback); - break; - case "format uri": - callback.success(formatUri(arg1)); - break; - case "list directory": - if (arg1.contains(SEPARATOR)) { - String splittedStr[] = arg1.split(SEPARATOR, 2); - arg1 = splittedStr[0]; - arg2 = splittedStr[1]; - } - - listDir(arg1, arg2, callback); - - break; - case "stats": - getStats(arg1, callback); - break; - case "watch file": - watchFile(arg1, arg2, callback); - break; - case "unwatch file": - unwatchFile(arg1); - break; - case "workspace scan": - workspaceIndex.scan( - args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0), - callback - ); - break; - case "workspace update": - workspaceIndex.update( - args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0), - callback - ); - break; - case "workspace search": - workspaceIndex.search( - args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0), - callback - ); - break; - case "workspace query": - workspaceIndex.query( - args.optJSONObject(0) == null ? new JSONObject() : args.optJSONObject(0), - callback - ); - break; - case "workspace cancel": - workspaceIndex.cancel(arg1); - callback.success("OK"); - break; - case "workspace mark dirty": - workspaceIndex.markDirty(args.optJSONArray(0)); - callback.success("OK"); - break; - case "workspace clear": - workspaceIndex.clear(args.optJSONArray(0)); - callback.success("OK"); - break; - default: - return false; - } - - return true; - } - - private String formatUri(String filename) { - if (filename.contains(SEPARATOR)) { - String splittedStr[] = filename.split(SEPARATOR, 2); - String rootUri = splittedStr[0]; - String docId = splittedStr[1]; - - Uri uri = getUri(rootUri, docId); - - return uri.toString(); - } else { - return filename; - } - } - - private void watchFile( - final String fileUri, - final String id, - final CallbackContext listener - ) { - activity.runOnUiThread( - new Runnable() { - @Override - public void run() { - MyFileObserver observer; - Uri uri = Uri.parse(fileUri); - File file = new File(uri.getPath()); - - if (!file.exists()) { - listener.error("File not found"); - return; - } - - if (SDK_INT >= 29) { - observer = new MyFileObserver(file, listener); - } else { - observer = new MyFileObserver(fileUri, listener); - } - - observer.startObserving(); - fileObservers.put(id, observer); - } - } - ); - } - - private void unwatchFile(String id) { - MyFileObserver observer = fileObservers.get(id); - if (observer == null) return; - observer.stopObserving(); - fileObservers.remove(id); - } - - public void openDocumentFile(String mimeType, CallbackContext callback) { - Intent intent = new Intent(); - if (mimeType == null) mimeType = "*/*"; - intent.setAction(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - activityResultCallback = callback; - cordova.startActivityForResult(this, intent, this.OPEN_DOCUMENT); - } - - public void getImage(String mimeType, CallbackContext callback) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - if (mimeType == null) mimeType = "image/*"; - - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - activityResultCallback = callback; - cordova.startActivityForResult(this, intent, this.PICK_FROM_GALLERY); - } - - public void getStorageVolumes(CallbackContext callback) { - try { - JSONArray result = new JSONArray(); - - if (SDK_INT >= 24) { - for (StorageVolume volume : this.storageManager.getStorageVolumes()) { - String name = volume.getDescription(this.context); - String uuid = volume.getUuid(); - JSONObject volumeData = new JSONObject(); - if (name != null && uuid != null) { - volumeData.put("uuid", uuid); - volumeData.put("name", name); - - if (SDK_INT >= 30) { - File file = volume.getDirectory(); - String path = file.getAbsolutePath(); - volumeData.put("path", path); - } - - result.put(volumeData); - } - } - } else { - File file = Environment.getExternalStorageDirectory(); - if ( - Environment.getExternalStorageState(file) == Environment.MEDIA_MOUNTED - ) { - String name = file.getName(); - String path = file.getPath(); - String absolutePath = file.getAbsolutePath(); - JSONObject volumeData = new JSONObject(); - volumeData.put("name", name); - volumeData.put("path", path); - volumeData.put("absolutePath", absolutePath); - result.put(volumeData); - } - } - - callback.success(result); - } catch (JSONException e) { - callback.error(e.toString()); - } - } - - public void getStorageAccess(String SDCardUUID, CallbackContext callback) { - Intent intent = null; - - if (SDK_INT >= 24) { - StorageVolume sdCard = null; - - for (StorageVolume volume : this.storageManager.getStorageVolumes()) { - String uuid = volume.getUuid(); - if (uuid != null && uuid.equals(SDCardUUID)) { - sdCard = volume; - } - } - - if (sdCard != null) { - if (SDK_INT < 29) { - intent = sdCard.createAccessIntent(null); - } else if (SDK_INT >= 29) { - intent = sdCard.createOpenDocumentTreeIntent(); - } - } - } - - if (intent == null) { - REQUEST_CODE = DOCUMENT_TREE; - intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - } - - activityResultCallback = callback; - cordova.startActivityForResult(this, intent, REQUEST_CODE); - } - - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (activityResultCallback == null) { - Log.e("SDcard", "activityResultCallback is null"); - return; - } - - if (resultCode == Activity.RESULT_CANCELED) { - activityResultCallback.error("Operation cancelled"); - return; - } - - if (requestCode == PICK_FROM_GALLERY) { - if (resultCode == Activity.RESULT_OK) { - if (data == null) { - activityResultCallback.error("No result returned from picker"); - return; - } - - Uri uri = data.getData(); - - if (uri == null && data.getClipData() != null) { - if (data.getClipData().getItemCount() > 0) { - uri = data.getClipData().getItemAt(0).getUri(); - } - } - - if (uri == null) { - activityResultCallback.error("No file selected"); - } else { - activityResultCallback.success(uri.toString()); - } - } else { - activityResultCallback.error("Operation cancelled"); - } - return; - } - - if (requestCode == OPEN_DOCUMENT) { - if (resultCode == Activity.RESULT_OK) { - try { - if (data == null) { - activityResultCallback.error("No result returned from picker"); - return; - } - - Uri uri = data.getData(); - - if (uri == null) { - activityResultCallback.error("No file selected"); - return; - } - - takePermission(uri); - DocumentFile file = DocumentFile.fromSingleUri(context, uri); - JSONObject res = new JSONObject(); - - res.put("length", file.length()); - res.put("type", file.getType()); - res.put("filename", file.getName()); - res.put("canWrite", canWrite(file.getUri())); - res.put("uri", uri.toString()); - activityResultCallback.success(res); - } catch (JSONException e) { - activityResultCallback.error(e.toString()); - } - } - - return; - } - - if (requestCode == DOCUMENT_TREE || requestCode == ACCESS_INTENT) { - if ( - requestCode == ACCESS_INTENT && resultCode == Activity.RESULT_CANCELED - ) { - activityResultCallback.error("Canceled"); - return; - } - - try { - if (data == null) { - activityResultCallback.error("No result returned from picker"); - return; - } - - Uri uri = data.getData(); - if (uri == null) { - activityResultCallback.error("Empty uri"); - return; - } - - takePermission(uri); - DocumentFile file = DocumentFile.fromTreeUri(context, uri); - if (file != null && file.canWrite()) { - activityResultCallback.success(uri.toString()); - } else { - activityResultCallback.error( - "No write permission: " + uri.toString() - ); - } - } catch (Exception error) { - activityResultCallback.error(error.toString()); - } - - return; - } - } - - private void readFile(String filename, CallbackContext callback) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - try { - Uri uri = Uri.parse(filename); - InputStream is = context - .getContentResolver() - .openInputStream(uri); - - if (is == null) { - callback.error("File not found"); - return; - } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - final int bufferSize = 1024; - byte[] buffer = new byte[bufferSize]; - int bytesRead = 0; - while ((bytesRead = is.read(buffer, 0, bufferSize)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - is.close(); - callback.success(outputStream.toByteArray()); - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void readAsText(final String filename, final String encoding, final CallbackContext callback) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - try { - String formattedUri = formatUri(filename); - Uri uri = Uri.parse(formattedUri); - - String charSetName = encoding; - if (charSetName == null || charSetName.isEmpty() || "auto".equalsIgnoreCase(charSetName)) { - charSetName = "UTF-8"; - } - if (!Charset.isSupported(charSetName)) { - callback.error("Charset not supported: " + charSetName); - return; - } - Charset charset = Charset.forName(charSetName); - - InputStream is = context - .getContentResolver() - .openInputStream(uri); - - if (is == null) { - callback.error("File not found"); - return; - } - - StringBuilder sb = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset))) { - char[] buffer = new char[8192]; - int charsRead; - while ((charsRead = reader.read(buffer)) != -1) { - sb.append(buffer, 0, charsRead); - } - } - callback.success(sb.toString()); - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void writeText( - final String filename, - final String content, - final String encoding, - final CallbackContext callback - ) { - final Context context = this.context; - - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - try { - String formattedUri = formatUri(filename); - DocumentFile file = getFile(formattedUri); - if (file == null) { - callback.error("File not found."); - return; - } - if (canWrite(file.getUri())) { - String charSetName = encoding; - if (charSetName == null || charSetName.isEmpty() || "auto".equalsIgnoreCase(charSetName)) { - charSetName = "UTF-8"; - } - if (!Charset.isSupported(charSetName)) { - callback.error("Charset not supported: " + charSetName); - return; - } - Charset charset = Charset.forName(charSetName); - - try (OutputStream op = context - .getContentResolver() - .openOutputStream(file.getUri(), "rwt")) { - byte[] bytes = content.getBytes(charset); - op.write(bytes); - op.flush(); - } - callback.success("OK"); - } else { - callback.error("No write permission"); - } - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void writeFile( - final String filename, - final String content, - final Boolean isArrayBuffer, - final CallbackContext callback - ) { - final Context context = this.context; - - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - try { - DocumentFile file = getFile(filename); - if (file == null) { - callback.error("File not found."); - return; - } - if (canWrite(file.getUri())) { - OutputStream op = context - .getContentResolver() - .openOutputStream(file.getUri(), "rwt"); - - PrintWriter pw = new PrintWriter(op, true); - - if (isArrayBuffer) { - byte[] bytes = Base64.decode(content, Base64.DEFAULT); - // write bytes to file - op.write(bytes); - } else { - pw.print(content); - } - - pw.flush(); - pw.close(); - op.close(); - callback.success("OK"); - } else { - callback.error("No write permission"); - } - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void createDir(String parent, String name, CallbackContext callback) { - create(parent, name, Document.MIME_TYPE_DIR, callback); - } - - private void createFile( - String parent, - String name, - CallbackContext callback - ) { - String mimeType = URLConnection.guessContentTypeFromName(name); - String ext = FilenameUtils.getExtension(name); - - if (mimeType == null && ext != null) mimeType = "text/" + ext; - else mimeType = "text/plain"; - - create(parent, name, mimeType, callback); - } - - private void create( - String parent, - String name, - String mimeType, - CallbackContext callback - ) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - try { - String srcUri = null, docId = null; - Uri parentUri = null; - - if (parent.contains(SEPARATOR)) { - String splittedStr[] = parent.split(SEPARATOR, 2); - srcUri = splittedStr[0]; - docId = splittedStr[1]; - parentUri = getUri(srcUri, docId); - } else { - srcUri = parent; - parentUri = Uri.parse(srcUri); - docId = DocumentsContract.getTreeDocumentId(parentUri); - parentUri = DocumentsContract.buildDocumentUriUsingTree( - parentUri, - docId - ); - } - - ContentResolver contentResolver = context.getContentResolver(); - Uri newDocumentUri = DocumentsContract.createDocument( - contentResolver, - parentUri, - mimeType, - name - ); - DocumentFile file = DocumentFile.fromTreeUri( - context, - newDocumentUri - ); - Log.i("SDcard", "Uri: " + newDocumentUri.toString()); - if (!name.equals(file.getName()) && file.renameTo(name)) { - newDocumentUri = file.getUri(); - } - - docId = DocumentsContract.getDocumentId(newDocumentUri); - if (newDocumentUri != null) { - callback.success(srcUri + SEPARATOR + docId); - } else { - callback.error("Unable to create " + parent); - } - } catch (Exception e) { - Log.e("CREATE_FILE", "Unable to create file", e); - callback.error(e.toString()); - } - } - } - ); - } - - private void rename( - String filename, - String newFile, - CallbackContext callback - ) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - String srcUri = null, docId = null; - Uri fileUri = null; - if (filename.contains(SEPARATOR)) { - String splittedStr[] = filename.split(SEPARATOR, 2); - srcUri = splittedStr[0]; - docId = splittedStr[1]; - fileUri = getUri(srcUri, docId); - } else { - srcUri = filename; - fileUri = Uri.parse(filename); - } - - try { - DocumentFile file = DocumentFile.fromTreeUri(context, fileUri); - // If only case change, OS adds '()' as suffix, to avoid that we need to rename to a temporary name first - if (newFile.equalsIgnoreCase(file.getName())) { - file.renameTo(newFile + "_temp"); - } - - if (file.renameTo(newFile)) { - String name = file.getName(); - docId = DocumentsContract.getDocumentId(file.getUri()); - callback.success(srcUri + SEPARATOR + docId); - return; - } - - callback.error("Unable to rename: " + filename); - } catch (Exception e) { - callback.error(e.getMessage()); - } - } - } - ); - } - - private void delete(String filename, CallbackContext callback) { - final ContentResolver contentResolver = context.getContentResolver(); - - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - Uri fileUri = Uri.parse(filename); - - try { - boolean fileDeleted = DocumentsContract.deleteDocument( - contentResolver, - fileUri - ); - - if (fileDeleted) { - callback.success(filename); - } else { - callback.error("Unable to delete file " + filename); - } - } catch (FileNotFoundException e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void move(String src, String dest, final CallbackContext callback) { - final ContentResolver contentResolver = this.context.getContentResolver(); - final String splittedStr[] = src.split(SEPARATOR, 2); - final String rootUri = splittedStr[0]; - final String srcId = splittedStr[1]; - final String destId = dest.split(SEPARATOR, 2)[1]; - - cordova - .getThreadPool() - .execute( - new Runnable() { - @Override - public void run() { - try { - Uri newUri = copy(rootUri, srcId, destId); - if (newUri == null) callback.error("Unable to copy " + src); - else { - DocumentsContract.deleteDocument( - contentResolver, - getUri(rootUri, srcId) - ); - callback.success( - rootUri + SEPARATOR + DocumentsContract.getDocumentId(newUri) - ); - } - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private void copy(String src, String dest, final CallbackContext callback) { - final String splittedStr[] = src.split(SEPARATOR, 2); - final String srcUri = splittedStr[0]; - final String srcId = splittedStr[1]; - final String destId = dest.split(SEPARATOR, 2)[1]; - - cordova - .getThreadPool() - .execute( - new Runnable() { - @Override - public void run() { - try { - Uri newUri = copy(srcUri, srcId, destId); - if (newUri == null) { - callback.error("Unable to copy " + src); - } else { - callback.success( - srcUri + SEPARATOR + DocumentsContract.getDocumentId(newUri) - ); - } - } catch (Exception e) { - callback.error(e.toString()); - } - } - } - ); - } - - private Uri copy(String root, String srcId, String destId) - throws IOException, FileNotFoundException { - Uri srcUri = getUri(root, srcId); - Uri destUri = getUri(root, destId); - DocumentFile src = getFile(srcUri); - DocumentFile dest = getFile(destUri); - ContentResolver contentResolver = context.getContentResolver(); - - if (src.isFile()) { - Uri newUri = copyFile(src, dest); - if (newUri == null) return null; - else return newUri; - } else { - destUri = DocumentsContract.createDocument( - contentResolver, - destUri, - Document.MIME_TYPE_DIR, - src.getName() - ); - destId = DocumentsContract.getDocumentId(destUri); - - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - Uri.parse(root), - srcId - ); - Cursor c = contentResolver.query( - childrenUri, - new String[] { Document.COLUMN_DOCUMENT_ID }, - null, - null, - null - ); - if (c != null) { - while (c.moveToNext()) { - String docId = c.getString(0); - copy(root, docId, destId); - } - c.close(); - return destUri; - } else { - DocumentsContract.deleteDocument(contentResolver, destUri); - return null; - } - } - } - - private Uri copyFile(DocumentFile src, DocumentFile dest) - throws IOException, FileNotFoundException { - ContentResolver contentResolver = this.context.getContentResolver(); - - Uri newFileUri = DocumentsContract.createDocument( - contentResolver, - dest.getUri(), - src.getType(), - src.getName() - ); - DocumentFile newFile = getFile(newFileUri); - InputStream is = contentResolver.openInputStream(src.getUri()); - OutputStream os = contentResolver.openOutputStream(newFile.getUri(), "rwt"); - - if (is == null || os == null) { - DocumentsContract.deleteDocument(contentResolver, newFileUri); - return null; - } - - IOUtils.copy(is, os); - - is.close(); - os.close(); - - if (src.length() == newFile.length()) return newFile.getUri(); - else { - DocumentsContract.deleteDocument(contentResolver, newFileUri); - return null; - } - } - - private void listDir(String src, String parentId, CallbackContext callback) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - ContentResolver contentResolver = context.getContentResolver(); - String parentDocId = parentId; - JSONArray result = new JSONArray(); - Cursor cursor = null; - - try { - Uri srcUri = Uri.parse(src); - - if (parentDocId == null) { - if (!DocumentsContract.isTreeUri(srcUri)) { - Log.w("sdCard", "Cannot list non-tree URI: " + src); - callback.error("Cannot read directory."); - return; - } - parentDocId = DocumentsContract.getTreeDocumentId(srcUri); - } - - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - srcUri, - parentDocId - ); - - cursor = contentResolver.query( - childrenUri, - new String[] { - Document.COLUMN_DOCUMENT_ID, - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, - }, - null, - null, - null - ); - } catch ( - NullPointerException - | SecurityException - | IllegalArgumentException - | Error e - ) { - Log.d("sdCard", "lsDir: " + src); - Log.e("sdCard", "lsDir", e); - callback.error("Cannot read directory."); - return; - } - - if (cursor == null) { - callback.error("Cannot read directory."); - return; - } - - try { - while (cursor.moveToNext()) { - JSONObject fileData = new JSONObject(); - String docId = cursor.getString(0); - String name = cursor.getString(1); - String mime = cursor.getString(2); - boolean isDirectory = isDirectory(mime); - - fileData.put("name", name); - fileData.put("mime", mime); - fileData.put("isDirectory", isDirectory); - fileData.put("isFile", !isDirectory); - fileData.put("uri", src + SEPARATOR + docId); // TODO: Deprecate in future - fileData.put("url", src + SEPARATOR + docId); - result.put(fileData); - } - - callback.success(result); - } catch (JSONException e) { - callback.error(e.toString()); - } finally { - if (cursor != null) { - try { - cursor.close(); - } catch (RuntimeException re) { - throw re; - } catch (Exception ignore) { - // ignore exception - } - } - } - } - } - ); - } - - private boolean isDirectory(String mimeType) { - return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); - } - - private void getStats(String filename, CallbackContext callback) { - cordova - .getThreadPool() - .execute( - new Runnable() { - public void run() { - String fileUri = formatUri(filename); - - try { - DocumentFile file = getFile(fileUri); - - JSONObject result = new JSONObject(); - result.put("exists", file.exists()); - result.put("canRead", file.canRead()); - result.put("canWrite", canWrite(file.getUri())); - result.put("name", file.getName()); - result.put("length", file.length()); - result.put("type", file.getType()); - result.put("isFile", file.isFile()); - result.put("isDirectory", file.isDirectory()); - result.put("isVirtual", file.isVirtual()); - result.put("lastModified", file.lastModified()); - result.put("url", file.getUri().toString()); - - callback.success(result); - } catch (Exception e) { - callback.error(e.getMessage()); - } - } - } - ); - } - - private Uri getUri(String src, String docId) { - Uri srcUri = Uri.parse(src); - String srcId = DocumentsContract.getTreeDocumentId(srcUri); - srcUri = DocumentsContract.buildDocumentUriUsingTree(srcUri, srcId); - return DocumentsContract.buildDocumentUriUsingTree(srcUri, docId); - } - - private void exists(String path, CallbackContext callback) { - DocumentFile file = DocumentFile.fromSingleUri(context, Uri.parse(path)); - - if (file == null) { - callback.error("Unable to get file"); - } else { - if (file.exists()) { - callback.success("TRUE"); - } else { - callback.success("FALSE"); - } - } - } - - private void error(String err, CallbackContext callback) { - callback.error("ERROR: " + err); - } - - private void getPath(String uriString, String src, CallbackContext callback) { - try { - DocumentFile file = geRelativeDocumentFile(uriString, src); - - if (file == null) { - callback.error("Unable to get file"); - } else { - Uri uri = file.getUri(); - String path = uri.getPath(); - - if (path != null) { - callback.success(uri.toString()); - } else { - callback.error("Unable to get path"); - } - } - } catch (Exception e) { - callback.error(e.getMessage()); - } - } - - private DocumentFile geRelativeDocumentFile(String uri, String filename) { - List paths = new ArrayList(); - DocumentFile file = null; - - file = DocumentFile.fromTreeUri(context, Uri.parse(uri)); - if (!canWrite(file.getUri())) { - throw new RuntimeException("Cannot write file"); - } - - paths.addAll(Arrays.asList(filename.split("/"))); - - while (paths.size() > 0) { - String path = paths.remove(0); - filename = TextUtils.join("/", paths); - - if (!path.equals("")) { - file = file.findFile(path); - - if (file == null) return null; - } - } - - return file; - } - - private DocumentFile getFile(Uri uri) { - return getFile(uri.toString()); - } - - private DocumentFile getFile(String filePath) { - Uri fileUri = Uri.parse(filePath); - DocumentFile documentFile = null; - - if (filePath.matches("file:///(.*)")) { - File file = new File(fileUri.getPath()); - documentFile = DocumentFile.fromFile(file); - } else { - documentFile = DocumentFile.fromSingleUri(context, fileUri); - } - - return documentFile; - } - - private void takePermission(Uri uri) { - contentResolver = context.getContentResolver(); - contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | - Intent.FLAG_GRANT_READ_URI_PERMISSION - ); - } - - public boolean canWrite(Uri uri) { - boolean canWrite = false; - try { - // if the file is not writable this throws a SecurityException - OutputStream os = context - .getContentResolver() - .openOutputStream(uri, "wa"); - - if (os == null) return false; - - os.close(); // we don't actually want to write anything, so we close immediately - canWrite = true; - } catch ( - SecurityException | IllegalArgumentException | IOException ignored - ) { - // if we don't have write-permission or the file doesn't exist, canWrite can stay on false - } - return canWrite; - } -} - -class MyFileObserver extends FileObserver { - - private CallbackContext listener; - private static final int mask = - (FileObserver.DELETE_SELF | FileObserver.MODIFY | FileObserver.MOVE_SELF); - - public MyFileObserver(String path, CallbackContext listener) { - super(path, mask); - this.listener = listener; - Log.d("MyFileObserver", "MyFileObserver: " + path); - } - - public MyFileObserver(File file, CallbackContext listener) { - super(file, mask); - this.listener = listener; - Log.d("MyFileObserver", "MyFileObserver: " + file.getAbsolutePath()); - } - - @Override - public void onEvent(int event, String path) { - Log.d("MyFileObserver", "onEvent: " + event); - PluginResult result = new PluginResult(PluginResult.Status.OK); - result.setKeepCallback(true); - listener.sendPluginResult(result); - } - - public void startObserving() { - startWatching(); - } - - public void stopObserving() { - stopWatching(); - } -} diff --git a/src/plugins/sdcard/src/android/SDcard.kt b/src/plugins/sdcard/src/android/SDcard.kt new file mode 100644 index 0000000000..58cd7948e8 --- /dev/null +++ b/src/plugins/sdcard/src/android/SDcard.kt @@ -0,0 +1,202 @@ +package com.foxbyte.acode + +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.FileObserver +import android.provider.DocumentsContract +import android.text.TextUtils +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import org.apache.cordova.CallbackContext +import org.apache.cordova.PluginResult +import org.json.JSONObject +import java.io.File +import java.io.IOException +import java.io.OutputStream + +// Assuming context, formatUri, filename, and callback exist in the enclosing scope/class +class SDCardManager(private val context: Context) { + + private var contentResolver: ContentResolver? = null + + fun getFileInfo(filename: String, callback: CallbackContext, formatUri: (String) -> String) { + // Kotlin runnable execution using SAM conversion + Runnable { + val fileUri = formatUri(filename) + + try { + val file = getFile(fileUri) + + if (file != null) { + val result = JSONObject().apply { + put("exists", file.exists()) + put("canRead", file.canRead()) + put("canWrite", canWrite(file.uri)) + put("name", file.name) + put("length", file.length()) + put("type", file.type) + put("isFile", file.isFile) + put("isDirectory", file.isDirectory) + put("isVirtual", file.isVirtual) + put("lastModified", file.lastModified()) + put("url", file.uri.toString()) + } + callback.success(result) + } else { + callback.error("File not found") + } + } catch (e: Exception) { + callback.error(e.message) + } + }.run() + } + + private fun getUri(src: String, docId: String): Uri { + val srcUri = Uri.parse(src) + val srcId = DocumentsContract.getTreeDocumentId(srcUri) + val buildUri = DocumentsContract.buildDocumentUriUsingTree(srcUri, srcId) + return DocumentsContract.buildDocumentUriUsingTree(buildUri, docId) + } + + private fun exists(path: String, callback: CallbackContext) { + val file = DocumentFile.fromSingleUri(context, Uri.parse(path)) + + if (file == null) { + callback.error("Unable to get file") + } else { + if (file.exists()) { + callback.success("TRUE") + } else { + callback.success("FALSE") + } + } + } + + private fun error(err: String, callback: CallbackContext) { + callback.error("ERROR: $err") + } + + private fun getPath(uriString: String, src: String, callback: CallbackContext) { + try { + val file = getRelativeDocumentFile(uriString, src) + + if (file == null) { + callback.error("Unable to get file") + } else { + val uri = file.uri + val path = uri.path + + if (path != null) { + callback.success(uri.toString()) + } else { + callback.error("Unable to get path") + } + } + } catch (e: Exception) { + callback.error(e.message) + } + } + + private fun getRelativeDocumentFile(uri: String, filename: String): DocumentFile? { + var currentFileName = filename + val paths = mutableListOf() + + var file: DocumentFile? = DocumentFile.fromTreeUri(context, Uri.parse(uri)) + ?: return null + + if (!canWrite(file!!.uri)) { + throw RuntimeException("Cannot write file") + } + + paths.addAll(currentFileName.split("/")) + + while (paths.isNotEmpty()) { + val path = paths.removeAt(0) + currentFileName = TextUtils.join("/", paths) + + if (path.isNotEmpty()) { + file = file?.findFile(path) + if (file == null) return null + } + } + + return file + } + + private fun getFile(uri: Uri): DocumentFile? { + return getFile(uri.toString()) + } + + private fun getFile(filePath: String): DocumentFile? { + val fileUri = Uri.parse(filePath) + + return if (filePath.matches(Regex("file:///(.*)"))) { + val path = fileUri.path + if (path != null) DocumentFile.fromFile(File(path)) else null + } else { + DocumentFile.fromSingleUri(context, fileUri) + } + } + + private fun takePermission(uri: Uri) { + contentResolver = context.contentResolver + contentResolver?.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + + fun canWrite(uri: Uri): Boolean { + var canWrite = false + try { + // Open stream to verify write permissions + val os: OutputStream? = context.contentResolver.openOutputStream(uri, "wa") + if (os != null) { + os.close() + canWrite = true + } + } catch (ignored: SecurityException) { + } catch (ignored: IllegalArgumentException) { + } catch (ignored: IOException) { + } + return canWrite + } +} + +class MyFileObserver : FileObserver { + + private val listener: CallbackContext + + companion object { + private const val MASK = DELETE_SELF or MODIFY or MOVE_SELF + } + + constructor(path: String, listener: CallbackContext) : super(path, MASK) { + this.listener = listener + Log.d("MyFileObserver", "MyFileObserver: $path") + } + + @Suppress("DEPRECATION") + constructor(file: File, listener: CallbackContext) : super(file.absolutePath, MASK) { + this.listener = listener + Log.d("MyFileObserver", "MyFileObserver: ${file.absolutePath}") + } + + override fun onEvent(event: Int, path: String?) { + Log.d("MyFileObserver", "onEvent: $event") + val result = PluginResult(PluginResult.Status.OK).apply { + keepCallback = true + } + listener.sendPluginResult(result) + } + + fun startObserving() { + startWatching() + } + + fun stopObserving() { + stopWatching() + } +}