diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index d532edcf25..d25cd1d7c0 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -17,6 +17,7 @@
+
diff --git a/apps/toolbox/src/pages/fs-helper.ts b/apps/toolbox/src/pages/fs-helper.ts
new file mode 100644
index 0000000000..5e100f1250
--- /dev/null
+++ b/apps/toolbox/src/pages/fs-helper.ts
@@ -0,0 +1,73 @@
+import { Page, EventData, Application, File } from '@nativescript/core';
+
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+}
+
+export function createRandom(args) {
+ if (global.isAndroid) {
+ try {
+ const activity = Application.android.foregroundActivity as androidx.appcompat.app.AppCompatActivity;
+ const selection = [android.provider.MediaStore.MediaColumns.DISPLAY_NAME, android.provider.MediaStore.MediaColumns._ID];
+ // testing with downloads as rename only works with a well know collection downloads/audio/photos/videos API 29+
+ let cursor = activity.getContentResolver().query(android.provider.MediaStore.Downloads.getContentUri('external'), selection, null, null);
+
+ let uri;
+
+ while (cursor.moveToNext()) {
+ const index = cursor.getColumnIndex(selection[0]);
+ const name = cursor.getString(index);
+ if (name === 'ns_tmp.txt') {
+ const idIndex = cursor.getColumnIndex(selection[1]);
+ const id = cursor.getLong(idIndex);
+ uri = android.net.Uri.parse(`${android.provider.MediaStore.Downloads.getContentUri('external').toString()}/${id}`);
+ cursor.close();
+ break;
+ }
+ }
+
+ if (!uri) {
+ const values = new android.content.ContentValues();
+ values.put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, 'ns_tmp.txt');
+ values.put(android.provider.MediaStore.MediaColumns.MIME_TYPE, 'text/plain');
+ uri = activity.getContentResolver().insert(android.provider.MediaStore.Downloads.getContentUri('external'), values);
+ }
+
+ doWork(uri.toString());
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+function doWork(path: string) {
+ try {
+ const file = File.fromPath(path) as File;
+ console.log('name: ', file.name);
+ console.log('path: ', file.path);
+ console.log('parent: ', file.parent);
+ console.log('size: ', file.size);
+ console.log('lastModified: ', file.lastModified);
+ console.log('extension: ', file.extension);
+ if (file.size > 0) {
+ console.log('current text: ', file.readTextSync());
+ } else {
+ file.writeTextSync('Hello World');
+ console.log('after write: ', file.readTextSync());
+ console.log('after write size: ', file.size);
+ }
+
+ file.renameSync(`ns_temp_${Date.now()}.txt`);
+
+ console.log('rename: ', file.name);
+ console.log('rename lastModified: ', file.lastModified);
+
+ file.removeSync();
+
+ console.log('deleted ?', !File.exists(path));
+ } catch (e) {
+ console.error(e);
+ }
+}
diff --git a/apps/toolbox/src/pages/fs-helper.xml b/apps/toolbox/src/pages/fs-helper.xml
new file mode 100644
index 0000000000..980cae5067
--- /dev/null
+++ b/apps/toolbox/src/pages/fs-helper.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/core/file-system/file-system-access.android.ts b/packages/core/file-system/file-system-access.android.ts
index 013e1206f4..75384f7ca2 100644
--- a/packages/core/file-system/file-system-access.android.ts
+++ b/packages/core/file-system/file-system-access.android.ts
@@ -1,6 +1,8 @@
import * as textModule from '../text';
import { getNativeApplication } from '../application';
+import type { IFileSystemAccess } from './file-system-access';
+
let applicationContext: android.content.Context;
function getApplicationContext() {
if (!applicationContext) {
@@ -10,7 +12,18 @@ function getApplicationContext() {
return applicationContext;
}
-export class FileSystemAccess {
+function getOrSetHelper(path: string): org.nativescript.widgets.FileHelper {
+ return org.nativescript.widgets.FileHelper.fromString(applicationContext, path);
+}
+
+function isContentUri(path: string): boolean {
+ if (typeof path === 'string' && path.startsWith('content:')) {
+ return true;
+ }
+ return false;
+}
+
+export class FileSystemAccess implements IFileSystemAccess {
private _pathSeparator = '/';
public getLastModified(path: string): Date {
@@ -592,3 +605,313 @@ export class FileSystemAccess {
return result;
}
}
+
+export class FileSystemAccess29 extends FileSystemAccess {
+ getLastModified(path: string): Date {
+ if (isContentUri(path)) {
+ return new Date(getOrSetHelper(path).getLastModified() * 1000);
+ }
+ return super.getLastModified(path);
+ }
+
+ getFileSize(path: string): number {
+ if (isContentUri(path)) {
+ return getOrSetHelper(path).getSize();
+ }
+ return super.getFileSize(path);
+ }
+
+ getParent(path: string, onError?: (error: any) => any): { path: string; name: string } {
+ if (isContentUri(path)) {
+ return null;
+ }
+ return super.getParent(path, onError);
+ }
+ getFile(path: string, onError?: (error: any) => any): { path: string; name: string; extension: string } {
+ if (isContentUri(path)) {
+ try {
+ const file = getOrSetHelper(path);
+ return {
+ path,
+ name: file.getName(),
+ extension: file.getExtension(),
+ };
+ } catch (e) {
+ if (typeof onError === 'function') {
+ onError(e);
+ }
+ return;
+ }
+ }
+ return super.getFile(path, onError);
+ }
+ getFolder(path: string, onError?: (error: any) => any): { path: string; name: string } {
+ if (isContentUri(path)) {
+ return null;
+ }
+ return super.getFolder(path, onError);
+ }
+ getEntities(path: string, onError?: (error: any) => any): { path: string; name: string; extension: string }[] {
+ if (isContentUri(path)) {
+ return null;
+ }
+ return super.getEntities(path, onError);
+ }
+ eachEntity(path: string, onEntity: (entity: { path: string; name: string; extension: string }) => boolean, onError?: (error: any) => any) {
+ if (isContentUri(path)) {
+ return null;
+ }
+ super.eachEntity(path, onEntity);
+ }
+ fileExists(path: string): boolean {
+ if (isContentUri(path)) {
+ return org.nativescript.widgets.FileHelper.exists(applicationContext, path);
+ }
+ return super.fileExists(path);
+ }
+ folderExists(path: string): boolean {
+ if (isContentUri(path)) {
+ return null;
+ }
+ return super.folderExists(path);
+ }
+ deleteFile(path: string, onError?: (error: any) => any) {
+ if (isContentUri(path)) {
+ try {
+ getOrSetHelper(path).delete(applicationContext);
+ } catch (e) {
+ onError?.(e);
+ }
+ } else {
+ super.deleteFile(path, onError);
+ }
+ }
+ deleteFolder(path: string, onError?: (error: any) => any) {
+ if (!isContentUri(path)) {
+ super.deleteFolder(path, onError);
+ }
+ }
+ emptyFolder(path: string, onError?: (error: any) => any): void {
+ if (!isContentUri(path)) {
+ super.emptyFolder(path, onError);
+ }
+ }
+ rename(path: string, newPath: string, onError?: (error: any) => any): void {
+ 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).renameSync(applicationContext, newPath, callback);
+ } else {
+ super.rename(path, newPath, onError);
+ }
+ }
+
+ public renameAsync(path: string, newPath: string): Promise {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).renameSync(
+ applicationContext,
+ newPath,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+
+ getDocumentsFolderPath(): string {
+ return super.getDocumentsFolderPath();
+ }
+ getTempFolderPath(): string {
+ return super.getDocumentsFolderPath();
+ }
+ getLogicalRootPath(): string {
+ return super.getDocumentsFolderPath();
+ }
+ getCurrentAppPath(): string {
+ return super.getDocumentsFolderPath();
+ }
+ public readText = this.readTextSync.bind(this);
+
+ readTextAsync(path: string, encoding?: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).readText(
+ applicationContext,
+ encoding ?? null,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve(result);
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.readTextAsync(path, encoding);
+ }
+ readTextSync(path: string, onError?: (error: any) => any, encoding?: any): string {
+ if (isContentUri(path)) {
+ let callback = null;
+ if (typeof onError === 'function') {
+ callback = new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {},
+ onError(error) {
+ onError(error);
+ },
+ });
+ }
+ return getOrSetHelper(path).readTextSync(applicationContext, encoding ?? null, callback);
+ } else {
+ return super.readTextSync(path, onError, encoding);
+ }
+ }
+
+ read = this.readSync.bind(this);
+
+ readAsync(path: string): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).read(
+ applicationContext,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve(result);
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.readAsync(path);
+ }
+
+ readSync(path: string, onError?: (error: any) => any) {
+ if (isContentUri(path)) {
+ let callback = null;
+ if (typeof onError === 'function') {
+ callback = new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {},
+ onError(error) {
+ onError(error);
+ },
+ });
+ }
+ return getOrSetHelper(path).readSync(applicationContext, callback);
+ }
+ return super.readSync(path, onError);
+ }
+
+ writeText = this.writeTextSync.bind(this);
+
+ writeTextAsync(path: string, content: string, encoding?: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).writeText(
+ applicationContext,
+ content,
+ encoding ?? null,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.writeTextAsync(path, content, encoding);
+ }
+
+ writeTextSync(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).writeTextSync(applicationContext, content, encoding ?? null, callback);
+ } else {
+ super.writeTextSync(path, content, onError);
+ }
+ }
+
+ write = this.writeSync.bind(this);
+
+ writeAsync(path: string, content: any): Promise {
+ if (isContentUri(path)) {
+ return new Promise((resolve, reject) => {
+ getOrSetHelper(path).write(
+ applicationContext,
+ content,
+ new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {
+ resolve();
+ },
+ onError(error) {
+ reject(error);
+ },
+ })
+ );
+ });
+ }
+ return super.writeAsync(path, content);
+ }
+
+ writeSync(path: string, content: any, onError?: (error: any) => any) {
+ if (isContentUri(path)) {
+ let callback = null;
+ if (typeof onError === 'function') {
+ callback = new org.nativescript.widgets.FileHelper.Callback({
+ onSuccess(result) {},
+ onError(error) {
+ onError(error);
+ },
+ });
+ }
+ getOrSetHelper(path).writeSync(applicationContext, content, callback);
+ } else {
+ super.writeSync(path, content, onError);
+ }
+ }
+
+ getFileExtension(path: string): string {
+ if (isContentUri(path)) {
+ return getOrSetHelper(path).getExtension();
+ }
+ return super.getFileExtension(path);
+ }
+ getPathSeparator(): string {
+ return super.getPathSeparator();
+ }
+ normalizePath(path: string): string {
+ return super.normalizePath(path);
+ }
+ joinPath(left: string, right: string): string {
+ return super.joinPath(left, right);
+ }
+ joinPaths(paths: string[]): string {
+ return super.joinPaths(paths);
+ }
+}
diff --git a/packages/core/file-system/file-system-access.d.ts b/packages/core/file-system/file-system-access.d.ts
index bec55401dc..3f69fbde21 100644
--- a/packages/core/file-system/file-system-access.d.ts
+++ b/packages/core/file-system/file-system-access.d.ts
@@ -1,7 +1,7 @@
/**
* An utility class used to provide methods to access and work with the file system.
*/
-export class FileSystemAccess {
+export interface IFileSystemAccess {
/**
* Gets the last modified date of a file with a given path.
* @param path Path to the file.
@@ -248,3 +248,75 @@ export class FileSystemAccess {
*/
joinPaths(paths: string[]): string;
}
+
+export class FileSystemAccess implements IFileSystemAccess {
+ getLastModified(path: string): Date;
+
+ getFileSize(path: string): number;
+
+ getParent(path: string, onError?: (error: any) => any): { path: string; name: string };
+
+ getFile(path: string, onError?: (error: any) => any): { path: string; name: string; extension: string };
+
+ getFolder(path: string, onError?: (error: any) => any): { path: string; name: string };
+
+ getEntities(path: string, onError?: (error: any) => any): Array<{ path: string; name: string; extension: string }>;
+
+ eachEntity(path: string, onEntity: (entity: { path: string; name: string; extension: string }) => boolean, onError?: (error: any) => any);
+
+ fileExists(path: string): boolean;
+
+ folderExists(path: string): boolean;
+
+ deleteFile(path: string, onError?: (error: any) => any);
+
+ deleteFolder(path: string, onError?: (error: any) => any);
+
+ emptyFolder(path: string, onError?: (error: any) => any): void;
+
+ rename(path: string, newPath: string, onError?: (error: any) => any): void;
+
+ getDocumentsFolderPath(): string;
+
+ getTempFolderPath(): string;
+
+ getLogicalRootPath(): string;
+
+ getCurrentAppPath(): string;
+
+ readText(path: string, onError?: (error: any) => any, encoding?: any): string;
+
+ readTextAsync(path: string, encoding?: any): Promise;
+
+ readTextSync(path: string, onError?: (error: any) => any, encoding?: any): string;
+
+ read(path: string, onError?: (error: any) => any): any;
+
+ readAsync(path: string): Promise;
+
+ readSync(path: string, onError?: (error: any) => any): any;
+
+ writeText(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
+ writeTextAsync(path: string, content: string, encoding?: any): Promise;
+
+ writeTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
+
+ write(path: string, content: any, onError?: (error: any) => any);
+
+ writeAsync(path: string, content: any): Promise;
+
+ writeSync(path: string, content: any, onError?: (error: any) => any);
+
+ getFileExtension(path: string): string;
+
+ getPathSeparator(): string;
+
+ normalizePath(path: string): string;
+
+ joinPath(left: string, right: string): string;
+
+ joinPaths(paths: string[]): string;
+}
+
+export class FileSystemAccess29 extends FileSystemAccess {}
diff --git a/packages/core/file-system/index.ts b/packages/core/file-system/index.ts
index 574682378f..7944e55000 100644
--- a/packages/core/file-system/index.ts
+++ b/packages/core/file-system/index.ts
@@ -1,15 +1,19 @@
-import { FileSystemAccess } from './file-system-access';
-
+import { IFileSystemAccess, FileSystemAccess, FileSystemAccess29 } from './file-system-access';
+import { Device } from '../platform';
// The FileSystemAccess implementation, used through all the APIs.
-let fileAccess: FileSystemAccess;
+let fileAccess: IFileSystemAccess;
/**
* 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
*/
-export function getFileAccess(): FileSystemAccess {
+export function getFileAccess(): IFileSystemAccess {
if (!fileAccess) {
- fileAccess = new FileSystemAccess();
+ if (global.isAndroid && parseInt(Device.sdkVersion) >= 29) {
+ fileAccess = new FileSystemAccess29();
+ } else {
+ fileAccess = new FileSystemAccess();
+ }
}
return fileAccess;
@@ -161,12 +165,7 @@ export class FileSystemEntity {
}
get lastModified(): Date {
- let value = this._lastModified;
- if (!this._lastModified) {
- value = this._lastModified = getFileAccess().getLastModified(this.path);
- }
-
- return value;
+ return getFileAccess().getLastModified(this.path);
}
}
@@ -204,7 +203,7 @@ export class File extends FileSystemEntity {
public read(): Promise {
return new Promise((resolve, reject) => {
try {
- this.checkAccess();
+ this._checkAccess();
} catch (ex) {
reject(ex);
@@ -229,7 +228,7 @@ export class File extends FileSystemEntity {
}
public readSync(onError?: (error: any) => any): any {
- this.checkAccess();
+ this._checkAccess();
this._locked = true;
@@ -251,7 +250,7 @@ export class File extends FileSystemEntity {
public write(content: any): Promise {
return new Promise((resolve, reject) => {
try {
- this.checkAccess();
+ this._checkAccess();
} catch (ex) {
reject(ex);
@@ -276,7 +275,7 @@ export class File extends FileSystemEntity {
}
public writeSync(content: any, onError?: (error: any) => any): void {
- this.checkAccess();
+ this._checkAccess();
try {
this._locked = true;
@@ -298,7 +297,7 @@ export class File extends FileSystemEntity {
public readText(encoding?: string): Promise {
return new Promise((resolve, reject) => {
try {
- this.checkAccess();
+ this._checkAccess();
} catch (ex) {
reject(ex);
@@ -323,7 +322,7 @@ export class File extends FileSystemEntity {
}
public readTextSync(onError?: (error: any) => any, encoding?: string): string {
- this.checkAccess();
+ this._checkAccess();
this._locked = true;
@@ -344,7 +343,7 @@ export class File extends FileSystemEntity {
public writeText(content: string, encoding?: string): Promise {
return new Promise((resolve, reject) => {
try {
- this.checkAccess();
+ this._checkAccess();
} catch (ex) {
reject(ex);
@@ -369,7 +368,7 @@ export class File extends FileSystemEntity {
}
public writeTextSync(content: string, onError?: (error: any) => any, encoding?: string): void {
- this.checkAccess();
+ this._checkAccess();
try {
this._locked = true;
@@ -388,7 +387,7 @@ export class File extends FileSystemEntity {
}
}
- private checkAccess() {
+ _checkAccess() {
if (this.isLocked) {
throw new Error('Cannot access a locked file.');
}
diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar
index 6df5b6aaf9..88a866e404 100644
Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ
diff --git a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts
index 85224251c7..54a9be7aeb 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
@@ -635,6 +635,53 @@
}
}
+declare module org {
+ export module nativescript {
+ export module widgets {
+ export class FileHelper {
+ public static class: java.lang.Class;
+ public readText(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public writeSync(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public static fromString(param1: globalAndroid.content.Context, param0: string): org.nativescript.widgets.FileHelper;
+ public writeText(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
+ public writeTextSync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
+ public copyToFileSync(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): boolean;
+ public getName(): string;
+ public read(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): void;
+ public copyToFile(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public static fromUri(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): org.nativescript.widgets.FileHelper;
+ public readSync(param0: globalAndroid.content.Context, param1: org.nativescript.widgets.FileHelper.Callback): androidNative.Array;
+ public write(param0: globalAndroid.content.Context, param1: androidNative.Array, param2: org.nativescript.widgets.FileHelper.Callback): void;
+ public getSize(): number;
+ public getMime(): string;
+ public readTextSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string;
+ public delete(param0: globalAndroid.content.Context): boolean;
+ public static exists(param0: globalAndroid.content.Context, param1: string): boolean;
+ public static exists(param0: globalAndroid.content.Context, param1: globalAndroid.net.Uri): boolean;
+ public getExtension(): string;
+ public getLastModified(): number;
+ public renameSync(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string;
+ public rename(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): string;
+ }
+ export module FileHelper {
+ export class Callback {
+ public static class: java.lang.Class;
+ /**
+ * Constructs a new instance of the org.nativescript.widgets.FileHelper$Callback interface with the provided implementation. An empty constructor exists calling super() when extending the interface class.
+ */
+ public constructor(implementation: {
+ onError(param0: java.lang.Exception): void;
+ onSuccess(param0: any): void;
+ });
+ public constructor();
+ public onError(param0: java.lang.Exception): void;
+ public onSuccess(param0: any): void;
+ }
+ }
+ }
+ }
+}
+
declare module org {
export module nativescript {
export module widgets {
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
new file mode 100644
index 0000000000..93ebda0e8f
--- /dev/null
+++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FileHelper.java
@@ -0,0 +1,388 @@
+package org.nativescript.widgets;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.Nullable;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+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.getMainLooper());
+ this.uri = uri;
+ }
+
+ 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) {
+ Cursor cursor = context.getContentResolver()
+ .query(uri, null, null, null, null);
+
+ 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 ignored) {
+ return null;
+ }
+
+ }
+
+ public static @Nullable
+ FileHelper fromUri(Context context, Uri uri) {
+ Cursor cursor = context.getContentResolver()
+ .query(uri, null, null, null, null);
+
+ int sizeIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.SIZE
+ );
+
+ int nameIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DISPLAY_NAME
+ );
+
+ int lastModifiedIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DATE_MODIFIED
+ );
+
+
+ boolean moved = cursor.moveToFirst();
+ FileHelper helper = null;
+ if (moved) {
+ 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 updateInternal(Context context, boolean force) {
+
+ if (force) {
+ // trigger db update
+ 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);
+ }
+ }
+
+ Cursor cursor = context.getContentResolver()
+ .query(uri, null, null, null, null);
+
+ int sizeIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.SIZE
+ );
+
+ int nameIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DISPLAY_NAME
+ );
+
+ int lastModifiedIndex = cursor.getColumnIndex(
+ MediaStore.MediaColumns.DATE_MODIFIED
+ );
+
+
+ boolean moved = cursor.moveToFirst();
+
+ if (moved) {
+ 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 byte[] readSyncInternal(Context context) throws Exception {
+ InputStream is = context.getContentResolver().openInputStream(uri);
+ byte[] array = new byte[(int) size];
+ is.read(array);
+ is.close();
+ return array;
+ }
+
+ 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));
+ }
+ });
+ }
+
+ private String readTextSyncInternal(Context context, @Nullable String encoding) throws Exception {
+ String characterSet = encoding;
+ if (characterSet == null) {
+ characterSet = "UTF-8";
+ }
+
+ InputStream is = context.getContentResolver().openInputStream(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 = context.getContentResolver().openOutputStream(uri);
+ os.write(content, 0, content.length);
+ 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));
+ }
+ });
+ }
+
+ private void writeTextSyncInternal(Context context, String content, @Nullable String encoding) throws Exception {
+ OutputStream os = context.getContentResolver().openOutputStream(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 = context.getContentResolver().openInputStream(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 {
+ return context.getContentResolver().delete(uri, null, null) > 0;
+ } catch (SecurityException 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 >= 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));
+ }
+ });
+ }
+
+}