diff --git a/packages/core/file-system/file-system-access.android.ts b/packages/core/file-system/file-system-access.android.ts index f9b048f9e..8b96c6358 100644 --- a/packages/core/file-system/file-system-access.android.ts +++ b/packages/core/file-system/file-system-access.android.ts @@ -254,6 +254,44 @@ export class FileSystemAccess implements IFileSystemAccess { return this.getLogicalRootPath() + '/app'; } + public readBuffer = this.readBufferSync.bind(this); + + public readBufferAsync(path: string): Promise { + return new Promise((resolve, reject) => { + try { + org.nativescript.widgets.Async.File.readBuffer( + path, + new org.nativescript.widgets.Async.CompleteCallback({ + onComplete: (result: java.nio.ByteBuffer) => { + resolve((ArrayBuffer as any).from(result)); + }, + onError: (err) => { + reject(new Error(err)); + }, + }), + null + ); + } catch (ex) { + reject(ex); + } + }); + } + + public readBufferSync(path: string, onError?: (error: any) => any) { + try { + const javaFile = new java.io.File(path); + const stream = new java.io.FileInputStream(javaFile); + const channel = stream.getChannel(); + const buffer = new ArrayBuffer(javaFile.length()); + channel.read(buffer as any); + return buffer; + } catch (exception) { + if (onError) { + onError(exception); + } + } + } + public read = this.readSync.bind(this); public readAsync(path: string): Promise { @@ -293,6 +331,52 @@ export class FileSystemAccess implements IFileSystemAccess { } } + static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): any { + if (buffer instanceof ArrayBuffer) { + return (buffer as any).nativeObject || buffer; + } else { + return (buffer?.buffer as any)?.nativeObject || buffer; + } + } + + public writeBuffer = this.writeBufferSync.bind(this); + + public writeBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise { + return new Promise((resolve, reject) => { + try { + org.nativescript.widgets.Async.File.writeBuffer( + path, + FileSystemAccess.getBuffer(buffer), + new org.nativescript.widgets.Async.CompleteCallback({ + onComplete: () => { + resolve(); + }, + onError: (err) => { + reject(new Error(err)); + }, + }), + null + ); + } catch (ex) { + reject(ex); + } + }); + } + + public writeBufferSync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) { + try { + const javaFile = new java.io.File(path); + const stream = new java.io.FileOutputStream(javaFile); + const channel = stream.getChannel(); + channel.write(FileSystemAccess.getBuffer(buffer)); + stream.close(); + } catch (exception) { + if (onError) { + onError(exception); + } + } + } + public write = this.writeSync.bind(this); public writeAsync(path: string, bytes: androidNative.Array): Promise { @@ -757,6 +841,7 @@ export class FileSystemAccess29 extends FileSystemAccess { getCurrentAppPath(): string { return super.getCurrentAppPath(); } + public readText = this.readTextSync.bind(this); readTextAsync(path: string, encoding?: any): Promise { @@ -795,6 +880,47 @@ export class FileSystemAccess29 extends FileSystemAccess { } } + readBuffer = this.readBufferSync.bind(this); + + readBufferAsync(path: string): Promise { + if (isContentUri(path)) { + return new Promise((resolve, reject) => { + getOrSetHelper(path).readBuffer( + applicationContext, + new org.nativescript.widgets.FileHelper.Callback({ + onSuccess(result) { + resolve(result); + }, + onError(error) { + reject(error); + }, + }) + ); + }); + } + return super.readBufferAsync(path); + } + + readBufferSync(path: string, onError?: (error: any) => any) { + if (isContentUri(path)) { + let callback = null; + if (typeof onError === 'function') { + callback = new org.nativescript.widgets.FileHelper.Callback({ + onSuccess(result) {}, + onError(error) { + onError(error); + }, + }); + } + const ret = getOrSetHelper(path).readBufferSync(applicationContext, callback); + if (ret) { + return null; + } + return (ArrayBuffer as any).from(ret); + } + return super.readBufferSync(path, onError); + } + read = this.readSync.bind(this); readAsync(path: string): Promise { @@ -872,6 +998,45 @@ export class FileSystemAccess29 extends FileSystemAccess { } } + writeBuffer = this.writeBufferSync.bind(this); + + writeBufferAsync(path: string, content: any): Promise { + if (isContentUri(path)) { + return new Promise((resolve, reject) => { + getOrSetHelper(path).writeBuffer( + applicationContext, + FileSystemAccess.getBuffer(content), + new org.nativescript.widgets.FileHelper.Callback({ + onSuccess(result) { + resolve(); + }, + onError(error) { + reject(error); + }, + }) + ); + }); + } + return super.writeAsync(path, content); + } + + writeBufferSync(path: string, content: any, onError?: (error: any) => any) { + if (isContentUri(path)) { + let callback = null; + if (typeof onError === 'function') { + callback = new org.nativescript.widgets.FileHelper.Callback({ + onSuccess(result) {}, + onError(error) { + onError(error); + }, + }); + } + getOrSetHelper(path).writeSync(applicationContext, FileSystemAccess.getBuffer(content), callback); + } else { + super.writeSync(path, content, onError); + } + } + write = this.writeSync.bind(this); writeAsync(path: string, content: any): Promise { diff --git a/packages/core/file-system/file-system-access.d.ts b/packages/core/file-system/file-system-access.d.ts index c2877254a..cfd6f74e5 100644 --- a/packages/core/file-system/file-system-access.d.ts +++ b/packages/core/file-system/file-system-access.d.ts @@ -298,6 +298,12 @@ export class FileSystemAccess implements IFileSystemAccess { readTextSync(path: string, onError?: (error: any) => any, encoding?: any): string; + readBuffer(path: string, onError?: (error: any) => any): ArrayBuffer; + + readBufferAsync(path: string): Promise; + + readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer; + read(path: string, onError?: (error: any) => any): any; readAsync(path: string): Promise; @@ -310,6 +316,12 @@ export class FileSystemAccess implements IFileSystemAccess { writeTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any); + writeBuffer(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any); + + writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise; + + writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any); + write(path: string, content: any, onError?: (error: any) => any); writeAsync(path: string, content: any): Promise; diff --git a/packages/core/file-system/file-system-access.ios.ts b/packages/core/file-system/file-system-access.ios.ts index c7b0cc0e7..952a162b3 100644 --- a/packages/core/file-system/file-system-access.ios.ts +++ b/packages/core/file-system/file-system-access.ios.ts @@ -294,6 +294,30 @@ export class FileSystemAccess { } } + public readBuffer = this.readBufferSync.bind(this); + + public readBufferAsync(path: string): Promise { + return new Promise((resolve, reject) => { + try { + (NSData as any).dataWithContentsOfFileCompletion(path, (data) => { + resolve(interop.bufferFromData(data)); + }); + } catch (ex) { + reject(new Error("Failed to read file at path '" + path + "': " + ex)); + } + }); + } + + public readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer { + try { + return interop.bufferFromData(NSData.dataWithContentsOfFile(path)); + } catch (ex) { + if (onError) { + onError(new Error("Failed to read file at path '" + path + "': " + ex)); + } + } + } + public read = this.readSync.bind(this); public readAsync(path: string): Promise { @@ -352,6 +376,40 @@ export class FileSystemAccess { } } + static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): NSData { + if (buffer instanceof ArrayBuffer) { + return NSData.dataWithData(buffer as any); + } else { + const buf = NSData.dataWithData(buffer?.buffer as any); + const len = buffer.byteLength; + return NSData.dataWithBytesNoCopyLength((buf.bytes as interop.Pointer).add(buffer?.byteOffset ?? 0), len); + } + } + + public writeBuffer = this.writeBufferSync.bind(this); + + public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise { + return new Promise((resolve, reject) => { + try { + FileSystemAccess.getBuffer(content).writeToFileAtomicallyCompletion(path, true, () => { + resolve(); + }); + } catch (ex) { + reject(new Error("Failed to write file at path '" + path + "': " + ex)); + } + }); + } + + public writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) { + try { + FileSystemAccess.getBuffer(content).writeToFileAtomically(path, true); + } catch (ex) { + if (onError) { + onError(new Error("Failed to write to file '" + path + "': " + ex)); + } + } + } + public write = this.writeSync.bind(this); public writeAsync(path: string, content: NSData): Promise { diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar index 0be222d8e..3894686dc 100644 Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ diff --git a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts index deb1cef3c..71e57f5bd 100644 --- a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts +++ b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts @@ -31,8 +31,10 @@ export module File { export function readText(path: string, encoding: string, callback: CompleteCallback, context: any); export function read(path: string, callback: CompleteCallback, context: any); + export function readBuffer(param0: string, param1: org.nativescript.widgets.Async.CompleteCallback, param2: any): void; export function writeText(path: string, content: string, encoding: string, callback: CompleteCallback, context: any); export function write(path: string, content: androidNative.Array, callback: CompleteCallback, context: any); + export function writeBuffer(param0: string, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.Async.CompleteCallback, param3: any): void; } export module Http { @@ -641,27 +643,31 @@ declare module org { export class FileHelper { public static class: java.lang.Class; public readText(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void; - public writeSync(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void; - public static fromString(param1: globalAndroid.content.Context, param0: string): org.nativescript.widgets.FileHelper; - public writeText(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void; + public writeBufferSync(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void; public writeTextSync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void; public copyToFileSync(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): boolean; - public getName(): string; public read(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): void; - public copyToFile(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): void; - public static fromUri(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): org.nativescript.widgets.FileHelper; + public renameSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void; public readSync(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): androidNative.Array; - public write(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void; + public static fromString(param0: globalAndroid.content.Context, param1: string): org.nativescript.widgets.FileHelper; public getSize(): number; public getMime(): string; - public readTextSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string; + public static exists(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): boolean; public delete(param0: globalAndroid.content.Context): boolean; - public static exists(param0: globalAndroid.content.Context, param1: string): boolean; - public static exists(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): boolean; - public getExtension(): string; - public getLastModified(): number; - public renameSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string; - public rename(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string; + public writeSync(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void; + public writeText(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void; + public readBuffer(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): void; + public getName(): string; + public rename(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void; + public writeBuffer(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void; + public copyToFile(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): void; + public readBufferSync(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): java.nio.ByteBuffer; + public write(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void; + public getExtension(): string; + public readTextSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string; + public static fromUri(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): org.nativescript.widgets.FileHelper; + public static exists(param0: globalAndroid.content.Context, param1: string): boolean; + public getLastModified(): number; } export module FileHelper { export class Callback { diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Async.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Async.java index e0e8d1cb2..e04fc181e 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Async.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Async.java @@ -26,7 +26,9 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.ByteBuffer; import java.nio.CharBuffer; +import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -627,6 +629,23 @@ public class Async { }); } + public static void readBuffer(final String path, final CompleteCallback callback, final Object context) { + final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper()); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final ReadBufferTask task = new ReadBufferTask(callback, context); + final ByteBuffer result = task.doInBackground(path); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + public static void writeText(final String path, final String content, final String encoding, final CompleteCallback callback, final Object context) { final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper()); threadPoolExecutor().execute(new Runnable() { @@ -661,6 +680,23 @@ public class Async { }); } + public static void writeBuffer(final String path, final ByteBuffer content, final CompleteCallback callback, final Object context) { + final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper()); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final WriteBufferTask task = new WriteBufferTask(callback, context); + final boolean result = task.doInBackground(path, content); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + static class ReadTextTask { private final CompleteCallback callback; private final Object context; @@ -768,6 +804,55 @@ public class Async { } } + static class ReadBufferTask { + private final CompleteCallback callback; + private final Object context; + + public ReadBufferTask(CompleteCallback callback, Object context) { + this.callback = callback; + this.context = context; + } + + protected ByteBuffer doInBackground(String... params) { + java.io.File javaFile = new java.io.File(params[0]); + FileInputStream stream = null; + + try { + stream = new FileInputStream(javaFile); + + ByteBuffer buffer = ByteBuffer.allocateDirect((int) javaFile.length()); + + FileChannel channel = stream.getChannel(); + channel.read(buffer); + buffer.rewind(); + + return buffer; + } catch (FileNotFoundException e) { + Log.e(TAG, "Failed to read file, FileNotFoundException: " + e.getMessage()); + return null; + } catch (IOException e) { + Log.e(TAG, "Failed to read file, IOException: " + e.getMessage()); + return null; + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage()); + } + } + } + } + + protected void onPostExecute(final ByteBuffer result) { + if (result != null) { + this.callback.onComplete(result, this.context); + } else { + this.callback.onError("ReadTask returns no result.", this.context); + } + } + } + static class WriteTextTask { private final CompleteCallback callback; private final Object context; @@ -784,7 +869,6 @@ public class Async { stream = new FileOutputStream(javaFile); OutputStreamWriter writer = new OutputStreamWriter(stream, params[2]); - writer.write(params[1]); writer.close(); @@ -863,5 +947,52 @@ public class Async { } } + static class WriteBufferTask { + private final CompleteCallback callback; + private final Object context; + + public WriteBufferTask(CompleteCallback callback, Object context) { + this.callback = callback; + this.context = context; + } + + protected boolean doInBackground(Object... params) { + java.io.File javaFile = new java.io.File((String) params[0]); + FileOutputStream stream = null; + ByteBuffer content = (ByteBuffer) params[1]; + + try { + stream = new FileOutputStream(javaFile); + FileChannel channel = stream.getChannel(); + content.rewind(); + channel.write(content); + content.rewind(); + return true; + } catch (FileNotFoundException e) { + Log.e(TAG, "Failed to write file, FileNotFoundException: " + e.getMessage()); + return false; + } catch (IOException e) { + Log.e(TAG, "Failed to write file, IOException: " + e.getMessage()); + return false; + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage()); + } + } + } + } + + protected void onPostExecute(final boolean result) { + if (result) { + this.callback.onComplete(null, this.context); + } else { + this.callback.onError("WriteTask returns no result.", this.context); + } + } + } + } } diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java index 706174d83..67d2b254b 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java @@ -24,6 +24,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -302,8 +307,7 @@ public class FileHelper { } return context.getContentResolver().openInputStream(uri); } - - + private OutputStream getOutputStream(Context context, Uri uri) throws Exception { if (Build.VERSION.SDK_INT >= 19) { if (isExternalStorageDocument(uri)) { @@ -332,6 +336,16 @@ public class FileHelper { return ret.buf(); } + private ByteBuffer readBufferSyncInternal(Context context) throws Exception { + InputStream is = getInputStream(context, uri); + + ReadableByteChannel channel = Channels.newChannel(is); + ByteBuffer buffer = ByteBuffer.allocateDirect(is.available()); + channel.read(buffer); + + return buffer; + } + public @Nullable byte[] readSync(Context context, @Nullable Callback callback) { try { @@ -355,6 +369,29 @@ public class FileHelper { }); } + public @Nullable + ByteBuffer readBufferSync(Context context, @Nullable Callback callback) { + try { + return readBufferSyncInternal(context); + } catch (Exception e) { + if (callback != null) { + callback.onError(e); + } + } + return null; + } + + public void readBuffer(Context context, Callback callback) { + executor.execute(() -> { + try { + ByteBuffer result = readBufferSyncInternal(context); + handler.post(() -> callback.onSuccess(result)); + } catch (Exception e) { + handler.post(() -> callback.onError(e)); + } + }); + } + private String readTextSyncInternal(Context context, @Nullable String encoding) throws Exception { String characterSet = encoding; if (characterSet == null) { @@ -400,6 +437,15 @@ public class FileHelper { updateInternal(context); } + private void writeBufferSyncInternal(Context context,ByteBuffer content) throws Exception { + OutputStream os = getOutputStream(context, uri); + WritableByteChannel channel = Channels.newChannel(os); + channel.write(content); + os.flush(); + os.close(); + updateInternal(context); + } + public void writeSync(Context context, byte[] content, @Nullable Callback callback) { try { writeSyncInternal(context, content); @@ -421,6 +467,27 @@ public class FileHelper { }); } + public void writeBufferSync(Context context, ByteBuffer content, @Nullable Callback callback) { + try { + writeBufferSyncInternal(context, content); + } catch (Exception e) { + if (callback != null) { + callback.onError(e); + } + } + } + + public void writeBuffer(Context context, ByteBuffer content, Callback callback) { + executor.execute(() -> { + try { + writeBufferSyncInternal(context, content); + handler.post(() -> callback.onSuccess(null)); + } catch (Exception e) { + handler.post(() -> callback.onError(e)); + } + }); + } + private void writeTextSyncInternal(Context context, String content, @Nullable String encoding) throws Exception { OutputStream os = getOutputStream(context, uri); String characterSet = encoding;