mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
parent
2f254d81cd
commit
b82bf16505
@ -3,11 +3,12 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
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()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -6,7 +6,6 @@ import android.os.Build.VERSION_CODES;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
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.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.reactnativesafx.utils.DocumentHelper;
|
||||
import com.reactnativesafx.utils.GeneralHelper;
|
||||
import com.reactnativesafx.utils.EfficientDocumentHelper;
|
||||
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)
|
||||
@ReactModule(name = SafXModule.NAME)
|
||||
public class SafXModule extends ReactContextBaseJavaModule {
|
||||
public static final String NAME = "SafX";
|
||||
public static Pattern trailingSlash = Pattern.compile("[/\\\\]$");
|
||||
private final DocumentHelper documentHelper;
|
||||
private final EfficientDocumentHelper efficientDocumentHelper;
|
||||
|
||||
public SafXModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.documentHelper = new DocumentHelper(reactContext);
|
||||
this.efficientDocumentHelper = new EfficientDocumentHelper(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -46,183 +36,84 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void openDocumentTree(final boolean persist, final Promise promise) {
|
||||
this.documentHelper.openDocumentTree(persist, promise);
|
||||
this.efficientDocumentHelper.openDocumentTree(persist, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
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
|
||||
public void createDocument(
|
||||
final String data,
|
||||
final String encoding,
|
||||
final String initialName,
|
||||
final String mimeType,
|
||||
final Promise promise) {
|
||||
this.documentHelper.createDocument(data, encoding, initialName, mimeType, promise);
|
||||
final String data,
|
||||
final String encoding,
|
||||
final String initialName,
|
||||
final String mimeType,
|
||||
final Promise promise) {
|
||||
this.efficientDocumentHelper.createDocument(data, encoding, initialName, mimeType, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hasPermission(String uriString, final Promise promise) {
|
||||
if (this.documentHelper.hasPermission(uriString)) {
|
||||
promise.resolve(true);
|
||||
} else {
|
||||
promise.resolve(false);
|
||||
}
|
||||
this.efficientDocumentHelper.hasPermission(uriString, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void exists(String uriString, final Promise promise) {
|
||||
try {
|
||||
promise.resolve(this.documentHelper.exists(uriString));
|
||||
} catch (SecurityException e) {
|
||||
promise.reject("EPERM", e.getLocalizedMessage());
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getLocalizedMessage());
|
||||
}
|
||||
this.efficientDocumentHelper.exists(uriString, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void readFile(String uriString, String encoding, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
this.efficientDocumentHelper.readFile(uriString, encoding, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void writeFile(
|
||||
String uriString,
|
||||
String data,
|
||||
String encoding,
|
||||
String mimeType,
|
||||
boolean append,
|
||||
final Promise promise) {
|
||||
try {
|
||||
DocumentFile file;
|
||||
|
||||
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());
|
||||
}
|
||||
String uriString,
|
||||
String data,
|
||||
String encoding,
|
||||
String mimeType,
|
||||
boolean append,
|
||||
final Promise promise) {
|
||||
this.efficientDocumentHelper.writeFile(
|
||||
uriString, data, encoding, mimeType, append, promise
|
||||
);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void transferFile(
|
||||
String srcUri, String destUri, boolean replaceIfDestExists, boolean copy, Promise promise) {
|
||||
this.documentHelper.transferFile(srcUri, destUri, replaceIfDestExists, copy, promise);
|
||||
String srcUri, String destUri, boolean replaceIfDestExists, boolean copy, Promise promise) {
|
||||
this.efficientDocumentHelper.transferFile(srcUri, destUri, replaceIfDestExists, copy, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void rename(String uriString, String newName, final Promise promise) {
|
||||
try {
|
||||
|
||||
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());
|
||||
}
|
||||
this.efficientDocumentHelper.renameTo(uriString, newName, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unlink(String uriString, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
this.efficientDocumentHelper.unlink(uriString, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void mkdir(String uriString, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
this.efficientDocumentHelper.mkdir(uriString, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void createFile(String uriString, String mimeType, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
this.efficientDocumentHelper.createFile(uriString, mimeType, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getPersistedUriPermissions(final Promise promise) {
|
||||
String[] uriList =
|
||||
getReactApplicationContext().getContentResolver().getPersistedUriPermissions().stream()
|
||||
.map(uriPermission -> uriPermission.getUri().toString())
|
||||
.toArray(String[]::new);
|
||||
getReactApplicationContext().getContentResolver().getPersistedUriPermissions().stream()
|
||||
.map(uriPermission -> uriPermission.getUri().toString())
|
||||
.toArray(String[]::new);
|
||||
|
||||
WritableArray wa = Arguments.fromArray(uriList);
|
||||
promise.resolve(wa);
|
||||
@ -232,52 +123,20 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
public void releasePersistableUriPermission(String uriString, final Promise promise) {
|
||||
Uri uriToRevoke = Uri.parse(UriHelper.normalize(uriString));
|
||||
final int takeFlags =
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
this.getReactApplicationContext()
|
||||
.getContentResolver()
|
||||
.releasePersistableUriPermission(uriToRevoke, takeFlags);
|
||||
.getContentResolver()
|
||||
.releasePersistableUriPermission(uriToRevoke, takeFlags);
|
||||
promise.resolve(null);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void listFiles(String uriString, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
efficientDocumentHelper.listFiles(uriString, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void stat(String uriString, final Promise promise) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
efficientDocumentHelper.stat(uriString, promise);
|
||||
}
|
||||
}
|
||||
|
@ -12,17 +12,17 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SafXPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
modules.add(new SafXModule(reactContext));
|
||||
return modules;
|
||||
}
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
modules.add(new SafXModule(reactContext));
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
@ -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,13 +1,14 @@
|
||||
package com.reactnativesafx.utils;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GeneralHelper {
|
||||
/**
|
||||
* String to byte converter method
|
||||
*
|
||||
* @param data Raw data in string format
|
||||
* @param data Raw data in string format
|
||||
* @param encoding Decoder name
|
||||
* @return Converted data byte array
|
||||
*/
|
||||
|
@ -2,61 +2,92 @@ package com.reactnativesafx.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
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 static final String CONTENT_URI_PREFIX = "content://";
|
||||
public static final Pattern DOCUMENT_TREE_PREFIX =
|
||||
Pattern.compile("^content://.*?/tree/.+?", Pattern.CASE_INSENSITIVE);
|
||||
private static final String PATH_TREE = "tree";
|
||||
private static final String PATH_DOCUMENT = "document";
|
||||
|
||||
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
|
||||
// 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");
|
||||
}
|
||||
|
||||
return Uri.parse(Uri.decode(uriString)).getLastPathSegment();
|
||||
}
|
||||
|
||||
public static boolean isContentDocumentTreeUri(String uriString) {
|
||||
return DOCUMENT_TREE_PREFIX.matcher(uriString).matches();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
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:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
// normalized:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
|
||||
// uri parts:
|
||||
String[] parts = Uri.decode(uriString).split(":");
|
||||
return parts[0] + ":" + parts[1] + Uri.encode(":" + parts[2]);
|
||||
if (uri.getPath().indexOf(":") != -1) {
|
||||
// uri parts:
|
||||
String[] parts = Uri.decode(uri.toString()).split(":");
|
||||
return parts[0] + ":" + parts[1] + Uri.encode(":" + parts[2]);
|
||||
}
|
||||
}
|
||||
return uriString;
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
public static String denormalize(String uriString) {
|
||||
if (isContentDocumentTreeUri(uriString)) {
|
||||
public static String denormalize(Uri uri) {
|
||||
if (DocumentsContract.isTreeUri(uri)) {
|
||||
// an normalized uri example:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
// denormalized:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B/Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
|
||||
return Uri.decode(normalize(uriString));
|
||||
}
|
||||
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.decode(normalize(uri));
|
||||
}
|
||||
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
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -5,7 +5,6 @@
|
||||
"main": "src/index",
|
||||
"react-native": "src/index",
|
||||
"source": "src/index",
|
||||
"private": true,
|
||||
"files": [
|
||||
"src",
|
||||
"lib",
|
||||
@ -15,7 +14,10 @@
|
||||
"react-native-saf-x.podspec",
|
||||
"!lib/typescript/example",
|
||||
"!android/build",
|
||||
"!ios/build"
|
||||
"!ios/build",
|
||||
"!**/__tests__",
|
||||
"!**/__fixtures__",
|
||||
"!**/__mocks__"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
@ -30,8 +32,10 @@
|
||||
"scoped",
|
||||
"storage",
|
||||
"SAF",
|
||||
"storage-access-framework"
|
||||
"storage-access-framework",
|
||||
"fs"
|
||||
],
|
||||
"repository": "https://github.com/jd1378/react-native-saf-x",
|
||||
"author": "Javad Mnjd (https://github.com/jd1378)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/react-native-saf-x",
|
||||
@ -40,10 +44,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@types/react": "16.14.21",
|
||||
"@types/react-native": "0.64.19",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.66.1",
|
||||
"react-native": "0.70.6",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -50,11 +50,11 @@ interface SafxInterface {
|
||||
encoding?: Encoding,
|
||||
mimeType?: string,
|
||||
append?: boolean,
|
||||
): Promise<string>;
|
||||
): Promise<void>;
|
||||
createFile(uriString: string, mimeType?: String): Promise<DocumentFileDetail>;
|
||||
unlink(uriString: string): Promise<boolean>;
|
||||
mkdir(uriString: string): Promise<DocumentFileDetail>;
|
||||
rename(uriString: string, newName: string): Promise<boolean>;
|
||||
rename(uriString: string, newName: string): Promise<DocumentFileDetail>;
|
||||
getPersistedUriPermissions(): Promise<Array<string>>;
|
||||
releasePersistableUriPermission(uriString: string): Promise<void>;
|
||||
listFiles(uriString: string): Promise<DocumentFileDetail[]>;
|
||||
@ -72,8 +72,8 @@ export type DocumentFileDetail = {
|
||||
name: string;
|
||||
type: 'directory' | 'file';
|
||||
lastModified: number;
|
||||
mime?: string;
|
||||
size?: number;
|
||||
mime: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type FileOperationOptions = {
|
||||
|
Loading…
Reference in New Issue
Block a user