mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Android: Fix note attachment issue (#6932)
This commit is contained in:
parent
5fd5be1e09
commit
0d5f96f5bb
@ -24,6 +24,9 @@ import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -43,9 +46,13 @@ public class SharePackage implements ReactPackage {
|
||||
}
|
||||
|
||||
public static class ShareModule extends ReactContextBaseJavaModule implements ActivityEventListener {
|
||||
private final String cacheDir;
|
||||
// when refactoring the `shareDirName` make sure to refactor the dir name in `ShareUtils.ts`
|
||||
private static String shareDirName = "sharedFiles";
|
||||
|
||||
ShareModule(@NonNull ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
cacheDir = reactContext.getCacheDir().getAbsolutePath();
|
||||
reactContext.addActivityEventListener(this);
|
||||
}
|
||||
|
||||
@ -139,7 +146,39 @@ public class SharePackage implements ReactPackage {
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
|
||||
Uri copiedUri = null;
|
||||
try {
|
||||
String shareFolderPath = cacheDir + "/" + shareDirName;
|
||||
String filepath = shareFolderPath + "/" + name;
|
||||
|
||||
File file = new File(filepath);
|
||||
copiedUri = Uri.fromFile(file);
|
||||
if (new File(shareFolderPath).mkdirs()) {
|
||||
try (InputStream inStream =
|
||||
contentResolver.openInputStream(uri);
|
||||
OutputStream outStream =
|
||||
contentResolver.openOutputStream(copiedUri, "wt");
|
||||
) {
|
||||
byte[] buffer = new byte[1024 * 4];
|
||||
int length;
|
||||
while ((length = inStream.read(buffer)) > 0) {
|
||||
outStream.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Cannot create sharedFiles directory in cacheDir");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
copiedUri = null;
|
||||
}
|
||||
|
||||
if (copiedUri != null) {
|
||||
imageData.putString("uri", copiedUri.toString());
|
||||
} else {
|
||||
imageData.putString("uri", uri.toString());
|
||||
}
|
||||
imageData.putString("name", name);
|
||||
imageData.putString("mimeType", mimeType);
|
||||
return imageData;
|
||||
|
@ -22,7 +22,6 @@ const { themeStyle } = require('../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
const RNFS = require('react-native-fs');
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions(): any {
|
||||
@ -114,11 +113,18 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
const logItemCsv = service.csvCreate(logItemRows);
|
||||
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = `${RNFS.ExternalDirectoryPath}/syncReport-${new Date().getTime()}.txt`;
|
||||
|
||||
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
|
||||
|
||||
if (!externalDir) {
|
||||
this.setState({ creatingReport: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`;
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
|
||||
alert(`Debug report exported to ${filePath}`);
|
||||
this.setState({ creatingReport: false });
|
||||
};
|
||||
@ -130,7 +136,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
};
|
||||
|
||||
this.exportProfileButtonPress_ = async () => {
|
||||
const p = this.state.profileExportPath ? this.state.profileExportPath : `${RNFS.ExternalStorageDirectoryPath}/JoplinProfileExport`;
|
||||
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
|
||||
if (!externalDir) {
|
||||
return;
|
||||
}
|
||||
const p = this.state.profileExportPath ? this.state.profileExportPath : `${externalDir}/JoplinProfileExport`;
|
||||
|
||||
this.setState({
|
||||
profileExportStatus: 'prompt',
|
||||
profileExportPath: p,
|
||||
@ -500,7 +511,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
if (md.key === 'sync.2.path' && Platform.OS === 'android' && Platform.Version > 28) {
|
||||
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
|
||||
return (
|
||||
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
|
||||
<View style={this.styles().settingContainer}>
|
||||
|
@ -13,7 +13,6 @@ const React = require('react');
|
||||
const { Platform, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share, PermissionsAndroid } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
@ -37,7 +36,6 @@ const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle, editorFont } = require('../global-style.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const DocumentPicker = require('react-native-document-picker').default;
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
|
||||
const ImagePicker = require('react-native-image-picker').default;
|
||||
@ -543,18 +541,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
private async pickDocuments() {
|
||||
try {
|
||||
// the result is an array
|
||||
const result = await DocumentPicker.pickMultiple();
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (DocumentPicker.isCancel(error)) {
|
||||
const result = await shim.fsDriver().pickDocument();
|
||||
if (!result) {
|
||||
console.info('pickDocuments: user has cancelled');
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async imageDimensions(uri: string) {
|
||||
@ -614,15 +605,15 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
|
||||
|
||||
await RNFS.copyFile(resizedImagePath, targetPath);
|
||||
await shim.fsDriver().copy(resizedImagePath, targetPath);
|
||||
|
||||
try {
|
||||
await RNFS.unlink(resizedImagePath);
|
||||
await shim.fsDriver().unlink(resizedImagePath);
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error when unlinking cached file: ', error);
|
||||
}
|
||||
} else {
|
||||
await RNFS.copyFile(localFilePath, targetPath);
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -678,8 +669,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return;
|
||||
} else {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
|
||||
const stat = await shim.fsDriver().stat(targetPath);
|
||||
|
||||
if (stat.size >= 200 * 1024 * 1024) {
|
||||
await shim.fsDriver().remove(targetPath);
|
||||
throw new Error('Resources larger than 200 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
|
@ -3,6 +3,7 @@ import { ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { CachesDirectoryPath } from 'react-native-fs';
|
||||
|
||||
// when refactoring this name, make sure to refactor the `SharePackage.java` (in android) as well
|
||||
const DIR_NAME = 'sharedFiles';
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,10 @@
|
||||
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||
const RNFS = require('react-native-fs');
|
||||
import RNSAF, { Encoding, DocumentFileDetail } from '@joplin/react-native-saf-x';
|
||||
const DocumentPicker = require('react-native-document-picker').default;
|
||||
import { openDocument } from '@joplin/react-native-saf-x';
|
||||
import RNSAF, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const ANDROID_URI_PREFIX = 'content://';
|
||||
|
||||
@ -249,4 +252,57 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
public async md5File(path: string): Promise<string> {
|
||||
throw new Error(`Not implemented: md5File(): ${path}`);
|
||||
}
|
||||
|
||||
public async getExternalDirectoryPath(): Promise<string | undefined> {
|
||||
let directory;
|
||||
if (this.isUsingAndroidSAF()) {
|
||||
const doc = await openDocumentTree(true);
|
||||
if (doc?.uri) {
|
||||
directory = doc?.uri;
|
||||
}
|
||||
} else {
|
||||
directory = RNFS.ExternalDirectoryPath;
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
public isUsingAndroidSAF() {
|
||||
return Platform.OS === 'android' && Platform.Version > 28;
|
||||
}
|
||||
|
||||
/** always returns an array */
|
||||
public async pickDocument(options: {multiple: false}) {
|
||||
const { multiple = false } = options || {};
|
||||
let result;
|
||||
try {
|
||||
if (this.isUsingAndroidSAF()) {
|
||||
result = await openDocument({ multiple });
|
||||
if (!result) {
|
||||
// to catch the error down below using the 'cancel' keyword
|
||||
throw new Error('User canceled document picker');
|
||||
}
|
||||
result = result.map(r => {
|
||||
(r.type as string) = r.mime;
|
||||
((r as any).fileCopyUri as string) = r.uri;
|
||||
return r;
|
||||
});
|
||||
} else {
|
||||
// the result is an array
|
||||
if (multiple) {
|
||||
result = await DocumentPicker.pickMultiple();
|
||||
} else {
|
||||
result = [await DocumentPicker.pick()];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (DocumentPicker.isCancel(error) || error?.message?.includes('cancel')) {
|
||||
console.info('pickDocuments: user has cancelled');
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ package com.reactnativesafx;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
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;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
@ -17,6 +19,7 @@ import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.reactnativesafx.utils.DocumentHelper;
|
||||
import com.reactnativesafx.utils.GeneralHelper;
|
||||
import com.reactnativesafx.utils.UriHelper;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@ -47,8 +50,8 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void openDocument(final boolean persist, final Promise promise) {
|
||||
this.documentHelper.openDocument(persist, promise);
|
||||
public void openDocument(final boolean persist, final boolean multiple, final Promise promise) {
|
||||
this.documentHelper.openDocument(persist, multiple, promise);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
@ -74,6 +77,8 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
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());
|
||||
}
|
||||
@ -176,7 +181,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
promise.resolve(true);
|
||||
} catch (FileNotFoundException e) {
|
||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
||||
promise.resolve(true);
|
||||
} catch (SecurityException e) {
|
||||
promise.reject("EPERM", e.getLocalizedMessage());
|
||||
} catch (Exception e) {
|
||||
@ -188,7 +193,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
public void mkdir(String uriString, final Promise promise) {
|
||||
try {
|
||||
DocumentFile dir = this.documentHelper.mkdir(uriString);
|
||||
DocumentHelper.resolveWithDocument(dir, promise, uriString);
|
||||
DocumentHelper.resolveWithDocument(dir, uriString, promise);
|
||||
} catch (IOException e) {
|
||||
promise.reject("EEXIST", e.getLocalizedMessage());
|
||||
} catch (SecurityException e) {
|
||||
@ -202,7 +207,11 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
public void createFile(String uriString, String mimeType, final Promise promise) {
|
||||
try {
|
||||
DocumentFile createdFile = this.documentHelper.createFile(uriString, mimeType);
|
||||
DocumentHelper.resolveWithDocument(createdFile, promise, uriString);
|
||||
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());
|
||||
}
|
||||
@ -241,10 +250,10 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
docEntry ->
|
||||
DocumentHelper.resolveWithDocument(
|
||||
docEntry,
|
||||
null,
|
||||
trailingSlash.matcher(uriString).replaceFirst("")
|
||||
+ "/"
|
||||
+ docEntry.getName()))
|
||||
+ docEntry.getName(),
|
||||
null))
|
||||
.toArray(WritableMap[]::new);
|
||||
WritableArray resolveData = Arguments.fromJavaArgs(resolvedDocs);
|
||||
promise.resolve(resolveData);
|
||||
@ -262,7 +271,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
||||
try {
|
||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
||||
|
||||
DocumentHelper.resolveWithDocument(doc, promise, uriString);
|
||||
DocumentHelper.resolveWithDocument(doc, uriString, promise);
|
||||
} catch (FileNotFoundException e) {
|
||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
||||
} catch (SecurityException e) {
|
||||
|
@ -2,6 +2,7 @@ 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;
|
||||
@ -9,16 +10,19 @@ 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;
|
||||
@ -71,9 +75,9 @@ public class DocumentHelper {
|
||||
|
||||
try {
|
||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
||||
resolveWithDocument(doc, promise, uri.toString());
|
||||
resolveWithDocument(doc, uri.toString(), promise);
|
||||
} catch (Exception e) {
|
||||
promise.resolve(null);
|
||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||
}
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
@ -99,16 +103,19 @@ public class DocumentHelper {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
promise.reject("ERROR", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void openDocument(final boolean persist, final Promise promise) {
|
||||
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) {
|
||||
@ -122,9 +129,14 @@ public class DocumentHelper {
|
||||
@Override
|
||||
public void onActivityResult(
|
||||
Activity activity, int requestCode, int resultCode, Intent intent) {
|
||||
if (requestCode == DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
if (intent != null) {
|
||||
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()
|
||||
@ -134,21 +146,38 @@ public class DocumentHelper {
|
||||
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
||||
resolveWithDocument(doc, promise, uri.toString());
|
||||
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.resolve(null);
|
||||
}
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
}
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
}
|
||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||
} finally {
|
||||
context.removeActivityEventListener(activityEventListener);
|
||||
activityEventListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {}
|
||||
@ -160,11 +189,12 @@ public class DocumentHelper {
|
||||
if (activity != null) {
|
||||
activity.startActivityForResult(intent, DOCUMENT_REQUEST_CODE);
|
||||
} else {
|
||||
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
||||
promise.reject(
|
||||
"EUNSPECIFIED", "Cannot get current activity, so cannot launch document picker");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +241,7 @@ public class DocumentHelper {
|
||||
os.write(bytes);
|
||||
}
|
||||
assert doc != null;
|
||||
resolveWithDocument(doc, promise, uri.toString());
|
||||
resolveWithDocument(doc, uri.toString(), promise);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getLocalizedMessage());
|
||||
}
|
||||
@ -239,7 +269,7 @@ public class DocumentHelper {
|
||||
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
promise.reject("ERROR", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,18 +302,19 @@ public class DocumentHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists(final String uriString) {
|
||||
public boolean exists(final String uriString) throws SecurityException {
|
||||
return this.exists(uriString, false);
|
||||
}
|
||||
|
||||
public boolean exists(final String uriString, final boolean shouldBeFile) {
|
||||
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 (Exception e) {
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -308,12 +339,8 @@ public class DocumentHelper {
|
||||
&& permission.isWritePermission();
|
||||
}
|
||||
|
||||
private String getPermissionErrorMsg(final String uriString) {
|
||||
return "You don't have read/write permission to access uri: " + uriString;
|
||||
}
|
||||
|
||||
public static WritableMap resolveWithDocument(
|
||||
@NonNull DocumentFile file, Promise promise, String SimplifiedUri) {
|
||||
@NonNull DocumentFile file, String SimplifiedUri, Promise promise) {
|
||||
WritableMap fileMap = Arguments.createMap();
|
||||
fileMap.putString("uri", UriHelper.denormalize(SimplifiedUri));
|
||||
fileMap.putString("name", file.getName());
|
||||
@ -362,13 +389,38 @@ public class DocumentHelper {
|
||||
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 : "*/*", fileName);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -409,14 +461,30 @@ public class DocumentHelper {
|
||||
"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) {
|
||||
if (!doc.canRead()) {
|
||||
throw new SecurityException(
|
||||
"You don't have read permission to access uri: " + unknownUriString);
|
||||
}
|
||||
if (doc.isFile() && doc.exists()) {
|
||||
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)) {
|
||||
@ -426,20 +494,11 @@ public class DocumentHelper {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (baseUri.equals("")) {
|
||||
throw new SecurityException(getPermissionErrorMsg(uriString));
|
||||
}
|
||||
|
||||
if (baseUri.matches("^content://[\\w.]+/document/.*")) {
|
||||
// It's a document picked by user
|
||||
DocumentFile doc = DocumentFile.fromSingleUri(context, Uri.parse(uriString));
|
||||
if (doc != null && doc.isFile() && doc.exists()) {
|
||||
return doc;
|
||||
} else {
|
||||
throw new FileNotFoundException(
|
||||
"Cannot find the given document. File does not exist at '" + uriString + "'");
|
||||
}
|
||||
// It's possible that the file access is temporary
|
||||
baseUri = uriString;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(baseUri);
|
||||
@ -479,7 +538,14 @@ public class DocumentHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert dir != null;
|
||||
|
||||
if (!dir.canRead() || !dir.canWrite()) {
|
||||
throw new SecurityException(
|
||||
"You don't have read/write permission to access uri: " + uriString);
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
@ -517,7 +583,7 @@ public class DocumentHelper {
|
||||
srcDoc.delete();
|
||||
}
|
||||
|
||||
promise.resolve(resolveWithDocument(destDoc, promise, destUri));
|
||||
promise.resolve(resolveWithDocument(destDoc, destUri, promise));
|
||||
} catch (Exception e) {
|
||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||
}
|
||||
|
@ -3,19 +3,28 @@ package com.reactnativesafx.utils;
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@RequiresApi(api = VERSION_CODES.Q)
|
||||
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);
|
||||
|
||||
public static String getLastSegment(String uriString) {
|
||||
|
||||
return Uri.parse(Uri.decode(uriString)).getLastPathSegment();
|
||||
}
|
||||
|
||||
public static boolean isContentDocumentTreeUri(String uriString) {
|
||||
return DOCUMENT_TREE_PREFIX.matcher(uriString).matches();
|
||||
}
|
||||
|
||||
public static String normalize(String uriString) {
|
||||
if (uriString.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
if (isContentDocumentTreeUri(uriString)) {
|
||||
// an abnormal uri example:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin/locks/2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
// normalized:
|
||||
@ -29,7 +38,7 @@ public class UriHelper {
|
||||
}
|
||||
|
||||
public static String denormalize(String uriString) {
|
||||
if (uriString.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
if (isContentDocumentTreeUri(uriString)) {
|
||||
// an normalized uri example:
|
||||
// content://com.android.externalstorage.documents/tree/1707-3F0B%3Ajoplin%2Flocks%2F2_2_fa4f9801e9a545a58f1a6c5d3a7cfded.json
|
||||
// denormalized:
|
||||
@ -48,11 +57,6 @@ public class UriHelper {
|
||||
|| uri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
|
||||
throw new IllegalArgumentException("Invalid Uri: Scheme not supported");
|
||||
}
|
||||
|
||||
if (uri.getScheme() == null) {
|
||||
throw new IllegalArgumentException("Invalid Uri: Cannot determine scheme");
|
||||
}
|
||||
|
||||
return uri.toString();
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,10 @@ export type Encoding = 'utf8' | 'base64' | 'ascii';
|
||||
/** Native interface of the module */
|
||||
interface SafxInterface {
|
||||
openDocumentTree(persist: boolean): Promise<DocumentFileDetail | null>;
|
||||
openDocument(persist: boolean): Promise<DocumentFileDetail | null>;
|
||||
openDocument(
|
||||
persist: boolean,
|
||||
multiple: boolean,
|
||||
): Promise<DocumentFileDetail[] | null>;
|
||||
createDocument(
|
||||
data: String,
|
||||
encoding?: String,
|
||||
@ -97,12 +100,21 @@ export function openDocumentTree(persist: boolean) {
|
||||
return SafX.openDocumentTree(persist);
|
||||
}
|
||||
|
||||
export type OpenDocumentOptions = {
|
||||
/** should the permission of returned document(s) be persisted ? */
|
||||
persist?: boolean;
|
||||
/** should the file picker allow multiple documents ? */
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the Document Picker to select a file.
|
||||
* Returns an object of type `DocumentFileDetail` or `null` if user did not select a file.
|
||||
* DocumentFileDetail is always an array.
|
||||
* @returns `DocumentFileDetail[]` or `null` if user did not select a file.
|
||||
*/
|
||||
export function openDocument(persist: boolean) {
|
||||
return SafX.openDocument(persist);
|
||||
export function openDocument(options: OpenDocumentOptions) {
|
||||
const { persist = false, multiple = false } = options;
|
||||
return SafX.openDocument(persist, multiple);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,9 +175,10 @@ export function createFile(
|
||||
return SafX.createFile(uriString, mimeType);
|
||||
}
|
||||
|
||||
//
|
||||
// Removes the file or directory at given uri.
|
||||
// Resolves with `true` if delete is successful, `false` otherwise.
|
||||
/**
|
||||
* Removes the file or directory at given uri.
|
||||
* Resolves with `true` if delete is successful, throws otherwise.
|
||||
*/
|
||||
export function unlink(uriString: string) {
|
||||
return SafX.unlink(uriString);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user