diff --git a/BUILDING.md b/BUILDING.md
index cc8f77b2..141dc3b2 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -215,6 +215,9 @@ Prerequisites: Android Studio with SDK installed
rm -rf dist
npm run build:web
npm run build:capacitor
+ cd android
+ ./gradlew clean
+ ./gradlew assembleDebug
```
2. Update Android project with latest build:
diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe
index 82c59317..44390a33 100644
Binary files a/android/.gradle/file-system.probe and b/android/.gradle/file-system.probe differ
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5442f779..70ac8410 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -39,4 +39,6 @@
+
+
diff --git a/package-lock.json b/package-lock.json
index 57cde520..cc5e7d01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12313,9 +12313,9 @@
}
},
"node_modules/bignumber.js": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.0.tgz",
- "integrity": "sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew==",
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz",
+ "integrity": "sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw==",
"license": "MIT",
"engines": {
"node": "*"
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 7a185017..68ce715d 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -3,7 +3,7 @@ import {
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
-import { Filesystem, Directory } from "@capacitor/filesystem";
+import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { FilePicker } from "@capawesome/capacitor-file-picker";
import { logger } from "../../utils/logger";
@@ -31,6 +31,113 @@ export class CapacitorPlatformService implements PlatformService {
};
}
+ /**
+ * Checks and requests storage permissions if needed
+ * @returns Promise that resolves when permissions are granted
+ * @throws Error if permissions are denied
+ */
+ private async checkStoragePermissions(): Promise {
+ try {
+ const logData = {
+ platform: this.getCapabilities().isIOS ? "iOS" : "Android",
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Checking storage permissions",
+ JSON.stringify(logData, null, 2),
+ );
+
+ if (this.getCapabilities().isIOS) {
+ // iOS uses different permission model
+ return;
+ }
+
+ // Try to access a test directory to check permissions
+ try {
+ await Filesystem.stat({
+ path: "/storage/emulated/0/Download",
+ directory: Directory.Documents,
+ });
+ logger.log(
+ "Storage permissions already granted",
+ JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
+ );
+ return;
+ } catch (error: unknown) {
+ const err = error as Error;
+ const errorLogData = {
+ error: {
+ message: err.message,
+ name: err.name,
+ stack: err.stack,
+ },
+ timestamp: new Date().toISOString(),
+ };
+
+ // "File does not exist" is expected and not a permission error
+ if (err.message === "File does not exist") {
+ logger.log(
+ "Directory does not exist (expected), proceeding with write",
+ JSON.stringify(errorLogData, null, 2),
+ );
+ return;
+ }
+
+ // Check for actual permission errors
+ if (
+ err.message.includes("permission") ||
+ err.message.includes("access")
+ ) {
+ logger.log(
+ "Permission check failed, requesting permissions",
+ JSON.stringify(errorLogData, null, 2),
+ );
+
+ // The Filesystem plugin will automatically request permissions when needed
+ // We just need to try the operation again
+ try {
+ await Filesystem.stat({
+ path: "/storage/emulated/0/Download",
+ directory: Directory.Documents,
+ });
+ logger.log(
+ "Storage permissions granted after request",
+ JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
+ );
+ return;
+ } catch (retryError: unknown) {
+ const retryErr = retryError as Error;
+ throw new Error(
+ `Failed to obtain storage permissions: ${retryErr.message}`,
+ );
+ }
+ }
+
+ // For any other error, log it but don't treat as permission error
+ logger.log(
+ "Unexpected error during permission check",
+ JSON.stringify(errorLogData, null, 2),
+ );
+ return;
+ }
+ } catch (error: unknown) {
+ const err = error as Error;
+ const errorLogData = {
+ error: {
+ message: err.message,
+ name: err.name,
+ stack: err.stack,
+ },
+ timestamp: new Date().toISOString(),
+ };
+ logger.error(
+ "Error checking/requesting permissions",
+ JSON.stringify(errorLogData, null, 2),
+ );
+ throw new Error(`Failed to obtain storage permissions: ${err.message}`);
+ }
+ }
+
/**
* Reads a file from the app's data directory.
* @param path - Relative path to the file in the app's data directory
@@ -57,48 +164,175 @@ export class CapacitorPlatformService implements PlatformService {
*/
async writeFile(path: string, content: string): Promise {
try {
+ const logData = {
+ targetPath: path,
+ contentLength: content.length,
+ platform: this.getCapabilities().isIOS ? "iOS" : "Android",
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Starting writeFile operation",
+ JSON.stringify(logData, null, 2),
+ );
+
+ // Check and request storage permissions if needed
+ await this.checkStoragePermissions();
+
// Let user pick save location first
const result = await FilePicker.pickDirectory();
- logger.log("FilePicker result path:", result.path);
-
+ const pickerLogData = {
+ path: result.path,
+ platform: this.getCapabilities().isIOS ? "iOS" : "Android",
+ timestamp: new Date().toISOString(),
+ };
+ logger.log("FilePicker result:", JSON.stringify(pickerLogData, null, 2));
+
// Handle paths based on platform
let cleanPath = result.path;
if (this.getCapabilities().isIOS) {
- // For iOS, keep content: prefix
+ const iosLogData = {
+ originalPath: cleanPath,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log("Processing iOS path", JSON.stringify(iosLogData, null, 2));
cleanPath = result.path;
} else {
- // For Android, extract the actual path from the content URI
- const pathMatch = result.path.match(/tree\/(.*?)(?:\/|$)/);
- logger.log("Path match result:", pathMatch);
- if (pathMatch) {
- const decodedPath = decodeURIComponent(pathMatch[1]);
- logger.log("Decoded path:", decodedPath);
- // Convert primary:Download to /storage/emulated/0/Download
- cleanPath = decodedPath.replace('primary:', '/storage/emulated/0/');
+ const androidLogData = {
+ originalPath: cleanPath,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Processing Android path",
+ JSON.stringify(androidLogData, null, 2),
+ );
+
+ // For Android, use the content URI directly
+ if (cleanPath.startsWith("content://")) {
+ const uriLogData = {
+ uri: cleanPath,
+ filename: path,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Using content URI for Android:",
+ JSON.stringify(uriLogData, null, 2),
+ );
+
+ // Extract the document ID from the content URI
+ const docIdMatch = cleanPath.match(/tree\/(.*?)(?:\/|$)/);
+ if (docIdMatch) {
+ const docId = docIdMatch[1];
+ const docIdLogData = {
+ docId,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Extracted document ID:",
+ JSON.stringify(docIdLogData, null, 2),
+ );
+
+ // Use the document ID as the path
+ cleanPath = docId;
+ }
}
}
-
- // For Android, ensure we're using the correct external storage path
- if (this.getCapabilities().isMobile && !this.getCapabilities().isIOS) {
- logger.log("Before Android path conversion:", cleanPath);
- cleanPath = cleanPath.replace('primary:', '/storage/emulated/0/');
- logger.log("After Android path conversion:", cleanPath);
- }
-
- const finalPath = `${cleanPath}/${path}`;
- logger.log("Final path for writeFile:", finalPath);
-
+
+ const finalPath = cleanPath;
+ const finalPathLogData = {
+ fullPath: finalPath,
+ filename: path,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Final path details:",
+ JSON.stringify(finalPathLogData, null, 2),
+ );
+
// Write to the selected directory
- await Filesystem.writeFile({
+ const writeLogData = {
path: finalPath,
- data: content,
- directory: Directory.External,
- recursive: true,
- });
+ contentLength: content.length,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Attempting file write:",
+ JSON.stringify(writeLogData, null, 2),
+ );
- } catch (error) {
- logger.error("Error saving file:", error);
- throw new Error("Failed to save file to selected location");
+ try {
+ if (this.getCapabilities().isIOS) {
+ await Filesystem.writeFile({
+ path: finalPath,
+ data: content,
+ directory: Directory.Documents,
+ recursive: true,
+ encoding: Encoding.UTF8,
+ });
+ } else {
+ // For Android, use the content URI directly
+ const androidPath = `Download/${path}`;
+ const directoryLogData = {
+ path: androidPath,
+ directory: Directory.ExternalStorage,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "Android path configuration:",
+ JSON.stringify(directoryLogData, null, 2),
+ );
+
+ await Filesystem.writeFile({
+ path: androidPath,
+ data: content,
+ directory: Directory.ExternalStorage,
+ recursive: true,
+ encoding: Encoding.UTF8,
+ });
+ }
+
+ const writeSuccessLogData = {
+ path: finalPath,
+ timestamp: new Date().toISOString(),
+ };
+ logger.log(
+ "File write successful",
+ JSON.stringify(writeSuccessLogData, null, 2),
+ );
+ } catch (writeError: unknown) {
+ const error = writeError as Error;
+ const writeErrorLogData = {
+ error: {
+ message: error.message,
+ name: error.name,
+ stack: error.stack,
+ },
+ path: finalPath,
+ contentLength: content.length,
+ timestamp: new Date().toISOString(),
+ };
+ logger.error(
+ "File write failed:",
+ JSON.stringify(writeErrorLogData, null, 2),
+ );
+ throw new Error(`Failed to write file: ${error.message}`);
+ }
+ } catch (error: unknown) {
+ const err = error as Error;
+ const finalErrorLogData = {
+ error: {
+ message: err.message,
+ name: err.name,
+ stack: err.stack,
+ },
+ timestamp: new Date().toISOString(),
+ };
+ logger.error(
+ "Error in writeFile operation:",
+ JSON.stringify(finalErrorLogData, null, 2),
+ );
+ throw new Error(
+ `Failed to save file to selected location: ${err.message}`,
+ );
}
}
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index a0921b40..86389c47 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -21,7 +21,10 @@ function safeStringify(obj: unknown) {
export const logger = {
log: (message: string, ...args: unknown[]) => {
- if (process.env.NODE_ENV !== "production" || process.env.VITE_PLATFORM === "capacitor") {
+ if (
+ process.env.NODE_ENV !== "production" ||
+ process.env.VITE_PLATFORM === "capacitor"
+ ) {
// eslint-disable-next-line no-console
console.log(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
@@ -29,7 +32,10 @@ export const logger = {
}
},
warn: (message: string, ...args: unknown[]) => {
- if (process.env.NODE_ENV !== "production" || process.env.VITE_PLATFORM === "capacitor") {
+ if (
+ process.env.NODE_ENV !== "production" ||
+ process.env.VITE_PLATFORM === "capacitor"
+ ) {
// eslint-disable-next-line no-console
console.warn(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";