diff --git a/apps/automated/src/file-system/file-system-tests.ts b/apps/automated/src/file-system/file-system-tests.ts
index 44dcd3658..d6a30e99b 100644
--- a/apps/automated/src/file-system/file-system-tests.ts
+++ b/apps/automated/src/file-system/file-system-tests.ts
@@ -5,7 +5,7 @@ import * as fs from '@nativescript/core/file-system';
import * as TKUnit from '../tk-unit';
import * as appModule from '@nativescript/core/application';
-import { isIOS, Device, platformNames } from '@nativescript/core';
+import { isIOS, Device, platformNames, isAndroid } from '@nativescript/core';
export var testPathNormalize = function () {
// >> file-system-normalize
@@ -719,3 +719,62 @@ export function test_FileCopy(done) {
.then(() => done())
.catch(done);
}
+
+export function testAndroidCreate() {
+ let testFunc = function testFunc() {
+ const file = fs.File.android.createFile({
+ directory: fs.AndroidDirectory.DOWNLOADS,
+ name: `${Date.now()}.txt`,
+ mime: 'text/plain',
+ relativePath: `NativeScript`,
+ });
+
+ file.writeTextSync('some text');
+
+ return file;
+ };
+ if (isAndroid) {
+ const file = testFunc();
+ TKUnit.assertEqual(file.readTextSync(), 'some text', `The contents of the new file created in the 'AndroidDirectory.DOWNLOADS' folder are not as expected.`);
+ file.removeSync();
+ TKUnit.assertTrue(!fs.File.exists(file.path));
+ } else {
+ TKUnit.assertThrows(testFunc, `Trying to retrieve createFile on a platform different from Android should throw!`, `createFile is available on Android only!`);
+ }
+}
+
+export function test_FileAppend(done) {
+ const content = 'Hello World';
+ const hello_world = global.isIOS ? NSString.stringWithString(content).dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String(content).getBytes('UTF-8');
+ const file = fs.File.fromPath(fs.path.join(fs.knownFolders.temp().path, `${Date.now()}-app.txt`));
+ file
+ .appendText('Hello')
+ .then(() => file.appendText(' World'))
+ .then(() => {
+ TKUnit.assert(file.size === hello_world.length);
+ return file.readText();
+ })
+ .then((value) => {
+ TKUnit.assert(value === content);
+
+ return Promise.allSettled([file.remove()]);
+ })
+ .then(() => done())
+ .catch(done);
+}
+
+export function test_FileAppendText(done) {
+ const content = 'Hello World';
+ const file = fs.File.fromPath(fs.path.join(fs.knownFolders.temp().path, `${Date.now()}-app.txt`));
+ file
+ .appendText('Hello')
+ .then(() => file.appendText(' World'))
+ .then(() => file.readText())
+ .then((value) => {
+ TKUnit.assert(value === content);
+
+ return Promise.allSettled([file.remove()]);
+ })
+ .then(() => done())
+ .catch(done);
+}
diff --git a/apps/toolbox/src/pages/fs-helper.ts b/apps/toolbox/src/pages/fs-helper.ts
index 0c66d8e04..fd5c56e34 100644
--- a/apps/toolbox/src/pages/fs-helper.ts
+++ b/apps/toolbox/src/pages/fs-helper.ts
@@ -1,4 +1,4 @@
-import { Page, EventData, Application, File, Folder, knownFolders, path, getFileAccess, Utils } from '@nativescript/core';
+import { Page, EventData, Application, File, Folder, knownFolders, path, getFileAccess, Utils, Screen, Http, AndroidDirectory, ImageSource, alert } from '@nativescript/core';
let page: Page;
@@ -217,3 +217,103 @@ function getFileNameFromContent(content: string) {
const file = getFileAccess().getFile(content);
return decodeURIComponent(file.name).split('/').pop().toLowerCase();
}
+
+let lastDownload: File;
+export function createFileInDownloads() {
+ if (!global.isAndroid) {
+ return;
+ }
+ const width = Screen.mainScreen.widthPixels;
+ const height = Screen.mainScreen.heightPixels;
+ const randomImageUrl = `https://picsum.photos/${width}/${height}.jpg?random=${Date.now()}`;
+
+ Http.getFile(randomImageUrl).then((result: File) => {
+ let file = File.android.createFile({
+ directory: AndroidDirectory.DOWNLOADS,
+ name: `${Date.now()}.jpg`,
+ mime: 'image/jpeg',
+ relativePath: `NativeScript`,
+ });
+ result
+ .copy(file.path)
+ .then((done) => {
+ lastDownload = file;
+ console.log('done: ' + done + '\n' + 'Original path: ' + result.path + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
+ alert(`File saved in ${AndroidDirectory.DOWNLOADS}/NativeScript`);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+}
+
+export function createFileInGallery() {
+ if (!global.isAndroid) {
+ return;
+ }
+ const width = Screen.mainScreen.widthPixels;
+ const height = Screen.mainScreen.heightPixels;
+ const randomImageUrl = `https://picsum.photos/${width}/${height}.jpg?random=${Date.now()}`;
+
+ Http.getFile(randomImageUrl).then((result: File) => {
+ let file = File.android.createFile({
+ directory: AndroidDirectory.PICTURES,
+ name: `${Date.now()}.jpg`,
+ mime: 'image/jpeg',
+ relativePath: `NativeScript`,
+ });
+ result
+ .copy(file.path)
+ .then((done) => {
+ console.log('done: ' + done + '\n' + 'Original path: ' + result + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
+ alert(`File saved in ${AndroidDirectory.PICTURES}/NativeScript`);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+}
+
+export function createFileInMusic() {
+ if (!global.isAndroid) {
+ return;
+ }
+
+ Http.getFile('https://github.com/rafaelreis-hotmart/Audio-Sample-files/raw/master/sample.mp3').then((result: File) => {
+ let file = File.android.createFile({
+ directory: AndroidDirectory.MUSIC,
+ name: `${Date.now()}.MP3`,
+ mime: 'audio/mp3',
+ relativePath: `NativeScript/MP3`,
+ });
+
+ result
+ .copy(file.path)
+ .then((done) => {
+ console.log('done: ' + done + '\n' + 'Original path: ' + result + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
+
+ alert(`File saved in ${AndroidDirectory.MUSIC}/NativeScript/MP3`);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+}
+
+export function createAndAppendText() {
+ const file = File.fromPath(path.join(knownFolders.temp().path, `${Date.now()}-app.txt`));
+ file.appendTextSync('Hello');
+ file.appendTextSync(' World');
+ const data = file.readTextSync();
+ console.log('createAndAppend:', data === 'Hello World');
+}
+
+export function createAndAppendData() {
+ const file = File.fromPath(path.join(knownFolders.temp().path, `${Date.now()}-app.txt`));
+ const hello = global.isIOS ? NSString.stringWithString('Hello').dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String('Hello').getBytes('UTF-8');
+ const world = global.isIOS ? NSString.stringWithString(' World').dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String(' World').getBytes('UTF-8');
+ file.appendSync(hello);
+ file.appendSync(world);
+ const data = file.readTextSync();
+ console.log('createAndAppendData:', data === 'Hello World');
+}
diff --git a/apps/toolbox/src/pages/fs-helper.xml b/apps/toolbox/src/pages/fs-helper.xml
index 7b0970cd1..6b78e36eb 100644
--- a/apps/toolbox/src/pages/fs-helper.xml
+++ b/apps/toolbox/src/pages/fs-helper.xml
@@ -5,5 +5,10 @@
+
+
+
+
+
diff --git a/packages/core/file-system/file-system-access.android.ts b/packages/core/file-system/file-system-access.android.ts
index a5f2d8e63..979e09283 100644
--- a/packages/core/file-system/file-system-access.android.ts
+++ b/packages/core/file-system/file-system-access.android.ts
@@ -375,6 +375,81 @@ export class FileSystemAccess implements IFileSystemAccess {
}
}
+ public appendBuffer = this.appendBufferSync.bind(this);
+
+ public appendBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ org.nativescript.widgets.Async.File.appendBuffer(
+ path,
+ FileSystemAccess.getBuffer(buffer),
+ new org.nativescript.widgets.Async.CompleteCallback({
+ onComplete: () => {
+ resolve();
+ },
+ onError: (err) => {
+ reject(new Error(err));
+ },
+ }),
+ null
+ );
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ public appendBufferSync(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 append = this.appendSync.bind(this);
+
+ public appendAsync(path: string, bytes: androidNative.Array): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ org.nativescript.widgets.Async.File.append(
+ path,
+ bytes,
+ new org.nativescript.widgets.Async.CompleteCallback({
+ onComplete: () => {
+ resolve();
+ },
+ onError: (err) => {
+ reject(new Error(err));
+ },
+ }),
+ null
+ );
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ public appendSync(path: string, bytes: androidNative.Array, onError?: (error: any) => any) {
+ try {
+ const javaFile = new java.io.File(path);
+ const stream = new java.io.FileOutputStream(javaFile, true);
+ stream.write(bytes, 0, bytes.length);
+ stream.close();
+ } catch (exception) {
+ if (onError) {
+ onError(exception);
+ }
+ }
+ }
+
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise {
@@ -538,6 +613,56 @@ export class FileSystemAccess implements IFileSystemAccess {
return s;
}
+ public appendText = this.appendTextSync.bind(this);
+
+ public appendTextAsync(path: string, content: string, encoding?: any): Promise {
+ let actualEncoding = encoding;
+ if (!actualEncoding) {
+ actualEncoding = textModule.encoding.UTF_8;
+ }
+
+ return new Promise((resolve, reject) => {
+ try {
+ org.nativescript.widgets.Async.File.appendText(
+ path,
+ content,
+ actualEncoding,
+ new org.nativescript.widgets.Async.CompleteCallback({
+ onComplete: () => {
+ resolve();
+ },
+ onError: (err) => {
+ reject(new Error(err));
+ },
+ }),
+ null
+ );
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ public appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
+ try {
+ const javaFile = new java.io.File(path);
+ const stream = new java.io.FileOutputStream(javaFile, true);
+
+ let actualEncoding = encoding;
+ if (!actualEncoding) {
+ actualEncoding = textModule.encoding.UTF_8;
+ }
+ const writer = new java.io.OutputStreamWriter(stream, actualEncoding);
+
+ writer.write(content);
+ writer.close();
+ } catch (exception) {
+ if (onError) {
+ onError(exception);
+ }
+ }
+ }
+
public writeText = this.writeTextSync.bind(this);
public writeTextAsync(path: string, content: string, encoding?: any): Promise {
@@ -879,6 +1004,124 @@ export class FileSystemAccess29 extends FileSystemAccess {
return super.getCurrentAppPath();
}
+ appendBuffer = this.appendBufferSync.bind(this);
+
+ appendBufferAsync(path: string, content: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).appendBuffer(
+ applicationContext,
+ FileSystemAccess.getBuffer(content),
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.appendAsync(path, content);
+ }
+
+ appendBufferSync(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).appendSync(applicationContext, FileSystemAccess.getBuffer(content), callback);
+ } else {
+ super.appendSync(path, content, onError);
+ }
+ }
+
+ append = this.appendSync.bind(this);
+
+ appendAsync(path: string, content: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).append(
+ applicationContext,
+ content,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.appendAsync(path, content);
+ }
+
+ appendSync(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).appendSync(applicationContext, content, callback);
+ } else {
+ super.appendSync(path, content, onError);
+ }
+ }
+
+ appendText = this.appendTextSync.bind(this);
+
+ appendTextAsync(path: string, content: string, encoding?: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).appendText(
+ applicationContext,
+ content,
+ encoding ?? null,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.appendTextAsync(path, content, encoding);
+ }
+
+ appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: 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).appendTextSync(applicationContext, content, encoding ?? null, callback);
+ } else {
+ super.appendTextSync(path, content, onError);
+ }
+ }
+
public readText = this.readTextSync.bind(this);
readTextAsync(path: string, encoding?: 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 a6b9668da..cad617fcd 100644
--- a/packages/core/file-system/file-system-access.d.ts
+++ b/packages/core/file-system/file-system-access.d.ts
@@ -2,6 +2,55 @@
* An utility class used to provide methods to access and work with the file system.
*/
export interface IFileSystemAccess {
+ /**
+ * Appends binary to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ * @param onError (optional) A callback function to use if any error occurs.
+ */
+ append(path: string, content: any, onError?: (error: any) => any);
+
+ /**
+ * Appends binary to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ */
+ appendAsync(path: string, content: any): Promise;
+
+ /**
+ * Appends a binary to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ * @param onError (optional) A callback function to use if any error occurs.
+ */
+ appendSync(path: string, content: any, onError?: (error: any) => any);
+
+ /**
+ * Appends text to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ * @param onError (optional) A callback function to use if any error occurs.
+ * @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
+ */
+ appendText(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
+ /**
+ * Appends text to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ * @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
+ */
+ appendTextAsync(path: string, content: string, encoding?: any): Promise;
+
+ /**
+ * Appends text to a file with a given path.
+ * @param path The path to the source file.
+ * @param content The content which will be written to the file.
+ * @param onError (optional) A callback function to use if any error occurs.
+ * @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
+ */
+ appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
/**
* Copies a file to a given path.
* @param src The path to the source file.
@@ -282,6 +331,24 @@ export interface IFileSystemAccess {
}
export class FileSystemAccess implements IFileSystemAccess {
+ appendBuffer(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
+
+ appendBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise;
+
+ appendBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
+
+ append(path: string, content: any, onError?: (error: any) => any);
+
+ appendAsync(path: string, content: any): Promise;
+
+ appendSync(path: string, content: any, onError?: (error: any) => any);
+
+ appendText(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
+ appendTextAsync(path: string, content: string, encoding?: any): Promise;
+
+ appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
copy(src: string, dest: string, onError?: (error: any) => any): boolean;
copySync(src: string, dest: string, onError?: (error: any) => any): boolean;
diff --git a/packages/core/file-system/file-system-access.ios.ts b/packages/core/file-system/file-system-access.ios.ts
index 0258a8fbc..c3f6a5091 100644
--- a/packages/core/file-system/file-system-access.ios.ts
+++ b/packages/core/file-system/file-system-access.ios.ts
@@ -445,6 +445,115 @@ export class FileSystemAccess {
}
}
+ public appendBuffer = this.appendBufferSync.bind(this);
+
+ public appendBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ (handle as any).appendDataCompletion(FileSystemAccess.getBuffer(content), (error) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ handle.closeFile();
+ });
+ } catch (ex) {
+ reject(new Error("Failed to write file at path '" + path + "': " + ex));
+ }
+ });
+ }
+
+ public appendBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
+ try {
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ handle.seekToEndOfFile();
+ handle.writeData(FileSystemAccess.getBuffer(content));
+ handle.closeFile();
+ } catch (ex) {
+ if (onError) {
+ onError(new Error("Failed to write to file '" + path + "': " + ex));
+ }
+ }
+ }
+
+ public append = this.appendSync.bind(this);
+
+ public appendAsync(path: string, content: NSData): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ (handle as any).appendDataCompletion(content, (error) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ handle.closeFile();
+ });
+ } catch (ex) {
+ reject(new Error("Failed to write file at path '" + path + "': " + ex));
+ }
+ });
+ }
+
+ public appendSync(path: string, content: NSData, onError?: (error: any) => any) {
+ try {
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ handle.seekToEndOfFile();
+ handle.writeData(content);
+ handle.closeFile();
+ } catch (ex) {
+ if (onError) {
+ onError(new Error("Failed to write to file '" + path + "': " + ex));
+ }
+ }
+ }
+
+ public appendText = this.appendTextSync.bind(this);
+
+ public appendTextAsync(path: string, content: string, encoding?: any): Promise {
+ const nsString = NSString.stringWithString(content);
+ const actualEncoding = encoding || textEncoding.UTF_8;
+
+ return new Promise((resolve, reject) => {
+ try {
+ const data = nsString.dataUsingEncoding(actualEncoding);
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ (handle as any).appendDataCompletion(data, (error) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ handle.closeFile();
+ });
+ } catch (ex) {
+ reject(new Error("Failed to append file at path '" + path + "': " + ex));
+ }
+ });
+ }
+
+ public appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
+ const nsString = NSString.stringWithString(content);
+
+ const actualEncoding = encoding || textEncoding.UTF_8;
+
+ // TODO: verify the useAuxiliaryFile parameter should be false
+ try {
+ const data = nsString.dataUsingEncoding(actualEncoding);
+ const handle = NSFileHandle.fileHandleForWritingAtPath(path);
+ handle.seekToEndOfFile();
+ handle.writeData(data);
+ handle.closeFile();
+ } catch (ex) {
+ if (onError) {
+ onError(new Error("Failed to append to file '" + path + "': " + ex));
+ }
+ }
+ }
+
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise {
diff --git a/packages/core/file-system/index.d.ts b/packages/core/file-system/index.d.ts
index d010a2c0b..5917b96ed 100644
--- a/packages/core/file-system/index.d.ts
+++ b/packages/core/file-system/index.d.ts
@@ -1,5 +1,20 @@
import { FileSystemAccess } from './file-system-access';
+export enum AndroidDirectory {
+ ALARMS,
+ AUDIOBOOKS,
+ DCIM,
+ DOCUMENTS,
+ DOWNLOADS,
+ MOVIES,
+ MUSIC,
+ NOTIFICATIONS,
+ PICTURES,
+ PODCASTS,
+ RINGTONES,
+ SCREENSHOTS,
+}
+
/**
* Returns FileSystemAccess, a shared singleton utility class to provide methods to access and work with the file system. This is used under the hood of all the file system apis in @nativescript/core and provided as a lower level convenience if needed.
* @returns FileSystemAccess
@@ -55,10 +70,27 @@ export class FileSystemEntity {
renameSync(newName: string, onError?: (error: any) => any): void;
}
+/**
+ * Contains Android-specific the file system helpers.
+ */
+class Android {
+ createFile(options: { relativePath?: string; name: string; mime: string; directory: AndroidDirectory }): File;
+}
+
+/**
+ * Contains iOS-specific the file system helpers.
+ */
+
+class iOS {}
+
/**
* Represents a File entity on the file system.
*/
export class File extends FileSystemEntity {
+ static readonly android: Android;
+
+ static readonly ios: iOS;
+
/**
* Checks whether a File with the specified path already exists.
* @param path The path to check for.
@@ -80,6 +112,34 @@ export class File extends FileSystemEntity {
*/
isLocked: boolean;
+ /**
+ * Appends the provided string to the file, using the specified encoding (defaults to UTF-8).
+ * @param content The content to be saved to the file.
+ * @param encoding An optional value specifying the preferred encoding (defaults to UTF-8).
+ */
+ appendText(content: string, encoding?: string): Promise;
+
+ /**
+ * Appends the provided string to the file synchronously, using the specified encoding (defaults to UTF-8).
+ * @param content The content to be saved to the file.
+ * @param onError An optional function to be called if some IO-error occurs.
+ * @param encoding An optional value specifying the preferred encoding (defaults to UTF-8).
+ */
+ appendTextSync(content: string, onError?: (error: any) => any, encoding?: string): void;
+
+ /**
+ * Appends the provided binary content to the file.
+ * @param content The binary content to be saved to the file.
+ */
+ append(content: any): Promise;
+
+ /**
+ * Appends the provided binary content to the file synchronously.
+ * @param content The binary content to be saved to the file.
+ * @param onError An optional function to be called if some IO-error occurs.
+ */
+ appendSync(content: any, onError?: (error: any) => any): void;
+
/**
* Copies a file to a given path.
* @param dest The path to the destination file.
diff --git a/packages/core/file-system/index.ts b/packages/core/file-system/index.ts
index 4e5982552..79110ed6a 100644
--- a/packages/core/file-system/index.ts
+++ b/packages/core/file-system/index.ts
@@ -190,7 +190,136 @@ function getApplicationContext() {
return applicationContext;
}
+export enum AndroidDirectory {
+ ALARMS = 'alarms',
+ AUDIOBOOKS = 'audiobooks',
+ DCIM = 'dcim',
+ DOCUMENTS = 'documents',
+ DOWNLOADS = 'downloads',
+ MOVIES = 'movies',
+ MUSIC = 'music',
+ NOTIFICATIONS = 'notifications',
+ PICTURES = 'pictures',
+ PODCASTS = 'podcasts',
+ RINGTONES = 'ringtones',
+ SCREENSHOTS = 'screenshots',
+}
+
+function getAndroidDirectory(value: AndroidDirectory): { path: string; column: android.net.Uri } | null {
+ switch (value) {
+ case AndroidDirectory.ALARMS:
+ return {
+ path: android.os.Environment.DIRECTORY_ALARMS,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.AUDIOBOOKS:
+ return {
+ path: android.os.Environment.DIRECTORY_AUDIOBOOKS,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.DCIM:
+ return {
+ path: android.os.Environment.DIRECTORY_DCIM,
+ column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.DOCUMENTS:
+ return {
+ path: android.os.Environment.DIRECTORY_DOCUMENTS,
+ column: android.provider.MediaStore.Files.getContentUri('external'),
+ };
+ case AndroidDirectory.DOWNLOADS:
+ return {
+ path: android.os.Environment.DIRECTORY_DOWNLOADS,
+ column: android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.MOVIES:
+ return {
+ path: android.os.Environment.DIRECTORY_MOVIES,
+ column: android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.MUSIC:
+ return {
+ path: android.os.Environment.DIRECTORY_MUSIC,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.NOTIFICATIONS:
+ return {
+ path: android.os.Environment.DIRECTORY_NOTIFICATIONS,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.PICTURES:
+ return {
+ path: android.os.Environment.DIRECTORY_PICTURES,
+ column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.PODCASTS:
+ return {
+ path: android.os.Environment.DIRECTORY_PODCASTS,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.RINGTONES:
+ return {
+ path: android.os.Environment.DIRECTORY_RINGTONES,
+ column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ };
+ case AndroidDirectory.SCREENSHOTS:
+ return {
+ path: android.os.Environment.DIRECTORY_SCREENSHOTS,
+ column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ };
+ default:
+ return null;
+ }
+}
+
+class Android {
+ createFile(options: { relativePath?: string; name: string; mime: string; directory: AndroidDirectory }): File {
+ if (!global.isAndroid) {
+ throw new Error(`createFile is available on Android only!`);
+ }
+
+ const context = getApplicationContext() as android.content.Context;
+
+ const meta = new android.content.ContentValues();
+ meta.put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, options.name);
+ meta.put(android.provider.MediaStore.MediaColumns.MIME_TYPE, options.mime);
+ //meta.put(android.provider.MediaStore.MediaColumns.DATE_ADDED, java.lang.System.currentTimeMillis() as any);
+
+ const externalDirectory = getAndroidDirectory(options.directory);
+
+ if (SDK_VERSION >= 29) {
+ const relativePath = options?.relativePath ? `/${options.relativePath}` : '';
+ meta.put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, `${externalDirectory.path}${relativePath}`);
+ // todo
+ // meta.put(android.provider.MediaStore.MediaColumns.IS_PENDING, java.lang.Integer.valueOf(1));
+ } else {
+ const relativePath = options?.relativePath ? `${options.relativePath}/` : '';
+ const directory = android.os.Environment.getExternalStoragePublicDirectory(externalDirectory.path);
+ const file = new java.io.File(directory, `${relativePath}${options.name}`);
+ meta.put(android.provider.MediaStore.MediaColumns.DATA, file.getAbsolutePath());
+ }
+
+ const uri = context.getContentResolver().insert(externalDirectory.column, meta);
+
+ return File.fromPath(uri.toString());
+ }
+}
+
+const ad = new Android();
+
+class iOS {}
+
+const ios = new iOS();
+
export class File extends FileSystemEntity {
+ public static get ios() {
+ return ios;
+ }
+
+ public static get android() {
+ return ad;
+ }
+
public static fromPath(path: string, copy: boolean = false) {
const onError = function (error) {
throw error;
@@ -239,6 +368,100 @@ export class File extends FileSystemEntity {
return getFileAccess().getFileSize(this.path);
}
+ public append(content: any): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ this._checkAccess();
+ } catch (ex) {
+ reject(ex);
+
+ return;
+ }
+
+ this._locked = true;
+
+ getFileAccess()
+ .appendAsync(this.path, content)
+ .then(
+ () => {
+ resolve();
+ this._locked = false;
+ },
+ (error) => {
+ reject(error);
+ this._locked = false;
+ }
+ );
+ });
+ }
+
+ public appendSync(content: any, onError?: (error: any) => any): void {
+ this._checkAccess();
+
+ try {
+ this._locked = true;
+
+ const that = this;
+ const localError = function (error) {
+ that._locked = false;
+ if (onError) {
+ onError(error);
+ }
+ };
+
+ getFileAccess().appendSync(this.path, content, localError);
+ } finally {
+ this._locked = false;
+ }
+ }
+
+ public appendText(content: string, encoding?: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ this._checkAccess();
+ } catch (ex) {
+ reject(ex);
+
+ return;
+ }
+
+ this._locked = true;
+
+ getFileAccess()
+ .appendTextAsync(this.path, content, encoding)
+ .then(
+ () => {
+ resolve(true);
+ this._locked = false;
+ },
+ (error) => {
+ reject(error);
+ this._locked = false;
+ }
+ );
+ });
+ }
+
+ public appendTextSync(content: string, onError?: (error: any) => any, encoding?: string): void {
+ this._checkAccess();
+
+ try {
+ this._locked = true;
+
+ const that = this;
+ const localError = function (error) {
+ that._locked = false;
+ if (onError) {
+ onError(error);
+ }
+ };
+
+ getFileAccess().appendTextSync(this.path, content, localError, encoding);
+ } finally {
+ this._locked = false;
+ }
+ }
+
public copy(dest: string): Promise {
return new Promise((resolve, reject) => {
try {
diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts
index cb12b4a51..c25e9d2d7 100644
--- a/packages/core/index.d.ts
+++ b/packages/core/index.d.ts
@@ -85,7 +85,7 @@ export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './dat
export type { PropertyChangeData, EventData } from './data/observable';
export { VirtualArray } from './data/virtual-array';
export type { ItemsLoading } from './data/virtual-array';
-export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess } from './file-system';
+export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess, AndroidDirectory } from './file-system';
export type { HttpRequestOptions, HttpResponse, Headers, HttpResponseEncoding, HttpContent } from './http';
import { getFile, getImage, getJSON, getString as httpGetString } from './http';
export declare const Http: {
diff --git a/packages/core/index.ts b/packages/core/index.ts
index 23418fc82..7a078eb27 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -105,7 +105,7 @@ export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './dat
export type { PropertyChangeData, EventData } from './data/observable';
export { VirtualArray } from './data/virtual-array';
export type { ItemsLoading } from './data/virtual-array';
-export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess } from './file-system';
+export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess, AndroidDirectory } from './file-system';
// Export all interfaces from "http" module
export type { HttpRequestOptions, HttpResponse, Headers, HttpResponseEncoding, HttpContent } from './http';
diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar
index 6a6422504..de63332b7 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/core/platforms/ios/TNSWidgets.xcframework/Info.plist b/packages/core/platforms/ios/TNSWidgets.xcframework/Info.plist
index 266a21267..706038704 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/Info.plist
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/Info.plist
@@ -4,6 +4,23 @@
AvailableLibraries
+
+ DebugSymbolsPath
+ dSYMs
+ LibraryIdentifier
+ ios-arm64_x86_64-maccatalyst
+ LibraryPath
+ TNSWidgets.framework
+ SupportedArchitectures
+
+ arm64
+ x86_64
+
+ SupportedPlatform
+ ios
+ SupportedPlatformVariant
+ maccatalyst
+
DebugSymbolsPath
dSYMs
@@ -35,23 +52,6 @@
SupportedPlatformVariant
simulator
-
- DebugSymbolsPath
- dSYMs
- LibraryIdentifier
- ios-arm64_x86_64-maccatalyst
- LibraryPath
- TNSWidgets.framework
- SupportedArchitectures
-
- arm64
- x86_64
-
- SupportedPlatform
- ios
- SupportedPlatformVariant
- maccatalyst
-
CFBundlePackageType
XFWK
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/NSFileHandle+Async.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/NSFileHandle+Async.h
new file mode 100644
index 000000000..6d8827a94
--- /dev/null
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/NSFileHandle+Async.h
@@ -0,0 +1,22 @@
+//
+// NSFileHandle+Async.h
+// TNSWidgets
+//
+// Created by Osei Fortune on 03/05/2023.
+// Copyright © 2023 Telerik A D. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSFileHandle (Async)
+
+- (void)appendData:(nonnull NSData*) data
+ completion:(void (^) (NSError*))callback;
+
++ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/TNSWidgets.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/TNSWidgets.h
index 7da1d2adb..d1448af88 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/TNSWidgets.h
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Headers/TNSWidgets.h
@@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
+#import "NSFileHandle+Async.h"
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Info.plist b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Info.plist
index a6aa165ba..0309aa2de 100644
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Info.plist and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/Info.plist differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/TNSWidgets
index 5fd84acdf..22da60eb3 100755
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/TNSWidgets.framework/TNSWidgets differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets
index 476ef269b..dd4ce4d2c 100644
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/NSFileHandle+Async.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/NSFileHandle+Async.h
new file mode 100644
index 000000000..6d8827a94
--- /dev/null
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/NSFileHandle+Async.h
@@ -0,0 +1,22 @@
+//
+// NSFileHandle+Async.h
+// TNSWidgets
+//
+// Created by Osei Fortune on 03/05/2023.
+// Copyright © 2023 Telerik A D. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSFileHandle (Async)
+
+- (void)appendData:(nonnull NSData*) data
+ completion:(void (^) (NSError*))callback;
+
++ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/TNSWidgets.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/TNSWidgets.h
index 7da1d2adb..d1448af88 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/TNSWidgets.h
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Headers/TNSWidgets.h
@@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
+#import "NSFileHandle+Async.h"
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Resources/Info.plist b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Resources/Info.plist
index a2ecc4880..5ae3bcc2b 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Resources/Info.plist
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/Resources/Info.plist
@@ -3,7 +3,7 @@
BuildMachineOSBuild
- 21G72
+ 22D68
CFBundleDevelopmentRegion
en
CFBundleExecutable
@@ -29,7 +29,7 @@
DTCompiler
com.apple.compilers.llvm.clang.1_0
DTPlatformBuild
- 13F100
+ 14A309
DTPlatformName
macosx
DTPlatformVersion
@@ -39,9 +39,9 @@
DTSDKName
macosx12.3
DTXcode
- 1341
+ 1400
DTXcodeBuild
- 13F100
+ 14A309
LSMinimumSystemVersion
10.15
UIDeviceFamily
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/TNSWidgets
index e09d5ff97..d4691c81d 100755
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/TNSWidgets.framework/Versions/A/TNSWidgets differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets
index 5da40a918..ef1e48cf6 100644
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-maccatalyst/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/NSFileHandle+Async.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/NSFileHandle+Async.h
new file mode 100644
index 000000000..6d8827a94
--- /dev/null
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/NSFileHandle+Async.h
@@ -0,0 +1,22 @@
+//
+// NSFileHandle+Async.h
+// TNSWidgets
+//
+// Created by Osei Fortune on 03/05/2023.
+// Copyright © 2023 Telerik A D. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSFileHandle (Async)
+
+- (void)appendData:(nonnull NSData*) data
+ completion:(void (^) (NSError*))callback;
+
++ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/TNSWidgets.h b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/TNSWidgets.h
index 7da1d2adb..d1448af88 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/TNSWidgets.h
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Headers/TNSWidgets.h
@@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
+#import "NSFileHandle+Async.h"
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Info.plist b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Info.plist
index 2cf653f34..774b84f42 100644
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Info.plist and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/Info.plist differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/TNSWidgets
index bbe04699d..21da99e6d 100755
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/TNSWidgets differ
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/_CodeSignature/CodeResources b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/_CodeSignature/CodeResources
index 203fb12f5..4a512948c 100644
--- a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/_CodeSignature/CodeResources
+++ b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/TNSWidgets.framework/_CodeSignature/CodeResources
@@ -8,6 +8,10 @@
9BsuK8QsA57YnHHgpWIgaHygSEk=
+ Headers/NSFileHandle+Async.h
+
+ iQtzsDHw/VTnFG27yW+QvGCeXGw=
+
Headers/NSString+Async.h
o8366y9zYMOVyObC3vtKKy/8jxA=
@@ -22,7 +26,7 @@
Headers/TNSWidgets.h
- gUvu5bjZg5Aie5iJ1krxFmDrHwk=
+ ZFbCov7mFiXa4ZA/gAxqYHzRX7Q=
Headers/UIImage+TNSBlocks.h
@@ -34,7 +38,7 @@
Info.plist
- RCfacSfbjEiH19hn34SSJzGdiFo=
+ myeIggkwNuTIqt7xlMgf8VGsKZ8=
Modules/module.modulemap
@@ -58,6 +62,13 @@
JdjuVUBed00Ged4cSDzYLXONUlESu+dae9KN0PYJ/nM=
+ Headers/NSFileHandle+Async.h
+
+ hash2
+
+ 1IqS81dD1dbKccHZ0lYPMMF1zBPdp3InM+rdOFjBo+k=
+
+
Headers/NSString+Async.h
hash2
@@ -83,7 +94,7 @@
hash2
- V99t2zLwRPOs90tbGiQbhbdAFJlW7mp7X2R5337ewUA=
+ 6L3xStgKRHkPvDZAHAUF+EMI3Skf5jvnoA4ED/WbB1w=
Headers/UIImage+TNSBlocks.h
diff --git a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets
index 23c7a23f9..3562b4c23 100644
Binary files a/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets and b/packages/core/platforms/ios/TNSWidgets.xcframework/ios-arm64_x86_64-simulator/dSYMs/TNSWidgets.framework.dSYM/Contents/Resources/DWARF/TNSWidgets 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 57312ed9c..e25dfb625 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
@@ -39,6 +39,9 @@
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 function append(path: string, content: androidNative.Array, callback: CompleteCallback, context: any);
+ export function appendBuffer(param0: string, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.Async.CompleteCallback, param3: any): void;
+ export function appendText(path: string, content: string, encoding: string, callback: CompleteCallback, context: any);
}
export module Http {
@@ -646,7 +649,13 @@ declare module org {
export module widgets {
export class FileHelper {
public static class: java.lang.Class;
+ public appendTextSync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
+ public appendText(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
public readText(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public appendSync(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public append(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public appendBufferSync(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public appendBuffer(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: 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;
diff --git a/packages/ui-mobile-base/android/widgets/src/main/AndroidManifest.xml b/packages/ui-mobile-base/android/widgets/src/main/AndroidManifest.xml
index 803685657..01c1dbaa7 100644
--- a/packages/ui-mobile-base/android/widgets/src/main/AndroidManifest.xml
+++ b/packages/ui-mobile-base/android/widgets/src/main/AndroidManifest.xml
@@ -1,6 +1,9 @@
+
+
+
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 a6b9320d5..a3c5c4467 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
@@ -1,11 +1,13 @@
package org.nativescript.widgets;
+import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
+import android.os.Build;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
@@ -599,19 +601,87 @@ public class Async {
public static class File {
+ static void updateValue(Context context, Uri uri) {
+ ContentValues values = new ContentValues();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ context.getContentResolver().update(uri, values, null);
+ } else {
+ context.getContentResolver().update(uri, values, null, null);
+ }
+ }
+
+ public static void append(final String path, final byte[] 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 AppendTask task = new AppendTask(callback, context);
+ final boolean result = task.doInBackground(path, content);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ task.onPostExecute(result);
+ }
+ });
+ }
+ });
+ }
+
+ public static void appendBuffer(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 AppendBufferTask task = new AppendBufferTask(callback, context);
+ final boolean result = task.doInBackground(path, content);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ task.onPostExecute(result);
+ }
+ });
+ }
+ });
+ }
+
+ public static void appendText(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() {
+ @Override
+ public void run() {
+ final AppendTextTask task = new AppendTextTask(callback, context);
+ final boolean result = task.doInBackground(path, content, encoding);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ task.onPostExecute(result);
+ }
+ });
+ }
+ });
+ }
+
public static boolean copySync(final String src, final String dest, final Context context) throws Exception {
InputStream is;
OutputStream os;
-
+ boolean requiresUpdate = false;
if(src.startsWith("content://")){
is = context.getContentResolver().openInputStream(Uri.parse(src));
}else is = new FileInputStream(new java.io.File(src));
if(dest.startsWith("content://")){
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
+ requiresUpdate = true;
}else os = new FileOutputStream(new java.io.File(dest));
- return copySync(is, os, context);
+ boolean ret = copySync(is, os, context);
+
+ if(ret && requiresUpdate){
+ updateValue(context, Uri.parse(dest));
+ }
+
+ return ret;
+
}
public static boolean copySync(final InputStream src, final OutputStream dest, final Object context) throws Exception {
@@ -630,15 +700,33 @@ public class Async {
InputStream is;
OutputStream os;
+ boolean requiresUpdate = false;
+
if(src.startsWith("content://")){
is = context.getContentResolver().openInputStream(Uri.parse(src));
}else is = new FileInputStream(new java.io.File(src));
if(dest.startsWith("content://")){
+ requiresUpdate = true;
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
}else os = new FileOutputStream(new java.io.File(dest));
- copy(is, os, callback, context);
+ boolean finalRequiresUpdate = requiresUpdate;
+ copy(is, os, new CompleteCallback() {
+ @Override
+ public void onComplete(Object result, Object tag) {
+ if(finalRequiresUpdate){
+ updateValue(context, Uri.parse(dest));
+ }
+ callback.onComplete(result, tag);
+ }
+
+ @Override
+ public void onError(String error, Object tag) {
+ callback.onError(error, tag);
+ }
+ }, context);
+
}catch (Exception exception){
callback.onError(exception.getMessage(), context);
}
@@ -666,11 +754,12 @@ public class Async {
return written;
}
-
public static void copy(final InputStream src, final OutputStream dest, final CompleteCallback callback, final Object context) {
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
threadPoolExecutor().execute((Runnable) () -> {
+ boolean done = false;
+ Exception error = null;
try (InputStream is = src; OutputStream os = dest){
ReadableByteChannel isc = java.nio.channels.Channels.newChannel(is);
WritableByteChannel osc = java.nio.channels.Channels.newChannel(os);
@@ -679,10 +768,19 @@ public class Async {
int written = fastChannelCopy(isc, osc);
- mHandler.post(() -> callback.onComplete(size == written, context));
+ done = size == written;
} catch (Exception e) {
- mHandler.post(() -> callback.onError(e.getMessage(), context));
+ error = e;
+
+ }finally {
+ if (error != null){
+ Exception finalError = error;
+ mHandler.post(() -> callback.onError(finalError.getMessage(), context));
+ }else {
+ boolean finalDone = done;
+ mHandler.post(() -> callback.onComplete(finalDone, context));
+ }
}
});
}
@@ -1086,5 +1184,146 @@ public class Async {
}
}
+ static class AppendTask {
+ private final CompleteCallback callback;
+ private final Object context;
+
+ public AppendTask(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;
+ byte[] content = (byte[]) params[1];
+
+ try {
+ stream = new FileOutputStream(javaFile, true);
+ stream.write(content, 0, content.length);
+
+ return true;
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to append 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("AppendTask returns no result.", this.context);
+ }
+ }
+ }
+
+ static class AppendBufferTask {
+ private final CompleteCallback callback;
+ private final Object context;
+
+ public AppendBufferTask(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, true);
+ FileChannel channel = stream.getChannel();
+ content.rewind();
+ channel.write(content);
+ content.rewind();
+ return true;
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to append to file, FileNotFoundException: " + e.getMessage());
+ return false;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to append 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("AppendTask returns no result.", this.context);
+ }
+ }
+ }
+
+ static class AppendTextTask {
+ private final CompleteCallback callback;
+ private final Object context;
+
+ public AppendTextTask(CompleteCallback callback, Object context) {
+ this.callback = callback;
+ this.context = context;
+ }
+
+ protected boolean doInBackground(String... params) {
+ java.io.File javaFile = new java.io.File(params[0]);
+ FileOutputStream stream = null;
+ try {
+ stream = new FileOutputStream(javaFile, true);
+
+ OutputStreamWriter writer = new OutputStreamWriter(stream, params[2]);
+ writer.write(params[1]);
+ writer.close();
+
+ return true;
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to append file, FileNotFoundException: " + e.getMessage());
+ return false;
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Failed to append file, UnsupportedEncodingException: " + e.getMessage());
+ return false;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to append 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("AppendTextTask 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 10096dfcb..0171a9594 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,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
@@ -33,625 +34,739 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FileHelper {
- private Uri uri;
- private long size;
- private String name;
- private String mime;
- private long lastModified;
- private ExecutorService executor = Executors.newSingleThreadExecutor();
- private Handler handler;
-
- public interface Callback {
- void onError(Exception exception);
-
- void onSuccess(@Nullable Object result);
- }
-
- FileHelper(Uri uri) {
- handler = new Handler(Looper.myLooper());
- this.uri = uri;
- }
-
- private static boolean isExternalStorageDocument(Uri uri) {
- return "com.android.externalstorage.documents".equals(uri
- .getAuthority());
- }
-
- private static @Nullable
- Cursor getCursor(Context context, Uri uri) {
- Cursor cursor = null;
- String[] projections = {
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.DATE_MODIFIED
- };
- try {
- if (Build.VERSION.SDK_INT >= 19) {
- if (DocumentsContract.isDocumentUri(context, uri)) {
- if (Build.VERSION.SDK_INT >= 29) {
- if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
- cursor = context.getContentResolver().query(
- MediaStore.getMediaUri(context, uri), projections, null, null, null, null
- );
- }
- }
- }
- }
- if (cursor == null) {
- cursor = context.getContentResolver().query(uri, projections, null, null, null);
- }
- } catch (Exception ignored) {
- }
-
- return cursor;
-
- }
-
- public static boolean exists(Context context, String string) {
- try {
- return exists(context, Uri.parse(string));
- } catch (Exception ignored) {
- return false;
- }
- }
-
- public static boolean exists(Context context, Uri uri) {
- if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- if (file != null) {
- return file.exists();
- }
- return false;
- }
- Cursor cursor = getCursor(context, uri);
- if (cursor == null) {
- return false;
- }
- boolean exists = cursor.moveToFirst();
- cursor.close();
- return exists;
- }
-
- public static @Nullable
- FileHelper fromString(Context context, String string) {
- try {
- return fromUri(context, Uri.parse(string));
- } catch (Exception e) {
- return null;
- }
- }
-
- @SuppressWarnings("deprecation")
- private static @Nullable
- File getFile(Context context, Uri uri) {
- if (Build.VERSION.SDK_INT >= 19) {
- String docId = DocumentsContract.getDocumentId(uri);
- String[] split = docId.split(":");
- String type = split[0];
- String path = split[1];
-
- if ("primary".equals(type)) {
- int nameIndex = path.lastIndexOf("/");
- String seg = path.substring(0, nameIndex);
- String[] parts = Uri.decode(uri.toString()).split(":" + seg);
- String file = Environment.getExternalStorageDirectory() + "/" + path + "/" + parts[1];
- return new File(file);
- } else {
- File[] cacheDirs = context.getExternalCacheDirs();
- String storageDir = null;
- for (File cacheDir : cacheDirs) {
- final String cachePath = cacheDir.getPath();
- int index = cachePath.indexOf(type);
- if (index >= 0) {
- storageDir = cachePath.substring(0, index + type.length());
- }
- }
-
- if (storageDir != null) {
- return new File(storageDir + "/" + path);
- }
- }
- }
- return null;
- }
-
-
- public static @Nullable
- FileHelper fromUri(Context context, Uri contentUri) {
- Uri uri = contentUri;
-
- if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- if (file == null) {
- return null;
- }
- FileHelper helper = new FileHelper(uri);
- helper.size = file.length();
- helper.name = file.getName();
- helper.mime = context.getContentResolver().getType(uri);
- helper.lastModified = file.lastModified() / 1000;
- return helper;
- }
-
- Cursor cursor = getCursor(context, uri);
-
- if (cursor == null) {
- return null;
- }
-
- boolean moved = cursor.moveToFirst();
- FileHelper helper = null;
- if (moved) {
- int sizeIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.SIZE
- );
-
- int nameIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.DISPLAY_NAME
- );
-
- int lastModifiedIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.DATE_MODIFIED
- );
-
- helper = new FileHelper(uri);
- helper.size = cursor.getLong(sizeIndex);
- helper.name = cursor.getString(nameIndex);
- helper.mime = context.getContentResolver().getType(uri);
- helper.lastModified = cursor.getLong(lastModifiedIndex);
- }
- cursor.close();
- return helper;
- }
-
- private void updateInternal(Context context) {
- updateInternal(context, true);
- }
-
-
- private void updateValue(Context context, Uri uri, ContentValues values) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- context.getContentResolver().update(uri, values, null);
- } else {
- context.getContentResolver().update(uri, values, null, null);
- }
- }
-
- private void updateInternal(Context context, boolean force) {
-
- if (force) {
- // trigger db update
- ContentValues values = new ContentValues();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- if (isExternalStorageDocument(uri)) {
- return;
- }
- if (DocumentsContract.isDocumentUri(context, uri)) {
- DocumentFile file = DocumentFile.fromSingleUri(context, uri);
- if (file != null) {
- updateValue(context, file.getUri(), values);
- }
- } else {
- updateValue(context, uri, values);
- }
- } else {
- updateValue(context, uri, values);
- }
- }
-
- Cursor cursor = getCursor(context, uri);
- if (cursor == null) {
- return;
- }
-
- boolean moved = cursor.moveToFirst();
-
- if (moved) {
- int sizeIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.SIZE
- );
-
- int nameIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.DISPLAY_NAME
- );
-
- int lastModifiedIndex = cursor.getColumnIndex(
- MediaStore.MediaColumns.DATE_MODIFIED
- );
-
- size = cursor.getLong(sizeIndex);
- name = cursor.getString(nameIndex);
- mime = context.getContentResolver().getType(uri);
- lastModified = cursor.getLong(lastModifiedIndex);
- }
-
- cursor.close();
- }
-
- public long getSize() {
- return size;
- }
-
- public String getName() {
- return name;
- }
-
- public String getMime() {
- if (mime == null) {
- return "application/octet-stream";
- }
- return mime;
- }
-
- public String getExtension() {
- if (mime == null) {
- return "";
- }
- return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
- }
-
- public long getLastModified() {
- return lastModified;
- }
-
- private InputStream getInputStream(Context context, Uri uri) throws Exception {
- if (Build.VERSION.SDK_INT >= 19) {
- if (isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- return new FileInputStream(file);
- }
- if (DocumentsContract.isDocumentUri(context, uri)) {
- return context.getContentResolver().openInputStream(DocumentFile.fromSingleUri(context, uri).getUri());
- }
- }
- return context.getContentResolver().openInputStream(uri);
- }
-
- private OutputStream getOutputStream(Context context, Uri uri) throws Exception {
- if (Build.VERSION.SDK_INT >= 19) {
- if (isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- return new FileOutputStream(file);
- }
- if (DocumentsContract.isDocumentUri(context, uri)) {
- return context.getContentResolver().openOutputStream(DocumentFile.fromSingleUri(context, uri).getUri());
- }
- }
- return context.getContentResolver().openOutputStream(uri);
- }
-
- private byte[] readSyncInternal(Context context) throws Exception {
- InputStream is = getInputStream(context, uri);
-
- Async.Http.RequestResult.ByteArrayOutputStream2 ret = new Async.Http.RequestResult.ByteArrayOutputStream2();
-
- byte[] buff = new byte[4096];
- int read;
- while ((read = is.read(buff, 0, buff.length)) != -1) {
- ret.write(buff, 0, read);
- }
-
- is.close();
- 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 {
- return readSyncInternal(context);
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- return null;
- }
-
- public void read(Context context, Callback callback) {
- executor.execute(() -> {
- try {
- byte[] result = readSyncInternal(context);
- handler.post(() -> callback.onSuccess(result));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
-
- 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) {
- characterSet = "UTF-8";
- }
-
- InputStream is = getInputStream(context, uri);
- InputStreamReader isr = new InputStreamReader(is, characterSet);
- BufferedReader reader = new BufferedReader(isr);
- char[] buf = new char[is.available()];
- reader.read(buf);
- reader.close();
- return new String(buf);
- }
-
- public String readTextSync(Context context, @Nullable String encoding, @Nullable Callback callback) {
- try {
- return readTextSyncInternal(context, encoding);
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- return null;
- }
-
- public void readText(Context context, @Nullable String encoding, Callback callback) {
- executor.execute(() -> {
- try {
- String result = readTextSyncInternal(context, encoding);
- handler.post(() -> callback.onSuccess(result));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
-
- private void writeSyncInternal(Context context, byte[] content) throws Exception {
- OutputStream os = getOutputStream(context, uri);
- os.write(content, 0, content.length);
- os.flush();
- os.close();
- 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);
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- }
-
- public void write(Context context, byte[] content, Callback callback) {
- executor.execute(() -> {
- try {
- writeSyncInternal(context, content);
- handler.post(() -> callback.onSuccess(null));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
-
- 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;
- if (characterSet == null) {
- characterSet = "UTF-8";
- }
- OutputStreamWriter osw = new OutputStreamWriter(os, characterSet);
- osw.write(content);
- osw.flush();
- osw.close();
- updateInternal(context);
- }
-
- public void writeTextSync(Context context, String content, @Nullable String encoding, @Nullable Callback callback) {
- try {
- writeTextSyncInternal(context, content, encoding);
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- }
-
- public void writeText(Context context, String content, @Nullable String encoding, Callback callback) {
- executor.execute(() -> {
- try {
- writeTextSyncInternal(context, content, encoding);
- handler.post(() -> callback.onSuccess(null));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
-
- private void copyToFileInternal(InputStream is, OutputStream os) throws Exception {
- int read;
- byte[] buf = new byte[1024];
- while ((read = is.read(buf)) != -1) {
- os.write(buf, 0, read);
- }
- is.close();
- os.flush();
- os.close();
- }
-
- private void copyToFileInternal(Context context, File file) throws Exception {
- InputStream is = getInputStream(context, uri);
- FileOutputStream os = new FileOutputStream(file);
- copyToFileInternal(is, os);
- }
-
- public boolean copyToFileSync(Context context, File file, @Nullable Callback callback) {
- boolean completed = false;
- try {
- copyToFileInternal(context, file);
- completed = true;
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- return completed;
- }
-
- public void copyToFile(Context context, File file, Callback callback) {
- executor.execute(() -> {
- try {
- copyToFileInternal(context, file);
- handler.post(() -> callback.onSuccess(true));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
-
- public boolean delete(Context context) {
- try {
- if (Build.VERSION.SDK_INT >= 19) {
- if (isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- if (file != null) {
- return file.delete();
- }
- return false;
- } else {
- if (DocumentsContract.isDocumentUri(context, uri)) {
- if (Build.VERSION.SDK_INT >= 29) {
- if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
- return context.getContentResolver().delete(
- MediaStore.getMediaUri(context, uri), null, null
- ) > 0;
- }
-
- } else {
- return DocumentsContract.deleteDocument(context.getContentResolver(), uri);
- }
- }
- }
- }
- return context.getContentResolver().delete(uri, null, null) > 0;
- } catch (SecurityException | FileNotFoundException e) {
- return false;
- }
- }
-
-
- private void renameInternal(Context context, String newName) throws Exception {
- ContentValues values = new ContentValues();
- values.put(MediaStore.MediaColumns.DISPLAY_NAME, newName);
-
- if (Build.VERSION.SDK_INT >= 19) {
- if (isExternalStorageDocument(uri)) {
- File file = getFile(context, uri);
- if (file != null) {
- file.renameTo(new File(file.getParentFile(), newName));
- return;
- }
- return;
- }
-
- if (DocumentsContract.isDocumentUri(context, uri)) {
- if (Build.VERSION.SDK_INT >= 29) {
- if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
- context.getContentResolver().update(
- uri, values, null, null
- );
- return;
- }
- DocumentsContract.renameDocument(context.getContentResolver(), uri, newName);
- } else if (Build.VERSION.SDK_INT >= 21) {
- DocumentsContract.renameDocument(context.getContentResolver(), uri, newName);
- return;
- }
- }
-
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- context.getContentResolver().update(uri, values, null);
- } else {
- context.getContentResolver().update(uri, values, null, null);
- }
-
- updateInternal(context, false);
- }
-
- public void renameSync(Context context, String newName, @Nullable Callback callback) {
- try {
- renameInternal(context, newName);
- } catch (Exception e) {
- if (callback != null) {
- callback.onError(e);
- }
- }
- }
-
- public void rename(Context context, String newName, Callback callback) {
- executor.execute(() -> {
- try {
- renameInternal(context, newName);
- handler.post(() -> callback.onSuccess(null));
- } catch (Exception e) {
- handler.post(() -> callback.onError(e));
- }
- });
- }
+ private Uri uri;
+ private String name;
+ private String mime;
+ private long lastModified;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final Handler handler;
+
+ private WeakReference context;
+
+ public interface Callback {
+ void onError(Exception exception);
+
+ void onSuccess(@Nullable Object result);
+ }
+
+ FileHelper(Uri uri) {
+ handler = new Handler(Looper.myLooper());
+ this.uri = uri;
+ }
+
+ private static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri
+ .getAuthority());
+ }
+
+ private static @Nullable
+ Cursor getCursor(Context context, Uri uri) {
+ return getCursor(context, uri, new String[]{
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ MediaStore.MediaColumns.DATE_MODIFIED
+ });
+ }
+
+ private static @Nullable
+ Cursor getCursor(Context context, Uri uri, String[] projections) {
+ Cursor cursor = null;
+ try {
+ if (Build.VERSION.SDK_INT >= 19) {
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
+ cursor = context.getContentResolver().query(
+ MediaStore.getMediaUri(context, uri), projections, null, null, null, null
+ );
+ }
+ }
+ }
+ }
+ if (cursor == null) {
+ cursor = context.getContentResolver().query(uri, projections, null, null, null);
+ }
+ } catch (Exception ignored) {
+ }
+
+ return cursor;
+
+ }
+
+ public static boolean exists(Context context, String string) {
+ try {
+ return exists(context, Uri.parse(string));
+ } catch (Exception ignored) {
+ return false;
+ }
+ }
+
+ public static boolean exists(Context context, Uri uri) {
+ if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ if (file != null) {
+ return file.exists();
+ }
+ return false;
+ }
+ Cursor cursor = getCursor(context, uri);
+ if (cursor == null) {
+ return false;
+ }
+ boolean exists = cursor.moveToFirst();
+ cursor.close();
+ return exists;
+ }
+
+ public static @Nullable
+ FileHelper fromString(Context context, String string) {
+ try {
+ FileHelper ret = fromUri(context, Uri.parse(string));
+ if (ret != null) {
+ ret.context = new WeakReference<>(context);
+ }
+ return ret;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static @Nullable
+ File getFile(Context context, Uri uri) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ String docId = DocumentsContract.getDocumentId(uri);
+ String[] split = docId.split(":");
+ String type = split[0];
+ String path = split[1];
+
+ if ("primary".equals(type)) {
+ int nameIndex = path.lastIndexOf("/");
+ String seg = path.substring(0, nameIndex);
+ String[] parts = Uri.decode(uri.toString()).split(":" + seg);
+ String file = Environment.getExternalStorageDirectory() + "/" + path + "/" + parts[1];
+ return new File(file);
+ } else {
+ File[] cacheDirs = context.getExternalCacheDirs();
+ String storageDir = null;
+ for (File cacheDir : cacheDirs) {
+ final String cachePath = cacheDir.getPath();
+ int index = cachePath.indexOf(type);
+ if (index >= 0) {
+ storageDir = cachePath.substring(0, index + type.length());
+ }
+ }
+
+ if (storageDir != null) {
+ return new File(storageDir + "/" + path);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ public static @Nullable
+ FileHelper fromUri(Context context, Uri contentUri) {
+ Uri uri = contentUri;
+
+ if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ if (file == null) {
+ return null;
+ }
+ FileHelper helper = new FileHelper(uri);
+ helper.name = file.getName();
+ helper.mime = context.getContentResolver().getType(uri);
+ helper.lastModified = file.lastModified() / 1000;
+ helper.context = new WeakReference<>(context);
+ return helper;
+ }
+
+ Cursor cursor = getCursor(context, uri);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ boolean moved = cursor.moveToFirst();
+ FileHelper helper = null;
+ if (moved) {
+ int nameIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DISPLAY_NAME
+ );
+
+ int lastModifiedIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DATE_MODIFIED
+ );
+
+ helper = new FileHelper(uri);
+ helper.name = cursor.getString(nameIndex);
+ helper.mime = context.getContentResolver().getType(uri);
+ helper.lastModified = cursor.getLong(lastModifiedIndex);
+ helper.context = new WeakReference<>(context);
+ }
+ cursor.close();
+
+ return helper;
+ }
+
+ private long getFileSize() {
+ Context context = this.context.get();
+ if (context == null) {
+ return 0;
+ }
+ if (Build.VERSION.SDK_INT >= 19 && isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ if (file == null) {
+ return 0;
+ }
+ return file.length();
+ }
+
+ Cursor cursor = getCursor(context, uri, null);
+
+ if (cursor == null) {
+ return 0;
+ }
+
+ long size = 0;
+
+ boolean moved = cursor.moveToFirst();
+ if (moved) {
+ int sizeIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.SIZE
+ );
+
+ size = cursor.getLong(sizeIndex);
+
+ }
+ cursor.close();
+
+ return size;
+ }
+
+ private void updateInternal(Context context) {
+ updateInternal(context, true);
+ }
+
+
+ private void updateValue(Context context, Uri uri, ContentValues values) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ context.getContentResolver().update(uri, values, null);
+ } else {
+ context.getContentResolver().update(uri, values, null, null);
+ }
+ }
+
+ private void updateInternal(Context context, boolean force) {
+
+ if (force) {
+ // trigger db update
+ ContentValues values = new ContentValues();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ if (isExternalStorageDocument(uri)) {
+ return;
+ }
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ DocumentFile file = DocumentFile.fromSingleUri(context, uri);
+ if (file != null) {
+ updateValue(context, file.getUri(), values);
+ }
+ } else {
+ updateValue(context, uri, values);
+ }
+ } else {
+ updateValue(context, uri, values);
+ }
+ }
+
+ Cursor cursor = getCursor(context, uri);
+ if (cursor == null) {
+ return;
+ }
+
+ boolean moved = cursor.moveToFirst();
+
+ if (moved) {
+ int nameIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DISPLAY_NAME
+ );
+
+ int lastModifiedIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DATE_MODIFIED
+ );
+
+ name = cursor.getString(nameIndex);
+ mime = context.getContentResolver().getType(uri);
+ lastModified = cursor.getLong(lastModifiedIndex);
+ }
+
+ cursor.close();
+ }
+
+ public long getSize() {
+ return getFileSize();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getMime() {
+ if (mime == null) {
+ return "application/octet-stream";
+ }
+ return mime;
+ }
+
+ public String getExtension() {
+ if (mime == null) {
+ return "";
+ }
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ private InputStream getInputStream(Context context, Uri uri) throws Exception {
+ if (Build.VERSION.SDK_INT >= 19) {
+ if (isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ return new FileInputStream(file);
+ }
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ return context.getContentResolver().openInputStream(DocumentFile.fromSingleUri(context, uri).getUri());
+ }
+ }
+ return context.getContentResolver().openInputStream(uri);
+ }
+
+ private OutputStream getOutputStream(Context context, Uri uri) throws Exception {
+ return getOutputStream(context, uri, false);
+ }
+
+ private OutputStream getOutputStream(Context context, Uri uri, boolean append) throws Exception {
+ if (Build.VERSION.SDK_INT >= 19) {
+ if (isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ return new FileOutputStream(file, append);
+ }
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ return context.getContentResolver().openOutputStream(DocumentFile.fromSingleUri(context, uri).getUri(), append ? "wa" : "w");
+ }
+ }
+ return context.getContentResolver().openOutputStream(uri, append ? "wa" : "w");
+ }
+
+ public void appendSync(Context context, byte[] content, @Nullable Callback callback) {
+ try {
+ writeSyncInternal(context, content, true);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void append(Context context, byte[] content, Callback callback) {
+ executor.execute(() -> {
+ try {
+ writeSyncInternal(context, content, true);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ public void appendBufferSync(Context context, ByteBuffer content, @Nullable Callback callback) {
+ try {
+ writeBufferSyncInternal(context, content, true);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void appendBuffer(Context context, ByteBuffer content, Callback callback) {
+ executor.execute(() -> {
+ try {
+ writeBufferSyncInternal(context, content, true);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ public void appendTextSync(Context context, String content, @Nullable String encoding, @Nullable Callback callback) {
+ try {
+ writeTextSyncInternal(context, content, encoding, true);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void appendText(Context context, String content, @Nullable String encoding, Callback callback) {
+ executor.execute(() -> {
+ try {
+ writeTextSyncInternal(context, content, encoding, true);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ private byte[] readSyncInternal(Context context) throws Exception {
+ InputStream is = getInputStream(context, uri);
+
+ Async.Http.RequestResult.ByteArrayOutputStream2 ret = new Async.Http.RequestResult.ByteArrayOutputStream2();
+
+ byte[] buff = new byte[4096];
+ int read;
+ while ((read = is.read(buff, 0, buff.length)) != -1) {
+ ret.write(buff, 0, read);
+ }
+
+ is.close();
+ 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 {
+ return readSyncInternal(context);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ return null;
+ }
+
+ public void read(Context context, Callback callback) {
+ executor.execute(() -> {
+ try {
+ byte[] result = readSyncInternal(context);
+ handler.post(() -> callback.onSuccess(result));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ 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) {
+ characterSet = "UTF-8";
+ }
+
+ InputStream is = getInputStream(context, uri);
+ InputStreamReader isr = new InputStreamReader(is, characterSet);
+ BufferedReader reader = new BufferedReader(isr);
+ char[] buf = new char[is.available()];
+ reader.read(buf);
+ reader.close();
+ return new String(buf);
+ }
+
+ public String readTextSync(Context context, @Nullable String encoding, @Nullable Callback callback) {
+ try {
+ return readTextSyncInternal(context, encoding);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ return null;
+ }
+
+ public void readText(Context context, @Nullable String encoding, Callback callback) {
+ executor.execute(() -> {
+ try {
+ String result = readTextSyncInternal(context, encoding);
+ handler.post(() -> callback.onSuccess(result));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ private void writeSyncInternal(Context context, byte[] content) throws Exception {
+ writeSyncInternal(context, content, false);
+ }
+
+ private void writeSyncInternal(Context context, byte[] content, boolean append) throws Exception {
+ OutputStream os = getOutputStream(context, uri, append);
+ os.write(content, 0, content.length);
+ os.flush();
+ os.close();
+ updateInternal(context);
+ }
+
+ private void writeBufferSyncInternal(Context context, ByteBuffer content) throws Exception {
+ writeBufferSyncInternal(context, content, false);
+ }
+
+ private void writeBufferSyncInternal(Context context, ByteBuffer content, boolean append) throws Exception {
+ OutputStream os = getOutputStream(context, uri, append);
+ 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);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void write(Context context, byte[] content, Callback callback) {
+ executor.execute(() -> {
+ try {
+ writeSyncInternal(context, content);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ 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 {
+ writeTextSyncInternal(context, content, encoding, false);
+ }
+
+ private void writeTextSyncInternal(Context context, String content, @Nullable String encoding, boolean append) throws Exception {
+ OutputStream os = getOutputStream(context, uri, append);
+ String characterSet = encoding;
+ if (characterSet == null) {
+ characterSet = "UTF-8";
+ }
+ OutputStreamWriter osw = new OutputStreamWriter(os, characterSet);
+ osw.write(content);
+ osw.flush();
+ osw.close();
+ updateInternal(context);
+ }
+
+ public void writeTextSync(Context context, String content, @Nullable String encoding, @Nullable Callback callback) {
+ try {
+ writeTextSyncInternal(context, content, encoding);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void writeText(Context context, String content, @Nullable String encoding, Callback callback) {
+ executor.execute(() -> {
+ try {
+ writeTextSyncInternal(context, content, encoding);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ private void copyToFileInternal(InputStream is, OutputStream os) throws Exception {
+ int read;
+ byte[] buf = new byte[1024];
+ while ((read = is.read(buf)) != -1) {
+ os.write(buf, 0, read);
+ }
+ is.close();
+ os.flush();
+ os.close();
+ }
+
+ private void copyToFileInternal(Context context, File file) throws Exception {
+ InputStream is = getInputStream(context, uri);
+ FileOutputStream os = new FileOutputStream(file);
+ copyToFileInternal(is, os);
+ }
+
+ public boolean copyToFileSync(Context context, File file, @Nullable Callback callback) {
+ boolean completed = false;
+ try {
+ copyToFileInternal(context, file);
+ completed = true;
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ return completed;
+ }
+
+ public void copyToFile(Context context, File file, Callback callback) {
+ executor.execute(() -> {
+ try {
+ copyToFileInternal(context, file);
+ handler.post(() -> callback.onSuccess(true));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
+
+ public boolean delete(Context context) {
+ try {
+ if (Build.VERSION.SDK_INT >= 19) {
+ if (isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ if (file != null) {
+ return file.delete();
+ }
+ return false;
+ } else {
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
+ return context.getContentResolver().delete(
+ MediaStore.getMediaUri(context, uri), null, null
+ ) > 0;
+ }
+
+ } else {
+ return DocumentsContract.deleteDocument(context.getContentResolver(), uri);
+ }
+ }
+ }
+ }
+ return context.getContentResolver().delete(uri, null, null) > 0;
+ } catch (SecurityException | FileNotFoundException e) {
+ return false;
+ }
+ }
+
+
+ private void renameInternal(Context context, String newName) throws Exception {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.DISPLAY_NAME, newName);
+
+ if (Build.VERSION.SDK_INT >= 19) {
+ if (isExternalStorageDocument(uri)) {
+ File file = getFile(context, uri);
+ if (file != null) {
+ file.renameTo(new File(file.getParentFile(), newName));
+ return;
+ }
+ return;
+ }
+
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ if (!uri.toString().startsWith("content://com.android.providers.downloads.documents")) {
+ context.getContentResolver().update(
+ uri, values, null, null
+ );
+ return;
+ }
+ DocumentsContract.renameDocument(context.getContentResolver(), uri, newName);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ DocumentsContract.renameDocument(context.getContentResolver(), uri, newName);
+ return;
+ }
+ }
+
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ context.getContentResolver().update(uri, values, null);
+ } else {
+ context.getContentResolver().update(uri, values, null, null);
+ }
+
+ updateInternal(context, false);
+ }
+
+ public void renameSync(Context context, String newName, @Nullable Callback callback) {
+ try {
+ renameInternal(context, newName);
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onError(e);
+ }
+ }
+ }
+
+ public void rename(Context context, String newName, Callback callback) {
+ executor.execute(() -> {
+ try {
+ renameInternal(context, newName);
+ handler.post(() -> callback.onSuccess(null));
+ } catch (Exception e) {
+ handler.post(() -> callback.onError(e));
+ }
+ });
+ }
}
diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj
index 50817817c..b52c1fd52 100644
--- a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj
+++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj
@@ -19,6 +19,8 @@
D004031022F781A50089EAD8 /* NSString+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = D004030E22F781A50089EAD8 /* NSString+Async.m */; };
D004031322FA27D60089EAD8 /* NSData+Async.h in Headers */ = {isa = PBXBuildFile; fileRef = D004031122FA27D60089EAD8 /* NSData+Async.h */; settings = {ATTRIBUTES = (Public, ); }; };
D004031422FA27D60089EAD8 /* NSData+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = D004031222FA27D60089EAD8 /* NSData+Async.m */; };
+ F11DE2162A02F39A00B70DC5 /* NSFileHandle+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */; };
+ F11DE2182A02F3B500B70DC5 /* NSFileHandle+Async.h in Headers */ = {isa = PBXBuildFile; fileRef = F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */; settings = {ATTRIBUTES = (Public, ); }; };
F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */ = {isa = PBXBuildFile; fileRef = F915D3531EC9EF5E00071914 /* TNSProcess.m */; };
F915D3561EC9EF5E00071914 /* TNSProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = F915D3541EC9EF5E00071914 /* TNSProcess.h */; settings = {ATTRIBUTES = (Public, ); }; };
F98F5CB31CD0EFEA00978308 /* TNSWidgets.h in Headers */ = {isa = PBXBuildFile; fileRef = F98F5CB21CD0EFEA00978308 /* TNSWidgets.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -51,6 +53,8 @@
D004030E22F781A50089EAD8 /* NSString+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+Async.m"; sourceTree = ""; };
D004031122FA27D60089EAD8 /* NSData+Async.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Async.h"; sourceTree = ""; };
D004031222FA27D60089EAD8 /* NSData+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Async.m"; sourceTree = ""; };
+ F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileHandle+Async.m"; sourceTree = ""; };
+ F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileHandle+Async.h"; sourceTree = ""; };
F915D3531EC9EF5E00071914 /* TNSProcess.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TNSProcess.m; path = ../TNSProcess.m; sourceTree = ""; };
F915D3541EC9EF5E00071914 /* TNSProcess.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TNSProcess.h; path = ../TNSProcess.h; sourceTree = ""; };
F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TNSWidgets.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -121,6 +125,8 @@
B8E76F5D212C3134009CFCE2 /* UIView+PassThroughParent.m */,
D004031122FA27D60089EAD8 /* NSData+Async.h */,
D004031222FA27D60089EAD8 /* NSData+Async.m */,
+ F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */,
+ F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */,
);
path = TNSWidgets;
sourceTree = "";
@@ -147,6 +153,7 @@
D004031322FA27D60089EAD8 /* NSData+Async.h in Headers */,
F98F5CCB1CD0F09E00978308 /* UIImage+TNSBlocks.h in Headers */,
B8E76F52212C2DA2009CFCE2 /* NSObject+Swizzling.h in Headers */,
+ F11DE2182A02F3B500B70DC5 /* NSFileHandle+Async.h in Headers */,
B8E76F5A212C2F4E009CFCE2 /* UIView+PropertyBag.h in Headers */,
D004030F22F781A50089EAD8 /* NSString+Async.h in Headers */,
8B7321CF1D097ECD00884AC6 /* TNSLabel.h in Headers */,
@@ -252,6 +259,7 @@
files = (
8B7321D01D097ECD00884AC6 /* TNSLabel.m in Sources */,
F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */,
+ F11DE2162A02F39A00B70DC5 /* NSFileHandle+Async.m in Sources */,
F98F5CCC1CD0F09E00978308 /* UIImage+TNSBlocks.m in Sources */,
B8E76F53212C2DA2009CFCE2 /* NSObject+Swizzling.m in Sources */,
D004031022F781A50089EAD8 /* NSString+Async.m in Sources */,
diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.h b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.h
new file mode 100644
index 000000000..6d8827a94
--- /dev/null
+++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.h
@@ -0,0 +1,22 @@
+//
+// NSFileHandle+Async.h
+// TNSWidgets
+//
+// Created by Osei Fortune on 03/05/2023.
+// Copyright © 2023 Telerik A D. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSFileHandle (Async)
+
+- (void)appendData:(nonnull NSData*) data
+ completion:(void (^) (NSError*))callback;
+
++ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.m b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.m
new file mode 100644
index 000000000..8d9be591d
--- /dev/null
+++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/NSFileHandle+Async.m
@@ -0,0 +1,82 @@
+//
+// NSFileHandle+Async.m
+// TNSWidgets
+//
+// Created by Osei Fortune on 03/05/2023.
+// Copyright © 2023 Telerik A D. All rights reserved.
+//
+
+#import "NSFileHandle+Async.h"
+
+@implementation NSFileHandle(Async)
+
+- (void)appendData:(NSData *)data completion:(void (^)(NSError*))callback {
+ dispatch_queue_t asyncQueue = dispatch_queue_create("org.nativescript.TNSWidgets.fileHandle", NULL);
+ dispatch_async(asyncQueue, ^(void) {
+ NSError *error = nil;
+
+ if (@available(iOS 13.0, *)) {
+ [self seekToEndReturningOffset:nil error:&error];
+ [self writeData:data error:&error];
+ } else {
+ @try {
+ [self seekToEndOfFile];
+ [self writeData:data];
+ } @catch (NSException *exception) {
+
+ NSMutableDictionary * info = [NSMutableDictionary dictionary];
+ [info setValue:exception.name forKey:@"ExceptionName"];
+ [info setValue:exception.reason forKey:@"ExceptionReason"];
+ [info setValue:exception.callStackReturnAddresses forKey:@"ExceptionCallStackReturnAddresses"];
+ [info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
+ [info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
+
+ error = [[NSError alloc] initWithDomain:@"" code: 1 userInfo:info];
+
+ }
+ }
+
+
+ dispatch_async(dispatch_get_main_queue(), ^(void) {
+ callback(error);
+ });
+
+ });
+}
+
++ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*, NSError*))callback {
+ dispatch_queue_t asyncQueue = dispatch_queue_create("org.nativescript.TNSWidgets.fileHandle", NULL);
+ dispatch_async(asyncQueue, ^(void) {
+ NSFileHandle* handle = [NSFileHandle fileHandleForWritingAtPath: path];
+ NSError *error = nil;
+
+ if (@available(iOS 13.0, *)) {
+ [handle seekToEndReturningOffset:nil error:&error];
+ [handle writeData:data error:&error];
+ } else {
+ @try {
+ [handle seekToEndOfFile];
+ [handle writeData:data];
+ } @catch (NSException *exception) {
+
+ NSMutableDictionary * info = [NSMutableDictionary dictionary];
+ [info setValue:exception.name forKey:@"ExceptionName"];
+ [info setValue:exception.reason forKey:@"ExceptionReason"];
+ [info setValue:exception.callStackReturnAddresses forKey:@"ExceptionCallStackReturnAddresses"];
+ [info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
+ [info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
+
+ error = [[NSError alloc] initWithDomain:@"" code: 1 userInfo:info];
+
+ }
+ }
+
+
+ dispatch_async(dispatch_get_main_queue(), ^(void) {
+ callback(handle,error);
+ });
+
+ });
+}
+
+@end
diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/TNSWidgets.h b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/TNSWidgets.h
index 7da1d2adb..d1448af88 100644
--- a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/TNSWidgets.h
+++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/TNSWidgets.h
@@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
+#import "NSFileHandle+Async.h"