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 com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -43,9 +46,13 @@ public class SharePackage implements ReactPackage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class ShareModule extends ReactContextBaseJavaModule implements ActivityEventListener {
|
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) {
|
ShareModule(@NonNull ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
cacheDir = reactContext.getCacheDir().getAbsolutePath();
|
||||||
reactContext.addActivityEventListener(this);
|
reactContext.addActivityEventListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +146,39 @@ public class SharePackage implements ReactPackage {
|
|||||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
imageData.putString("uri", uri.toString());
|
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("name", name);
|
||||||
imageData.putString("mimeType", mimeType);
|
imageData.putString("mimeType", mimeType);
|
||||||
return imageData;
|
return imageData;
|
||||||
|
@ -22,7 +22,6 @@ const { themeStyle } = require('../global-style.js');
|
|||||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||||
const RNFS = require('react-native-fs');
|
|
||||||
|
|
||||||
class ConfigScreenComponent extends BaseScreenComponent {
|
class ConfigScreenComponent extends BaseScreenComponent {
|
||||||
static navigationOptions(): any {
|
static navigationOptions(): any {
|
||||||
@ -114,11 +113,18 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
const logItemCsv = service.csvCreate(logItemRows);
|
const logItemCsv = service.csvCreate(logItemRows);
|
||||||
|
|
||||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
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');
|
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
|
||||||
|
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
|
||||||
await RNFS.writeFile(filePath, finalText);
|
|
||||||
alert(`Debug report exported to ${filePath}`);
|
alert(`Debug report exported to ${filePath}`);
|
||||||
this.setState({ creatingReport: false });
|
this.setState({ creatingReport: false });
|
||||||
};
|
};
|
||||||
@ -130,7 +136,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.exportProfileButtonPress_ = async () => {
|
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({
|
this.setState({
|
||||||
profileExportStatus: 'prompt',
|
profileExportStatus: 'prompt',
|
||||||
profileExportPath: p,
|
profileExportPath: p,
|
||||||
@ -500,7 +511,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else if (md.type === Setting.TYPE_STRING) {
|
} 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 (
|
return (
|
||||||
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
|
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
|
||||||
<View 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 { Platform, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share, PermissionsAndroid } = require('react-native');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
||||||
const RNFS = require('react-native-fs');
|
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
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 { themeStyle, editorFont } = require('../global-style.js');
|
||||||
const { dialogs } = require('../../utils/dialogs.js');
|
const { dialogs } = require('../../utils/dialogs.js');
|
||||||
const DialogBox = require('react-native-dialogbox').default;
|
const DialogBox = require('react-native-dialogbox').default;
|
||||||
const DocumentPicker = require('react-native-document-picker').default;
|
|
||||||
const ImageResizer = require('react-native-image-resizer').default;
|
const ImageResizer = require('react-native-image-resizer').default;
|
||||||
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
|
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
|
||||||
const ImagePicker = require('react-native-image-picker').default;
|
const ImagePicker = require('react-native-image-picker').default;
|
||||||
@ -543,18 +541,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pickDocuments() {
|
private async pickDocuments() {
|
||||||
try {
|
const result = await shim.fsDriver().pickDocument();
|
||||||
// the result is an array
|
if (!result) {
|
||||||
const result = await DocumentPicker.pickMultiple();
|
console.info('pickDocuments: user has cancelled');
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
if (DocumentPicker.isCancel(error)) {
|
|
||||||
console.info('pickDocuments: user has cancelled');
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async imageDimensions(uri: string) {
|
async imageDimensions(uri: string) {
|
||||||
@ -614,15 +605,15 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
reg.logger().info('Resized image ', resizedImagePath);
|
reg.logger().info('Resized image ', resizedImagePath);
|
||||||
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
|
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
|
||||||
|
|
||||||
await RNFS.copyFile(resizedImagePath, targetPath);
|
await shim.fsDriver().copy(resizedImagePath, targetPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await RNFS.unlink(resizedImagePath);
|
await shim.fsDriver().unlink(resizedImagePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reg.logger().warn('Error when unlinking cached file: ', error);
|
reg.logger().warn('Error when unlinking cached file: ', error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await RNFS.copyFile(localFilePath, targetPath);
|
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -678,8 +669,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||||
|
|
||||||
const stat = await shim.fsDriver().stat(targetPath);
|
const stat = await shim.fsDriver().stat(targetPath);
|
||||||
|
|
||||||
if (stat.size >= 200 * 1024 * 1024) {
|
if (stat.size >= 200 * 1024 * 1024) {
|
||||||
await shim.fsDriver().remove(targetPath);
|
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.');
|
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 shim from '@joplin/lib/shim';
|
||||||
import { CachesDirectoryPath } from 'react-native-fs';
|
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';
|
const DIR_NAME = 'sharedFiles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
||||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||||
const RNFS = require('react-native-fs');
|
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://';
|
const ANDROID_URI_PREFIX = 'content://';
|
||||||
|
|
||||||
@ -249,4 +252,57 @@ export default class FsDriverRN extends FsDriverBase {
|
|||||||
public async md5File(path: string): Promise<string> {
|
public async md5File(path: string): Promise<string> {
|
||||||
throw new Error(`Not implemented: md5File(): ${path}`);
|
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.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build.VERSION_CODES;
|
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 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;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
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.DocumentHelper;
|
||||||
import com.reactnativesafx.utils.GeneralHelper;
|
import com.reactnativesafx.utils.GeneralHelper;
|
||||||
import com.reactnativesafx.utils.UriHelper;
|
import com.reactnativesafx.utils.UriHelper;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -47,8 +50,8 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void openDocument(final boolean persist, final Promise promise) {
|
public void openDocument(final boolean persist, final boolean multiple, final Promise promise) {
|
||||||
this.documentHelper.openDocument(persist, promise);
|
this.documentHelper.openDocument(persist, multiple, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
@ -74,6 +77,8 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
public void exists(String uriString, final Promise promise) {
|
public void exists(String uriString, final Promise promise) {
|
||||||
try {
|
try {
|
||||||
promise.resolve(this.documentHelper.exists(uriString));
|
promise.resolve(this.documentHelper.exists(uriString));
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
promise.reject("EPERM", e.getLocalizedMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
promise.reject("ERROR", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@ -176,7 +181,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
}
|
}
|
||||||
promise.resolve(true);
|
promise.resolve(true);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
promise.resolve(true);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
promise.reject("EPERM", e.getLocalizedMessage());
|
promise.reject("EPERM", e.getLocalizedMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -188,7 +193,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
public void mkdir(String uriString, final Promise promise) {
|
public void mkdir(String uriString, final Promise promise) {
|
||||||
try {
|
try {
|
||||||
DocumentFile dir = this.documentHelper.mkdir(uriString);
|
DocumentFile dir = this.documentHelper.mkdir(uriString);
|
||||||
DocumentHelper.resolveWithDocument(dir, promise, uriString);
|
DocumentHelper.resolveWithDocument(dir, uriString, promise);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
promise.reject("EEXIST", e.getLocalizedMessage());
|
promise.reject("EEXIST", e.getLocalizedMessage());
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
@ -202,7 +207,11 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
public void createFile(String uriString, String mimeType, final Promise promise) {
|
public void createFile(String uriString, String mimeType, final Promise promise) {
|
||||||
try {
|
try {
|
||||||
DocumentFile createdFile = this.documentHelper.createFile(uriString, mimeType);
|
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) {
|
} catch (Exception e) {
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@ -236,16 +245,16 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
||||||
|
|
||||||
WritableMap[] resolvedDocs =
|
WritableMap[] resolvedDocs =
|
||||||
Arrays.stream(doc.listFiles())
|
Arrays.stream(doc.listFiles())
|
||||||
.map(
|
.map(
|
||||||
docEntry ->
|
docEntry ->
|
||||||
DocumentHelper.resolveWithDocument(
|
DocumentHelper.resolveWithDocument(
|
||||||
docEntry,
|
docEntry,
|
||||||
null,
|
trailingSlash.matcher(uriString).replaceFirst("")
|
||||||
trailingSlash.matcher(uriString).replaceFirst("")
|
+ "/"
|
||||||
+ "/"
|
+ docEntry.getName(),
|
||||||
+ docEntry.getName()))
|
null))
|
||||||
.toArray(WritableMap[]::new);
|
.toArray(WritableMap[]::new);
|
||||||
WritableArray resolveData = Arguments.fromJavaArgs(resolvedDocs);
|
WritableArray resolveData = Arguments.fromJavaArgs(resolvedDocs);
|
||||||
promise.resolve(resolveData);
|
promise.resolve(resolveData);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
@ -262,7 +271,7 @@ public class SafXModule extends ReactContextBaseJavaModule {
|
|||||||
try {
|
try {
|
||||||
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
DocumentFile doc = this.documentHelper.goToDocument(uriString, false, true);
|
||||||
|
|
||||||
DocumentHelper.resolveWithDocument(doc, promise, uriString);
|
DocumentHelper.resolveWithDocument(doc, uriString, promise);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
promise.reject("ENOENT", e.getLocalizedMessage());
|
promise.reject("ENOENT", e.getLocalizedMessage());
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
|
@ -2,6 +2,7 @@ package com.reactnativesafx.utils;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.UriPermission;
|
import android.content.UriPermission;
|
||||||
@ -9,16 +10,19 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.documentfile.provider.DocumentFileHelper;
|
import androidx.documentfile.provider.DocumentFileHelper;
|
||||||
|
|
||||||
import com.facebook.react.bridge.ActivityEventListener;
|
import com.facebook.react.bridge.ActivityEventListener;
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.Promise;
|
import com.facebook.react.bridge.Promise;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.WritableArray;
|
import com.facebook.react.bridge.WritableArray;
|
||||||
import com.facebook.react.bridge.WritableMap;
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -71,9 +75,9 @@ public class DocumentHelper {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
DocumentFile doc = goToDocument(uri.toString(), false);
|
||||||
resolveWithDocument(doc, promise, uri.toString());
|
resolveWithDocument(doc, uri.toString(), promise);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
promise.resolve(null);
|
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
promise.resolve(null);
|
promise.resolve(null);
|
||||||
@ -99,16 +103,19 @@ public class DocumentHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} 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 {
|
try {
|
||||||
|
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
if (multiple) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
}
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
|
|
||||||
if (activityEventListener != null) {
|
if (activityEventListener != null) {
|
||||||
@ -122,32 +129,54 @@ public class DocumentHelper {
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(
|
public void onActivityResult(
|
||||||
Activity activity, int requestCode, int resultCode, Intent intent) {
|
Activity activity, int requestCode, int resultCode, Intent intent) {
|
||||||
if (requestCode == DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
try {
|
||||||
if (intent != null) {
|
WritableArray resolvedDocs = Arguments.createArray();
|
||||||
|
if (requestCode == DOCUMENT_REQUEST_CODE
|
||||||
|
&& resultCode == Activity.RESULT_OK
|
||||||
|
&& intent != null) {
|
||||||
Uri uri = intent.getData();
|
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);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
DocumentFile doc = goToDocument(uri.toString(), false);
|
DocumentFile doc = goToDocument(uri.toString(), false);
|
||||||
resolveWithDocument(doc, promise, uri.toString());
|
WritableMap docInfo = resolveWithDocument(doc, uri.toString(), null);
|
||||||
} catch (Exception e) {
|
resolvedDocs.pushMap(docInfo);
|
||||||
promise.resolve(null);
|
} 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 {
|
} else {
|
||||||
promise.resolve(null);
|
promise.resolve(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
promise.resolve(resolvedDocs);
|
||||||
promise.resolve(null);
|
} catch (Exception e) {
|
||||||
|
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||||
|
} finally {
|
||||||
|
context.removeActivityEventListener(activityEventListener);
|
||||||
|
activityEventListener = null;
|
||||||
}
|
}
|
||||||
context.removeActivityEventListener(activityEventListener);
|
|
||||||
activityEventListener = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -160,11 +189,12 @@ public class DocumentHelper {
|
|||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
activity.startActivityForResult(intent, DOCUMENT_REQUEST_CODE);
|
activity.startActivityForResult(intent, DOCUMENT_REQUEST_CODE);
|
||||||
} else {
|
} 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) {
|
} catch (Exception e) {
|
||||||
promise.reject("ERROR", e.getMessage());
|
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +241,7 @@ public class DocumentHelper {
|
|||||||
os.write(bytes);
|
os.write(bytes);
|
||||||
}
|
}
|
||||||
assert doc != null;
|
assert doc != null;
|
||||||
resolveWithDocument(doc, promise, uri.toString());
|
resolveWithDocument(doc, uri.toString(), promise);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
promise.reject("ERROR", e.getLocalizedMessage());
|
promise.reject("ERROR", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@ -239,7 +269,7 @@ public class DocumentHelper {
|
|||||||
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
promise.reject("ERROR", "Cannot get current activity, so cannot launch document picker");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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);
|
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 {
|
try {
|
||||||
DocumentFile fileOrFolder = goToDocument(uriString, false);
|
DocumentFile fileOrFolder = goToDocument(uriString, false);
|
||||||
if (shouldBeFile) {
|
if (shouldBeFile) {
|
||||||
return !fileOrFolder.isDirectory();
|
return !fileOrFolder.isDirectory();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,12 +339,8 @@ public class DocumentHelper {
|
|||||||
&& permission.isWritePermission();
|
&& permission.isWritePermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPermissionErrorMsg(final String uriString) {
|
|
||||||
return "You don't have read/write permission to access uri: " + uriString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WritableMap resolveWithDocument(
|
public static WritableMap resolveWithDocument(
|
||||||
@NonNull DocumentFile file, Promise promise, String SimplifiedUri) {
|
@NonNull DocumentFile file, String SimplifiedUri, Promise promise) {
|
||||||
WritableMap fileMap = Arguments.createMap();
|
WritableMap fileMap = Arguments.createMap();
|
||||||
fileMap.putString("uri", UriHelper.denormalize(SimplifiedUri));
|
fileMap.putString("uri", UriHelper.denormalize(SimplifiedUri));
|
||||||
fileMap.putString("name", file.getName());
|
fileMap.putString("name", file.getName());
|
||||||
@ -362,13 +389,38 @@ public class DocumentHelper {
|
|||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Invalid file name: Could not extract filename from uri string provided");
|
"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 =
|
DocumentFile createdFile =
|
||||||
parentDirOfFile.createFile(
|
parentDirOfFile.createFile(
|
||||||
mimeType != null && !mimeType.equals("") ? mimeType : "*/*", fileName);
|
mimeType != null && !mimeType.equals("") ? mimeType : "*/*", correctFileName);
|
||||||
if (createdFile == null) {
|
if (createdFile == null) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
"File creation failed without any specific error for '" + fileName + "'");
|
"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;
|
return createdFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,19 +432,19 @@ public class DocumentHelper {
|
|||||||
public DocumentFile goToDocument(
|
public DocumentFile goToDocument(
|
||||||
String unknownUriStr, boolean createIfDirectoryNotExist, boolean includeLastSegment)
|
String unknownUriStr, boolean createIfDirectoryNotExist, boolean includeLastSegment)
|
||||||
throws SecurityException, IOException, IllegalArgumentException {
|
throws SecurityException, IOException, IllegalArgumentException {
|
||||||
String unknownUriString = UriHelper.getUnifiedUri(unknownUriStr);
|
String unknownUriString = UriHelper.getUnifiedUri(unknownUriStr);
|
||||||
if (unknownUriString.startsWith(ContentResolver.SCHEME_FILE)) {
|
if (unknownUriString.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||||
Uri uri = Uri.parse(unknownUriString);
|
Uri uri = Uri.parse(unknownUriString);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
throw new IllegalArgumentException("Invalid Uri String");
|
throw new IllegalArgumentException("Invalid Uri String");
|
||||||
}
|
}
|
||||||
String path =
|
String path =
|
||||||
uri.getPath()
|
uri.getPath()
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
includeLastSegment
|
includeLastSegment
|
||||||
? uri.getPath().length()
|
? uri.getPath().length()
|
||||||
: uri.getPath().length() - uri.getLastPathSegment().length());
|
: uri.getPath().length() - uri.getLastPathSegment().length());
|
||||||
|
|
||||||
if (createIfDirectoryNotExist) {
|
if (createIfDirectoryNotExist) {
|
||||||
File targetFile = new File(path);
|
File targetFile = new File(path);
|
||||||
@ -406,40 +458,47 @@ public class DocumentHelper {
|
|||||||
DocumentFile targetFile = DocumentFile.fromFile(new File(path));
|
DocumentFile targetFile = DocumentFile.fromFile(new File(path));
|
||||||
if (!targetFile.exists()) {
|
if (!targetFile.exists()) {
|
||||||
throw new FileNotFoundException(
|
throw new FileNotFoundException(
|
||||||
"Cannot find the given document. File does not exist at '" + unknownUriString + "'");
|
"Cannot find the given document. File does not exist at '" + unknownUriString + "'");
|
||||||
}
|
}
|
||||||
return targetFile;
|
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 uriString = UriHelper.normalize(unknownUriString);
|
||||||
String baseUri = "";
|
String baseUri = "";
|
||||||
String appendUri;
|
String appendUri;
|
||||||
String[] strings = new String[0];
|
String[] strings = new String[0];
|
||||||
|
|
||||||
List<UriPermission> uriList = context.getContentResolver().getPersistedUriPermissions();
|
{
|
||||||
|
// Helps traversal and folder creation by knowing where to start traverse
|
||||||
for (UriPermission uriPermission : uriList) {
|
List<UriPermission> uriList = context.getContentResolver().getPersistedUriPermissions();
|
||||||
String uriPath = uriPermission.getUri().toString();
|
for (UriPermission uriPermission : uriList) {
|
||||||
if (this.permissionMatchesAndHasAccess(uriPermission, uriString)) {
|
String uriPath = uriPermission.getUri().toString();
|
||||||
baseUri = uriPath;
|
if (this.permissionMatchesAndHasAccess(uriPermission, uriString)) {
|
||||||
appendUri = Uri.decode(uriString.substring(uriPath.length()));
|
baseUri = uriPath;
|
||||||
strings = appendUri.split("/");
|
appendUri = Uri.decode(uriString.substring(uriPath.length()));
|
||||||
break;
|
strings = appendUri.split("/");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUri.equals("")) {
|
if (baseUri.equals("")) {
|
||||||
throw new SecurityException(getPermissionErrorMsg(uriString));
|
// It's possible that the file access is temporary
|
||||||
}
|
baseUri = 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 + "'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uri = Uri.parse(baseUri);
|
Uri uri = Uri.parse(baseUri);
|
||||||
@ -479,7 +538,14 @@ public class DocumentHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert dir != null;
|
assert dir != null;
|
||||||
|
|
||||||
|
if (!dir.canRead() || !dir.canWrite()) {
|
||||||
|
throw new SecurityException(
|
||||||
|
"You don't have read/write permission to access uri: " + uriString);
|
||||||
|
}
|
||||||
|
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -517,7 +583,7 @@ public class DocumentHelper {
|
|||||||
srcDoc.delete();
|
srcDoc.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.resolve(resolveWithDocument(destDoc, promise, destUri));
|
promise.resolve(resolveWithDocument(destDoc, destUri, promise));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,28 @@ 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.VERSION_CODES;
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.Q)
|
@RequiresApi(api = VERSION_CODES.Q)
|
||||||
public class UriHelper {
|
public class UriHelper {
|
||||||
public static final String CONTENT_URI_PREFIX = "content://";
|
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) {
|
public static String getLastSegment(String uriString) {
|
||||||
|
|
||||||
return Uri.parse(Uri.decode(uriString)).getLastPathSegment();
|
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) {
|
public static String normalize(String uriString) {
|
||||||
if (uriString.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
if (isContentDocumentTreeUri(uriString)) {
|
||||||
// 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:
|
||||||
@ -29,7 +38,7 @@ public class UriHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String denormalize(String uriString) {
|
public static String denormalize(String uriString) {
|
||||||
if (uriString.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
if (isContentDocumentTreeUri(uriString)) {
|
||||||
// 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:
|
||||||
@ -45,14 +54,9 @@ public class UriHelper {
|
|||||||
if (uri.getScheme() == null) {
|
if (uri.getScheme() == null) {
|
||||||
uri = Uri.parse(ContentResolver.SCHEME_FILE + "://" + uriString);
|
uri = Uri.parse(ContentResolver.SCHEME_FILE + "://" + uriString);
|
||||||
} else if (!(uri.getScheme().equals(ContentResolver.SCHEME_FILE)
|
} else if (!(uri.getScheme().equals(ContentResolver.SCHEME_FILE)
|
||||||
|| uri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
|
|| uri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
|
||||||
throw new IllegalArgumentException("Invalid Uri: Scheme not supported");
|
throw new IllegalArgumentException("Invalid Uri: Scheme not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri.getScheme() == null) {
|
|
||||||
throw new IllegalArgumentException("Invalid Uri: Cannot determine scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri.toString();
|
return uri.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,10 @@ export type Encoding = 'utf8' | 'base64' | 'ascii';
|
|||||||
/** Native interface of the module */
|
/** Native interface of the module */
|
||||||
interface SafxInterface {
|
interface SafxInterface {
|
||||||
openDocumentTree(persist: boolean): Promise<DocumentFileDetail | null>;
|
openDocumentTree(persist: boolean): Promise<DocumentFileDetail | null>;
|
||||||
openDocument(persist: boolean): Promise<DocumentFileDetail | null>;
|
openDocument(
|
||||||
|
persist: boolean,
|
||||||
|
multiple: boolean,
|
||||||
|
): Promise<DocumentFileDetail[] | null>;
|
||||||
createDocument(
|
createDocument(
|
||||||
data: String,
|
data: String,
|
||||||
encoding?: String,
|
encoding?: String,
|
||||||
@ -97,12 +100,21 @@ export function openDocumentTree(persist: boolean) {
|
|||||||
return SafX.openDocumentTree(persist);
|
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.
|
* 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) {
|
export function openDocument(options: OpenDocumentOptions) {
|
||||||
return SafX.openDocument(persist);
|
const { persist = false, multiple = false } = options;
|
||||||
|
return SafX.openDocument(persist, multiple);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,9 +175,10 @@ export function createFile(
|
|||||||
return SafX.createFile(uriString, mimeType);
|
return SafX.createFile(uriString, mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
/**
|
||||||
// Removes the file or directory at given uri.
|
* Removes the file or directory at given uri.
|
||||||
// Resolves with `true` if delete is successful, `false` otherwise.
|
* Resolves with `true` if delete is successful, throws otherwise.
|
||||||
|
*/
|
||||||
export function unlink(uriString: string) {
|
export function unlink(uriString: string) {
|
||||||
return SafX.unlink(uriString);
|
return SafX.unlink(uriString);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user