You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
@ -3,11 +3,12 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
classpath("com.android.tools.build:gradle:7.2.1")
|
||||||
|
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||||
|
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +51,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -6,7 +6,6 @@ import android.os.Build.VERSION_CODES;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.Promise;
|
import com.facebook.react.bridge.Promise;
|
||||||
@ -14,28 +13,19 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
import com.facebook.react.bridge.ReactMethod;
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
import com.facebook.react.bridge.WritableArray;
|
import com.facebook.react.bridge.WritableArray;
|
||||||
import com.facebook.react.bridge.WritableMap;
|
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
import com.reactnativesafx.utils.DocumentHelper;
|
import com.reactnativesafx.utils.EfficientDocumentHelper;
|
||||||
import com.reactnativesafx.utils.GeneralHelper;
|
|
||||||
import com.reactnativesafx.utils.UriHelper;
|
import com.reactnativesafx.utils.UriHelper;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.Q)
|
@RequiresApi(api = VERSION_CODES.Q)
|
||||||
@ReactModule(name = SafXModule.NAME)
|
@ReactModule(name = SafXModule.NAME)
|
||||||
public class SafXModule extends ReactContextBaseJavaModule {
|
public class SafXModule extends ReactContextBaseJavaModule {
|
||||||
public static final String NAME = "SafX";
|
public static final String NAME = "SafX";
|
||||||
public static Pattern trailingSlash = Pattern.compile("[/\\\\]$");
|
private final EfficientDocumentHelper efficientDocumentHelper;
|
||||||
private final DocumentHelper documentHelper;
|
|
||||||
|
|
||||||
public SafXModule(ReactApplicationContext reactContext) {
|
public SafXModule(ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
this.documentHelper = new DocumentHelper(reactContext);
|
this.efficientDocumentHelper = new EfficientDocumentHelper(reactContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -46,12 +36,12 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void openDocumentTree(final boolean persist, final Promise promise) {
|
public void openDocumentTree(final boolean persist, final Promise promise) {
|
||||||
this.documentHelper.openDocumentTree(persist, promise);
|
this.efficientDocumentHelper.openDocumentTree(persist, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void openDocument(final boolean persist, final boolean multiple, final Promise promise) {
|
public void openDocument(final boolean persist, final boolean multiple, final Promise promise) {
|
||||||
this.documentHelper.openDocument(persist, multiple, promise);
|
this.efficientDocumentHelper.openDocument(persist, multiple, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
@ -61,54 +51,22 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
final String initialName,
|
final String initialName,
|
||||||
final String mimeType,
|
final String mimeType,
|
||||||
final Promise promise) {
|
final Promise promise) {
|
||||||
this.documentHelper.createDocument(data, encoding, initialName, mimeType, promise);
|
this.efficientDocumentHelper.createDocument(data, encoding, initialName, mimeType, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void hasPermission(String uriString, final Promise promise) {
|
public void hasPermission(String uriString, final Promise promise) {
|
||||||
if (this.documentHelper.hasPermission(uriString)) {
|
this.efficientDocumentHelper.hasPermission(uriString, promise);
|
||||||
promise.resolve(true);
|
|
||||||
} else {
|
|
||||||
promise.resolve(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void exists(String uriString, final Promise promise) {
|
public void exists(String uriString, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.exists(uriString, promise);
|
||||||
promise.resolve(this.documentHelper.exists(uriString));
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void readFile(String uriString, String encoding, final Promise promise) {
|
public void readFile(String uriString, String encoding, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.readFile(uriString, encoding, promise);
|
||||||
DocumentFile file;
|
|
||||||
|
|
||||||
try {
|
|
||||||
file = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
promise.reject("ENOENT", "'" + uriString + "' does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (encoding != null) {
|
|
||||||
if (encoding.equals("ascii")) {
|
|
||||||
WritableArray arr =
|
|
||||||
(WritableArray) this.documentHelper.readFromUri(file.getUri(), encoding);
|
|
||||||
promise.resolve((arr));
|
|
||||||
} else {
|
|
||||||
promise.resolve(this.documentHelper.readFromUri(file.getUri(), encoding));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(this.documentHelper.readFromUri(file.getUri(), "utf8"));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
@ -119,102 +77,35 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
String mimeType,
|
String mimeType,
|
||||||
boolean append,
|
boolean append,
|
||||||
final Promise promise) {
|
final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.writeFile(
|
||||||
DocumentFile file;
|
uriString, data, encoding, mimeType, append, promise
|
||||||
|
);
|
||||||
try {
|
|
||||||
file = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
file = this.documentHelper.createFile(uriString, mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes = GeneralHelper.stringToBytes(data, encoding);
|
|
||||||
|
|
||||||
try (OutputStream fout =
|
|
||||||
this.getReactApplicationContext()
|
|
||||||
.getContentResolver()
|
|
||||||
.openOutputStream(file.getUri(), append ? "wa" : "wt")) {
|
|
||||||
fout.write(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.resolve(uriString);
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void transferFile(
|
public void transferFile(
|
||||||
String srcUri, String destUri, boolean replaceIfDestExists, boolean copy, Promise promise) {
|
String srcUri, String destUri, boolean replaceIfDestExists, boolean copy, Promise promise) {
|
||||||
this.documentHelper.transferFile(srcUri, destUri, replaceIfDestExists, copy, promise);
|
this.efficientDocumentHelper.transferFile(srcUri, destUri, replaceIfDestExists, copy, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void rename(String uriString, String newName, final Promise promise) {
|
public void rename(String uriString, String newName, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.renameTo(uriString, newName, promise);
|
||||||
|
|
||||||
DocumentFile doc;
|
|
||||||
try {
|
|
||||||
doc = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
promise.reject("ENOENT", "'" + uriString + "' does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc.renameTo(newName)) {
|
|
||||||
promise.resolve(true);
|
|
||||||
} else {
|
|
||||||
promise.resolve(false);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void unlink(String uriString, final Promise promise) {
|
public void unlink(String uriString, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.unlink(uriString, promise);
|
||||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
boolean result = doc.delete();
|
|
||||||
if (!result) {
|
|
||||||
throw new Exception("Failed to unlink file. Unknown error.");
|
|
||||||
}
|
|
||||||
promise.resolve(true);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
promise.resolve(true);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void mkdir(String uriString, final Promise promise) {
|
public void mkdir(String uriString, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.mkdir(uriString, promise);
|
||||||
DocumentFile dir = this.documentHelper.mkdir(uriString);
|
|
||||||
DocumentHelper.resolveWithDocument(dir, uriString, promise);
|
|
||||||
} catch (IOException e) {
|
|
||||||
promise.reject("EEXIST", e.getLocalizedMessage());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void createFile(String uriString, String mimeType, final Promise promise) {
|
public void createFile(String uriString, String mimeType, final Promise promise) {
|
||||||
try {
|
this.efficientDocumentHelper.createFile(uriString, mimeType, promise);
|
||||||
DocumentFile createdFile = this.documentHelper.createFile(uriString, mimeType);
|
|
||||||
DocumentHelper.resolveWithDocument(createdFile, uriString, promise);
|
|
||||||
} catch (IOException e) {
|
|
||||||
promise.reject("EEXIST", e.getLocalizedMessage());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
@ -241,43 +132,11 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void listFiles(String uriString, final Promise promise) {
|
public void listFiles(String uriString, final Promise promise) {
|
||||||
try {
|
efficientDocumentHelper.listFiles(uriString, promise);
|
||||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
|
|
||||||
WritableMap[] resolvedDocs =
|
|
||||||
Arrays.stream(doc.listFiles())
|
|
||||||
.map(
|
|
||||||
docEntry ->
|
|
||||||
DocumentHelper.resolveWithDocument(
|
|
||||||
docEntry,
|
|
||||||
trailingSlash.matcher(uriString).replaceFirst("")
|
|
||||||
+ "/"
|
|
||||||
+ docEntry.getName(),
|
|
||||||
null))
|
|
||||||
.toArray(WritableMap[]::new);
|
|
||||||
WritableArray resolveData = Arguments.fromJavaArgs(resolvedDocs);
|
|
||||||
promise.resolve(resolveData);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void stat(String uriString, final Promise promise) {
|
public void stat(String uriString, final Promise promise) {
|
||||||
try {
|
efficientDocumentHelper.stat(uriString, promise);
|
||||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
|
||||||
|
|
||||||
DocumentHelper.resolveWithDocument(doc, uriString, promise);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.reactnativesafx.utils;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
// from https://stackoverflow.com/a/73666782/3542461
|
||||||
|
public class Async {
|
||||||
|
|
||||||
|
private static final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
|
private static final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
public static <T> void execute(Task<T> task) {
|
||||||
|
executorService.execute(() -> {
|
||||||
|
T t = task.doAsync();
|
||||||
|
handler.post(() -> {
|
||||||
|
task.doSync(t);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Task<T> {
|
||||||
|
T doAsync();
|
||||||
|
|
||||||
|
void doSync(T t);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,585 +0,0 @@
|
|||||||
package com.reactnativesafx.utils;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.UriPermission;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.util.Base64;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.documentfile.provider.DocumentFileHelper;
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.ActivityEventListener;
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
|
||||||
import com.facebook.react.bridge.Promise;
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
|
||||||
import com.facebook.react.bridge.WritableArray;
|
|
||||||
import com.facebook.react.bridge.WritableMap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.Q)
|
|
||||||
public class DocumentHelper {
|
|
||||||
|
|
||||||
private static final int DOCUMENT_TREE_REQUEST_CODE = 1;
|
|
||||||
private static final int DOCUMENT_REQUEST_CODE = 2;
|
|
||||||
private static final int DOCUMENT_CREATE_CODE = 3;
|
|
||||||
|
|
||||||
private final ReactApplicationContext context;
|
|
||||||
private ActivityEventListener activityEventListener;
|
|
||||||
|
|
||||||
public DocumentHelper(ReactApplicationContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openDocumentTree(final boolean persist, final Promise promise) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setAction(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
|
||||||
|
|
||||||
if (activityEventListener != null) {
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
activityEventListener =
|
|
||||||
new ActivityEventListener() {
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(
|
|
||||||
Activity activity, int requestCode, int resultCode, Intent intent) {
|
|
||||||
if (requestCode == DOCUMENT_TREE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
|
||||||
if (intent != null) {
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
if (persist) {
|
|
||||||
final int takeFlags =
|
|
||||||
intent.getFlags()
|
|
||||||
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
||||||
|
|
||||||
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
|
||||||
resolveWithDocument(doc, uri.toString(), promise);
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(null);
|
|
||||||
}
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewIntent(Intent intent) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
context.addActivityEventListener(activityEventListener);
|
|
||||||
|
|
||||||
Activity activity = context.getCurrentActivity();
|
|
||||||
if (activity != null) {
|
|
||||||
activity.startActivityForResult(intent, DOCUMENT_TREE_REQUEST_CODE);
|
|
||||||
} else {
|
|
||||||
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openDocument(final boolean persist, final boolean multiple, final Promise promise) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
||||||
if (multiple) {
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
|
||||||
}
|
|
||||||
intent.setType("*/*");
|
|
||||||
|
|
||||||
if (activityEventListener != null) {
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
activityEventListener =
|
|
||||||
new ActivityEventListener() {
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(
|
|
||||||
Activity activity, int requestCode, int resultCode, Intent intent) {
|
|
||||||
try {
|
|
||||||
WritableArray resolvedDocs = Arguments.createArray();
|
|
||||||
if (requestCode == DOCUMENT_REQUEST_CODE
|
|
||||||
&& resultCode == Activity.RESULT_OK
|
|
||||||
&& intent != null) {
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
|
|
||||||
if (uri != null) {
|
|
||||||
if (persist) {
|
|
||||||
final int takeFlags =
|
|
||||||
intent.getFlags()
|
|
||||||
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
||||||
|
|
||||||
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
|
||||||
WritableMap docInfo = resolveWithDocument(doc, uri.toString(), null);
|
|
||||||
resolvedDocs.pushMap(docInfo);
|
|
||||||
} else if (multiple) {
|
|
||||||
ClipData clipData = intent.getClipData();
|
|
||||||
if (clipData != null) {
|
|
||||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
|
||||||
ClipData.Item item = clipData.getItemAt(i);
|
|
||||||
Uri clipUri = item.getUri();
|
|
||||||
DocumentFile doc = goToDocument(clipUri.toString(), false);
|
|
||||||
WritableMap docInfo = resolveWithDocument(doc, clipUri.toString(), null);
|
|
||||||
resolvedDocs.pushMap(docInfo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Exception("Unexpected Error: ClipData was null");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Exception(
|
|
||||||
"Unexpected Error: Could not retrieve information about selected documents");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promise.resolve(resolvedDocs);
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
} finally {
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewIntent(Intent intent) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
context.addActivityEventListener(activityEventListener);
|
|
||||||
|
|
||||||
Activity activity = context.getCurrentActivity();
|
|
||||||
if (activity != null) {
|
|
||||||
activity.startActivityForResult(intent, DOCUMENT_REQUEST_CODE);
|
|
||||||
} else {
|
|
||||||
promise.reject(
|
|
||||||
"EUNSPECIFIED", "Cannot get current activity, so cannot launch document picker");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createDocument(
|
|
||||||
final String data,
|
|
||||||
final String encoding,
|
|
||||||
final String initialName,
|
|
||||||
final String mimeType,
|
|
||||||
final Promise promise) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setAction(Intent.ACTION_CREATE_DOCUMENT);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
||||||
if (initialName != null) {
|
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, initialName);
|
|
||||||
}
|
|
||||||
if (mimeType != null) {
|
|
||||||
intent.setType(mimeType);
|
|
||||||
} else {
|
|
||||||
intent.setType("*/*");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activityEventListener != null) {
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
activityEventListener =
|
|
||||||
new ActivityEventListener() {
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(
|
|
||||||
Activity activity, int requestCode, int resultCode, Intent intent) {
|
|
||||||
|
|
||||||
if (requestCode == DOCUMENT_CREATE_CODE && resultCode == Activity.RESULT_OK) {
|
|
||||||
if (intent != null) {
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
|
|
||||||
DocumentFile doc = DocumentFile.fromSingleUri(context, uri);
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] bytes = GeneralHelper.stringToBytes(data, encoding);
|
|
||||||
try (OutputStream os = context.getContentResolver().openOutputStream(uri)) {
|
|
||||||
os.write(bytes);
|
|
||||||
}
|
|
||||||
assert doc != null;
|
|
||||||
resolveWithDocument(doc, uri.toString(), promise);
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewIntent(Intent intent) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
context.addActivityEventListener(activityEventListener);
|
|
||||||
|
|
||||||
Activity activity = context.getCurrentActivity();
|
|
||||||
if (activity != null) {
|
|
||||||
activity.startActivityForResult(intent, DOCUMENT_CREATE_CODE);
|
|
||||||
} else {
|
|
||||||
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
|
||||||
@SuppressWarnings({"UnusedDeclaration", "UnusedAssignment"})
|
|
||||||
public Object readFromUri(Uri uri, String encoding) throws IOException {
|
|
||||||
byte[] bytes;
|
|
||||||
int bytesRead;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
|
||||||
|
|
||||||
length = inputStream.available();
|
|
||||||
bytes = new byte[length];
|
|
||||||
bytesRead = inputStream.read(bytes);
|
|
||||||
inputStream.close();
|
|
||||||
|
|
||||||
switch (encoding.toLowerCase()) {
|
|
||||||
case "base64":
|
|
||||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
||||||
case "ascii":
|
|
||||||
WritableArray asciiResult = Arguments.createArray();
|
|
||||||
for (byte b : bytes) {
|
|
||||||
asciiResult.pushInt(b);
|
|
||||||
}
|
|
||||||
return asciiResult;
|
|
||||||
case "utf8":
|
|
||||||
default:
|
|
||||||
return new String(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(final String uriString) throws SecurityException {
|
|
||||||
return this.exists(uriString, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean exists(final String uriString, final boolean shouldBeFile)
|
|
||||||
throws SecurityException {
|
|
||||||
try {
|
|
||||||
DocumentFile fileOrFolder = goToDocument(uriString, false);
|
|
||||||
if (shouldBeFile) {
|
|
||||||
return !fileOrFolder.isDirectory();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasPermission(String uriString) {
|
|
||||||
// list of all persisted permissions for our app
|
|
||||||
List<UriPermission> uriList = context.getContentResolver().getPersistedUriPermissions();
|
|
||||||
for (UriPermission uriPermission : uriList) {
|
|
||||||
if (permissionMatchesAndHasAccess(uriPermission, UriHelper.normalize(uriString))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean permissionMatchesAndHasAccess(
|
|
||||||
UriPermission permission, String normalizedUriString) {
|
|
||||||
String permittedUri = permission.getUri().toString();
|
|
||||||
return (permittedUri.startsWith(normalizedUriString)
|
|
||||||
|| normalizedUriString.startsWith(permittedUri))
|
|
||||||
&& permission.isReadPermission()
|
|
||||||
&& permission.isWritePermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WritableMap resolveWithDocument(
|
|
||||||
@NonNull DocumentFile file, String SimplifiedUri, Promise promise) {
|
|
||||||
WritableMap fileMap = Arguments.createMap();
|
|
||||||
fileMap.putString("uri", UriHelper.denormalize(SimplifiedUri));
|
|
||||||
fileMap.putString("name", file.getName());
|
|
||||||
fileMap.putString("type", file.isDirectory() ? "directory" : "file");
|
|
||||||
if (file.isFile()) {
|
|
||||||
fileMap.putString("mime", file.getType());
|
|
||||||
fileMap.putDouble("size", file.length());
|
|
||||||
}
|
|
||||||
fileMap.putDouble("lastModified", file.lastModified());
|
|
||||||
|
|
||||||
if (promise != null) {
|
|
||||||
promise.resolve(fileMap);
|
|
||||||
}
|
|
||||||
return fileMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentFile mkdir(String uriString)
|
|
||||||
throws IOException, SecurityException, IllegalArgumentException {
|
|
||||||
return this.mkdir(uriString, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a DocumentFile that is created using DocumentFile.fromTreeUri()
|
|
||||||
*/
|
|
||||||
public DocumentFile mkdir(String uriString, boolean includeLastSegment)
|
|
||||||
throws IOException, SecurityException, IllegalArgumentException {
|
|
||||||
DocumentFile dir = goToDocument(uriString, true, includeLastSegment);
|
|
||||||
assert dir != null;
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentFile createFile(String uriString) throws IOException, SecurityException {
|
|
||||||
return createFile(uriString, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentFile createFile(String uriString, String mimeType)
|
|
||||||
throws IOException, SecurityException {
|
|
||||||
if (this.exists(uriString)) {
|
|
||||||
throw new IOException("a file or directory already exist at: " + uriString);
|
|
||||||
}
|
|
||||||
DocumentFile parentDirOfFile = this.mkdir(uriString, false);
|
|
||||||
// it should be safe because user cannot select sd root or primary root
|
|
||||||
// and any other path would have at least one '/' to provide a file name in a folder
|
|
||||||
String fileName = UriHelper.getLastSegment(uriString);
|
|
||||||
if (fileName.indexOf(':') != -1) {
|
|
||||||
throw new IOException(
|
|
||||||
"Invalid file name: Could not extract filename from uri string provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe edited maybe not
|
|
||||||
String correctFileName = fileName;
|
|
||||||
|
|
||||||
// only files with mime type are special, so we treat it special
|
|
||||||
if (mimeType != null && !mimeType.equals("")) {
|
|
||||||
int indexOfDot = fileName.indexOf('.');
|
|
||||||
// len - 1 because there should be an extension that has at least 1 letter
|
|
||||||
if (indexOfDot != -1 && indexOfDot < fileName.length() - 1) {
|
|
||||||
correctFileName = fileName.substring(0, indexOfDot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentFile createdFile =
|
|
||||||
parentDirOfFile.createFile(
|
|
||||||
mimeType != null && !mimeType.equals("") ? mimeType : "*/*", correctFileName);
|
|
||||||
if (createdFile == null) {
|
|
||||||
throw new IOException(
|
|
||||||
"File creation failed without any specific error for '" + fileName + "'");
|
|
||||||
}
|
|
||||||
// some times setting mimetypes causes name changes, this is to prevent that.
|
|
||||||
if (!createdFile.getName().equals(fileName)) {
|
|
||||||
if (!createdFile.renameTo(fileName) || !createdFile.getName().equals(fileName)) {
|
|
||||||
createdFile.delete();
|
|
||||||
throw new IOException(
|
|
||||||
"The created file name was not as expected: '"
|
|
||||||
+ uriString
|
|
||||||
+ "'"
|
|
||||||
+ "but got: "
|
|
||||||
+ createdFile.getUri());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return createdFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentFile goToDocument(String uriString, boolean createIfDirectoryNotExist)
|
|
||||||
throws SecurityException, IOException {
|
|
||||||
return goToDocument(uriString, createIfDirectoryNotExist, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentFile goToDocument(
|
|
||||||
String unknownUriStr, boolean createIfDirectoryNotExist, boolean includeLastSegment)
|
|
||||||
throws SecurityException, IOException, IllegalArgumentException {
|
|
||||||
String unknownUriString = UriHelper.getUnifiedUri(unknownUriStr);
|
|
||||||
if (unknownUriString.startsWith(ContentResolver.SCHEME_FILE)) {
|
|
||||||
Uri uri = Uri.parse(unknownUriString);
|
|
||||||
if (uri == null) {
|
|
||||||
throw new IllegalArgumentException("Invalid Uri String");
|
|
||||||
}
|
|
||||||
String path =
|
|
||||||
uri.getPath()
|
|
||||||
.substring(
|
|
||||||
0,
|
|
||||||
includeLastSegment
|
|
||||||
? uri.getPath().length()
|
|
||||||
: uri.getPath().length() - uri.getLastPathSegment().length());
|
|
||||||
|
|
||||||
if (createIfDirectoryNotExist) {
|
|
||||||
File targetFile = new File(path);
|
|
||||||
if (!targetFile.exists()) {
|
|
||||||
boolean madeFolder = targetFile.mkdirs();
|
|
||||||
if (!madeFolder) {
|
|
||||||
throw new IOException("mkdir failed for Uri with `file` scheme");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DocumentFile targetFile = DocumentFile.fromFile(new File(path));
|
|
||||||
if (!targetFile.exists()) {
|
|
||||||
throw new FileNotFoundException(
|
|
||||||
"Cannot find the given document. File does not exist at '" + unknownUriString + "'");
|
|
||||||
}
|
|
||||||
return targetFile;
|
|
||||||
} else if (!UriHelper.isContentDocumentTreeUri(unknownUriString)) {
|
|
||||||
// It's a document picked by user
|
|
||||||
DocumentFile doc = DocumentFile.fromSingleUri(context, Uri.parse(unknownUriString));
|
|
||||||
if (doc != null) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
throw new FileNotFoundException(
|
|
||||||
"Cannot find the given document. File does not exist at '" + unknownUriString + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
String uriString = UriHelper.normalize(unknownUriString);
|
|
||||||
String baseUri = "";
|
|
||||||
String appendUri;
|
|
||||||
String[] strings = new String[0];
|
|
||||||
|
|
||||||
{
|
|
||||||
// Helps traversal and folder creation by knowing where to start traverse
|
|
||||||
List<UriPermission> uriList = context.getContentResolver().getPersistedUriPermissions();
|
|
||||||
for (UriPermission uriPermission : uriList) {
|
|
||||||
String uriPath = uriPermission.getUri().toString();
|
|
||||||
if (this.permissionMatchesAndHasAccess(uriPermission, uriString)) {
|
|
||||||
baseUri = uriPath;
|
|
||||||
appendUri = Uri.decode(uriString.substring(uriPath.length()));
|
|
||||||
strings = appendUri.split("/");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseUri.equals("")) {
|
|
||||||
// It's possible that the file access is temporary
|
|
||||||
baseUri = uriString;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = Uri.parse(baseUri);
|
|
||||||
DocumentFile dir = DocumentFile.fromTreeUri(context, uri);
|
|
||||||
|
|
||||||
int pathSegmentsToTraverseLength = includeLastSegment ? strings.length : strings.length - 1;
|
|
||||||
for (int i = 0; i < pathSegmentsToTraverseLength; i++) {
|
|
||||||
if (!strings[i].equals("")) {
|
|
||||||
assert dir != null;
|
|
||||||
DocumentFile childDoc = DocumentFileHelper.findFile(context, dir, strings[i]);
|
|
||||||
if (childDoc != null) {
|
|
||||||
if (childDoc.isDirectory()) {
|
|
||||||
dir = childDoc;
|
|
||||||
} else if (i == pathSegmentsToTraverseLength - 1) {
|
|
||||||
// we are at the last part to traverse, its our destination, doesn't matter if its a
|
|
||||||
// file or directory
|
|
||||||
dir = childDoc;
|
|
||||||
} else {
|
|
||||||
// child doc is a file
|
|
||||||
throw new IOException(
|
|
||||||
"There's a document with the same name as the one we are trying to traverse at: '"
|
|
||||||
+ childDoc.getUri()
|
|
||||||
+ "'");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (createIfDirectoryNotExist) {
|
|
||||||
dir = dir.createDirectory(strings[i]);
|
|
||||||
} else {
|
|
||||||
throw new FileNotFoundException(
|
|
||||||
"Cannot traverse to the pointed document. Directory '"
|
|
||||||
+ strings[i]
|
|
||||||
+ "'"
|
|
||||||
+ " does not exist in '"
|
|
||||||
+ dir.getUri()
|
|
||||||
+ "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert dir != null;
|
|
||||||
|
|
||||||
if (!dir.canRead() || !dir.canWrite()) {
|
|
||||||
throw new SecurityException(
|
|
||||||
"You don't have read/write permission to access uri: " + uriString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void transferFile(
|
|
||||||
String srcUri, String destUri, boolean replaceIfDestExists, boolean copy, Promise promise) {
|
|
||||||
try {
|
|
||||||
DocumentFile srcDoc = this.goToDocument(srcUri, false, true);
|
|
||||||
|
|
||||||
if (srcDoc.isDirectory()) {
|
|
||||||
throw new IllegalArgumentException("Cannot move directories");
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentFile destDoc;
|
|
||||||
try {
|
|
||||||
destDoc = this.goToDocument(destUri, false, true);
|
|
||||||
if (!replaceIfDestExists) {
|
|
||||||
throw new IOException("a document with the same name already exists in destination");
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
destDoc = this.createFile(destUri, srcDoc.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream inStream =
|
|
||||||
this.context.getContentResolver().openInputStream(srcDoc.getUri());
|
|
||||||
OutputStream outStream =
|
|
||||||
this.context.getContentResolver().openOutputStream(destDoc.getUri(), "wt"); ) {
|
|
||||||
byte[] buffer = new byte[1024 * 4];
|
|
||||||
int length;
|
|
||||||
while ((length = inStream.read(buffer)) > 0) {
|
|
||||||
outStream.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!copy) {
|
|
||||||
srcDoc.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.resolve(resolveWithDocument(destDoc, destUri, promise));
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,132 @@
|
|||||||
|
package com.reactnativesafx.utils;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
|
public class DocumentStat {
|
||||||
|
private final Uri uri;
|
||||||
|
private final Uri internalUri;
|
||||||
|
private final String displayName;
|
||||||
|
private final String mimeType;
|
||||||
|
private final Boolean isDirectory;
|
||||||
|
private final long size;
|
||||||
|
private final long lastModified;
|
||||||
|
private static final String encodedSlash = Uri.encode("/");
|
||||||
|
private static final String PATH_DOCUMENT = "document";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor columns must be in the following format:
|
||||||
|
* 0 - DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||||
|
* 1 - DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||||
|
* 2 - DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
* 3 - DocumentsContract.Document.COLUMN_SIZE
|
||||||
|
* 4 - DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||||
|
*
|
||||||
|
* @param uri if this is a tree uri, it must be in it's simplified form
|
||||||
|
* (before being processed for the library)
|
||||||
|
*/
|
||||||
|
public DocumentStat(Cursor c, final Uri uri) {
|
||||||
|
if (DocumentsContract.isTreeUri(uri)) {
|
||||||
|
final Uri tree = DocumentsContract.buildTreeDocumentUri(uri.getAuthority(), DocumentsContract.getTreeDocumentId(uri));
|
||||||
|
this.internalUri = DocumentsContract.buildDocumentUriUsingTree(tree, c.getString(0));
|
||||||
|
|
||||||
|
if (uri.toString().contains("document/raw") || !c.getString(0).contains(DocumentsContract.getTreeDocumentId(uri))) {
|
||||||
|
this.uri = this.internalUri;
|
||||||
|
} else {
|
||||||
|
this.uri = DocumentsContract.buildTreeDocumentUri(uri.getAuthority(), c.getString(0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.uri = uri;
|
||||||
|
this.internalUri = this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.displayName = c.getString(1);
|
||||||
|
this.mimeType = c.getString(2);
|
||||||
|
this.size = c.getLong(3);
|
||||||
|
this.lastModified = c.getLong(4);
|
||||||
|
this.isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(this.mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DocumentStat(final File file) {
|
||||||
|
this.uri = Uri.fromFile(file);
|
||||||
|
this.internalUri = this.uri;
|
||||||
|
this.displayName = file.getName();
|
||||||
|
this.lastModified = file.lastModified();
|
||||||
|
this.size = file.length();
|
||||||
|
this.isDirectory = file.isDirectory();
|
||||||
|
if (this.isDirectory) {
|
||||||
|
this.mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
|
||||||
|
} else {
|
||||||
|
this.mimeType = getTypeForName(file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WritableMap getWritableMap() {
|
||||||
|
WritableMap fileMap = Arguments.createMap();
|
||||||
|
fileMap.putString("uri", UriHelper.denormalize(uri));
|
||||||
|
fileMap.putString("name", displayName);
|
||||||
|
if (isDirectory) {
|
||||||
|
fileMap.putString("type", "directory");
|
||||||
|
} else {
|
||||||
|
fileMap.putString("type", "file");
|
||||||
|
}
|
||||||
|
fileMap.putString("mime", mimeType);
|
||||||
|
fileMap.putDouble("size", size);
|
||||||
|
fileMap.putDouble("lastModified", lastModified);
|
||||||
|
return fileMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUri() {
|
||||||
|
return UriHelper.denormalize(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isDirectory() {
|
||||||
|
return this.isDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastModified() {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTypeForName(String name) {
|
||||||
|
final int lastDot = name.lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
final String extension = name.substring(lastDot + 1).toLowerCase();
|
||||||
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
if (mime != null) {
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Uri getInternalUri() {
|
||||||
|
return internalUri;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
package com.reactnativesafx.utils;
|
package com.reactnativesafx.utils;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class GeneralHelper {
|
public class GeneralHelper {
|
||||||
|
@ -2,61 +2,92 @@ package com.reactnativesafx.utils;
|
|||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
import com.reactnativesafx.utils.exceptions.IllegalArgumentExceptionFast;
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.Q)
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
public class UriHelper {
|
public class UriHelper {
|
||||||
public static final String CONTENT_URI_PREFIX = "content://";
|
private static final String PATH_TREE = "tree";
|
||||||
public static final Pattern DOCUMENT_TREE_PREFIX =
|
private static final String PATH_DOCUMENT = "document";
|
||||||
Pattern.compile("^content://.*?/tree/.+?", Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
public static String getLastSegment(String uriString) {
|
public static String getFileName(String uriStr) {
|
||||||
|
// it should be safe because user cannot select sd root or primary root
|
||||||
return Uri.parse(Uri.decode(uriString)).getLastPathSegment();
|
// and any other path would have at least one '/' to provide a file name in a folder
|
||||||
|
String fileName = Uri.parse(Uri.decode(uriStr)).getLastPathSegment();
|
||||||
|
if (fileName.indexOf(':') != -1) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Invalid file name: Could not extract filename from uri string provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isContentDocumentTreeUri(String uriString) {
|
return fileName;
|
||||||
return DOCUMENT_TREE_PREFIX.matcher(uriString).matches();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String normalize(String uriString) {
|
public static String normalize(String uriString) {
|
||||||
if (isContentDocumentTreeUri(uriString)) {
|
return normalize(Uri.parse(uriString));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalize(Uri uri) {
|
||||||
|
if (DocumentsContract.isTreeUri(uri)) {
|
||||||
// an abnormal uri example:
|
// an abnormal uri example:
|
||||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||||
// normalized:
|
// normalized:
|
||||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||||
|
|
||||||
|
if (uri.getPath().indexOf(":") != -1) {
|
||||||
// uri parts:
|
// uri parts:
|
||||||
String[] parts = Uri.decode(uriString).split(":");
|
String[] parts = Uri.decode(uri.toString()).split(":");
|
||||||
return parts[0] + ":" + parts[1] + Uri.encode(":" + parts[2]);
|
return parts[0] + ":" + parts[1] + Uri.encode(":" + parts[2]);
|
||||||
}
|
}
|
||||||
return uriString;
|
}
|
||||||
|
return uri.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String denormalize(String uriString) {
|
public static String denormalize(Uri uri) {
|
||||||
if (isContentDocumentTreeUri(uriString)) {
|
if (DocumentsContract.isTreeUri(uri)) {
|
||||||
// an normalized uri example:
|
// an normalized uri example:
|
||||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||||
// denormalized:
|
// denormalized:
|
||||||
// content://com.android.externalstorage.documents/tree/1707-3F0B/Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
// content://com.android.externalstorage.documents/tree/1707-3F0B/Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||||
|
|
||||||
return Uri.decode(normalize(uriString));
|
return Uri.decode(normalize(uri));
|
||||||
}
|
|
||||||
return uriString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUnifiedUri(String uriString) throws IllegalArgumentException {
|
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
if (uri.getScheme() == null) {
|
|
||||||
uri = Uri.parse(ContentResolver.SCHEME_FILE + "://" + uriString);
|
|
||||||
} else if (!(uri.getScheme().equals(ContentResolver.SCHEME_FILE)
|
|
||||||
|| uri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
|
|
||||||
throw new IllegalArgumentException("Invalid Uri: Scheme not supported");
|
|
||||||
}
|
}
|
||||||
return uri.toString();
|
return uri.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Uri getUnifiedUri(String unknownUriStr) {
|
||||||
|
if (unknownUriStr == null || unknownUriStr.equals("")) {
|
||||||
|
throw new IllegalArgumentExceptionFast("Invalid Uri: No input was given");
|
||||||
|
}
|
||||||
|
Uri uri = Uri.parse(unknownUriStr);
|
||||||
|
if (uri.getScheme() == null) {
|
||||||
|
uri = Uri.parse(ContentResolver.SCHEME_FILE + "://" + unknownUriStr);
|
||||||
|
} else if (!(uri.getScheme().equals(ContentResolver.SCHEME_FILE)
|
||||||
|
|| uri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
|
||||||
|
throw new IllegalArgumentExceptionFast("Invalid Uri: Scheme not supported");
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isContentUri(Uri uri) {
|
||||||
|
return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDocumentUri(Uri uri) {
|
||||||
|
if (isContentUri(uri)) {
|
||||||
|
final List<String> paths = uri.getPathSegments();
|
||||||
|
if (paths.size() >= 4) {
|
||||||
|
return PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2));
|
||||||
|
} else if (paths.size() >= 2) {
|
||||||
|
return PATH_DOCUMENT.equals(paths.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
public class ExceptionFast extends Exception {
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionFast(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
|
public class FileNotFoundExceptionFast extends FileNotFoundException {
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileNotFoundExceptionFast(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileNotFoundExceptionFast() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class IOExceptionFast extends IOException {
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IOExceptionFast(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
public class IllegalArgumentExceptionFast extends IllegalArgumentException {
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IllegalArgumentExceptionFast(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class RenameFailedException extends IOException {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Uri inputUri;
|
||||||
|
private final String inputName;
|
||||||
|
|
||||||
|
private final Uri resultUri;
|
||||||
|
private final String resultName;
|
||||||
|
|
||||||
|
|
||||||
|
public RenameFailedException(final Uri inputUri, final String inputName, final Uri resultUri, final String resultName) {
|
||||||
|
super("Failed to rename file at: " + inputUri + " expected: '"
|
||||||
|
+ inputName
|
||||||
|
+ "'"
|
||||||
|
+ "but got: "
|
||||||
|
+ resultName);
|
||||||
|
this.inputUri = inputUri;
|
||||||
|
this.inputName = inputName;
|
||||||
|
this.resultUri = resultUri;
|
||||||
|
this.resultName = resultName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getInputUri() {
|
||||||
|
return inputUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInputName() {
|
||||||
|
return inputName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getResultUri() {
|
||||||
|
return resultUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResultName() {
|
||||||
|
return resultName;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.reactnativesafx.utils.exceptions;
|
||||||
|
|
||||||
|
public class SecurityExceptionFast extends SecurityException {
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecurityExceptionFast(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"main": "src/index",
|
"main": "src/index",
|
||||||
"react-native": "src/index",
|
"react-native": "src/index",
|
||||||
"source": "src/index",
|
"source": "src/index",
|
||||||
"private": true,
|
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
"lib",
|
"lib",
|
||||||
@ -15,7 +14,10 @@
|
|||||||
"react-native-saf-x.podspec",
|
"react-native-saf-x.podspec",
|
||||||
"!lib/typescript/example",
|
"!lib/typescript/example",
|
||||||
"!android/build",
|
"!android/build",
|
||||||
"!ios/build"
|
"!ios/build",
|
||||||
|
"!**/__tests__",
|
||||||
|
"!**/__fixtures__",
|
||||||
|
"!**/__mocks__"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --project tsconfig.json",
|
"build": "tsc --project tsconfig.json",
|
||||||
@ -30,8 +32,10 @@
|
|||||||
"scoped",
|
"scoped",
|
||||||
"storage",
|
"storage",
|
||||||
"SAF",
|
"SAF",
|
||||||
"storage-access-framework"
|
"storage-access-framework",
|
||||||
|
"fs"
|
||||||
],
|
],
|
||||||
|
"repository": "https://github.com/jd1378/react-native-saf-x",
|
||||||
"author": "Javad Mnjd (https://github.com/jd1378)",
|
"author": "Javad Mnjd (https://github.com/jd1378)",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/react-native-saf-x",
|
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/react-native-saf-x",
|
||||||
@ -40,10 +44,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.16.0",
|
"@babel/core": "7.16.0",
|
||||||
"@types/react": "16.14.21",
|
|
||||||
"@types/react-native": "0.64.19",
|
"@types/react-native": "0.64.19",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.66.1",
|
"react-native": "0.70.6",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -50,11 +50,11 @@ interface SafxInterface {
|
|||||||
encoding?: Encoding,
|
encoding?: Encoding,
|
||||||
mimeType?: string,
|
mimeType?: string,
|
||||||
append?: boolean,
|
append?: boolean,
|
||||||
): Promise<string>;
|
): Promise<void>;
|
||||||
createFile(uriString: string, mimeType?: String): Promise<DocumentFileDetail>;
|
createFile(uriString: string, mimeType?: String): Promise<DocumentFileDetail>;
|
||||||
unlink(uriString: string): Promise<boolean>;
|
unlink(uriString: string): Promise<boolean>;
|
||||||
mkdir(uriString: string): Promise<DocumentFileDetail>;
|
mkdir(uriString: string): Promise<DocumentFileDetail>;
|
||||||
rename(uriString: string, newName: string): Promise<boolean>;
|
rename(uriString: string, newName: string): Promise<DocumentFileDetail>;
|
||||||
getPersistedUriPermissions(): Promise<Array<string>>;
|
getPersistedUriPermissions(): Promise<Array<string>>;
|
||||||
releasePersistableUriPermission(uriString: string): Promise<void>;
|
releasePersistableUriPermission(uriString: string): Promise<void>;
|
||||||
listFiles(uriString: string): Promise<DocumentFileDetail[]>;
|
listFiles(uriString: string): Promise<DocumentFileDetail[]>;
|
||||||
@ -72,8 +72,8 @@ export type DocumentFileDetail = {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'directory' | 'file';
|
type: 'directory' | 'file';
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
mime?: string;
|
mime: string;
|
||||||
size?: number;
|
size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileOperationOptions = {
|
export type FileOperationOptions = {
|
||||||
|
Reference in New Issue
Block a user