diff --git a/doc/android-filesaver-plugin.md b/doc/android-filesaver-plugin.md new file mode 100644 index 00000000..2c1180b2 --- /dev/null +++ b/doc/android-filesaver-plugin.md @@ -0,0 +1,251 @@ +# Android File Saver Plugin Implementation Guide + +## Overview + +This document outlines the implementation of the `AndroidFileSaver` Capacitor plugin that provides Storage Access Framework (SAF) and MediaStore functionality for direct file saving on Android devices. + +## Plugin Purpose + +The `AndroidFileSaver` plugin enables two key file operations: +1. **`saveToDownloads`**: Direct save to Downloads folder using MediaStore (API 29+) +2. **`saveAs`**: User-chosen location using Storage Access Framework (SAF) + +## Implementation Requirements + +### 1. Plugin Structure + +```typescript +// Plugin interface +interface AndroidFileSaverPlugin { + saveToDownloads(options: { + fileName: string; + content: string; + mimeType: string + }): Promise<{ + success: boolean; + path?: string; + error?: string + }>; + + saveAs(options: { + fileName: string; + content: string; + mimeType: string + }): Promise<{ + success: boolean; + path?: string; + error?: string + }>; +} +``` + +### 2. Android Implementation + +#### MediaStore for Downloads (API 29+) + +```java +@PluginMethod +public void saveToDownloads(PluginCall call) { + String fileName = call.getString("fileName"); + String content = call.getString("content"); + String mimeType = call.getString("mimeType"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Use MediaStore for Downloads + ContentValues values = new ContentValues(); + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + values.put(MediaStore.Downloads.MIME_TYPE, mimeType); + values.put(MediaStore.Downloads.IS_PENDING, 1); + + ContentResolver resolver = getContext().getContentResolver(); + Uri uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + + if (uri != null) { + try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) { + if (pfd != null) { + try (FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor())) { + fos.write(content.getBytes()); + fos.flush(); + + // Mark as no longer pending + values.clear(); + values.put(MediaStore.Downloads.IS_PENDING, 0); + resolver.update(uri, values, null, null); + + call.resolve(new JSObject() + .put("success", true) + .put("path", uri.toString())); + return; + } + } + } catch (IOException e) { + resolver.delete(uri, null, null); + } + } + + call.resolve(new JSObject() + .put("success", false) + .put("error", "Failed to save file")); + } else { + // Fallback for older Android versions + call.resolve(new JSObject() + .put("success", false) + .put("error", "Requires Android API 29+")); + } +} +``` + +#### Storage Access Framework (SAF) for Save As + +```java +@PluginMethod +public void saveAs(PluginCall call) { + String fileName = call.getString("fileName"); + String content = call.getString("content"); + String mimeType = call.getString("mimeType"); + + // Store call for later use + bridge.saveCall(call); + + // Create intent for SAF + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + + // Start activity for result + bridge.startActivityForResult(call, intent, "saveAsResult"); +} + +@ActivityCallback +private void saveAsResult(PluginCall call, ActivityResult result) { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri uri = result.getData().getData(); + String content = call.getString("content"); + + try (ParcelFileDescriptor pfd = getContext().getContentResolver() + .openFileDescriptor(uri, "w")) { + if (pfd != null) { + try (FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor())) { + fos.write(content.getBytes()); + fos.flush(); + + call.resolve(new JSObject() + .put("success", true) + .put("path", uri.toString())); + return; + } + } + } catch (IOException e) { + // Handle error + } + + call.resolve(new JSObject() + .put("success", false) + .put("error", "Failed to save file")); + } else { + call.resolve(new JSObject() + .put("success", false) + .put("error", "User cancelled or failed")); + } +} +``` + +### 3. Plugin Registration + +```java +@CapacitorPlugin(name = "AndroidFileSaver") +public class AndroidFileSaverPlugin extends Plugin { + // Implementation methods here +} +``` + +## Integration Steps + +### 1. Create Plugin Project + +```bash +# Create new Capacitor plugin +npx @capacitor/cli plugin:generate android-file-saver +cd android-file-saver +``` + +### 2. Add to Android Project + +```bash +# In your main project +npm install ./android-file-saver +npx cap sync android +``` + +### 3. Update Android Manifest + +Ensure the following permissions are present: + +```xml + + +``` + +## Fallback Behavior + +When the plugin is not available, the system falls back to: +1. **Web**: Browser download mechanism +2. **Electron**: Native file save via IPC +3. **Capacitor**: Share dialog (existing behavior) + +## Testing + +### 1. Plugin Availability + +```typescript +// Check if plugin is available +if (AndroidFileSaver) { + // Plugin available - use native methods +} else { + // Plugin not available - use fallback +} +``` + +### 2. Error Handling + +```typescript +try { + const result = await AndroidFileSaver.saveToDownloads({ + fileName: "test.json", + content: '{"test": "data"}', + mimeType: "application/json" + }); + + if (result.success) { + logger.info("File saved:", result.path); + } else { + logger.error("Save failed:", result.error); + } +} catch (error) { + logger.error("Plugin error:", error); +} +``` + +## Security Considerations + +1. **Content Validation**: Validate file content before saving +2. **MIME Type Verification**: Ensure MIME type matches file content +3. **Permission Handling**: Request storage permissions appropriately +4. **Error Logging**: Log errors without exposing sensitive data + +## Future Enhancements + +1. **Progress Callbacks**: Add progress reporting for large files +2. **Batch Operations**: Support saving multiple files +3. **Custom Locations**: Allow saving to app-specific directories +4. **File Compression**: Add optional file compression + +## References + +- [Android Storage Access Framework](https://developer.android.com/guide/topics/providers/document-provider) +- [MediaStore Downloads](https://developer.android.com/reference/android/provider/MediaStore.Downloads) +- [Capacitor Plugin Development](https://capacitorjs.com/docs/plugins/android) +- [Android Scoped Storage](https://developer.android.com/training/data-storage) diff --git a/scripts/git-hooks/debug-checker.config b/scripts/git-hooks/debug-checker.config index e9bd016d..1301ea87 100644 --- a/scripts/git-hooks/debug-checker.config +++ b/scripts/git-hooks/debug-checker.config @@ -61,6 +61,22 @@ SKIP_PATTERNS=( "\.yaml$" # YAML config files ) +# Files that are whitelisted for console statements +# These files may contain intentional console.log statements that are +# properly whitelisted with eslint-disable-next-line no-console comments +WHITELIST_FILES=( + "src/services/platforms/WebPlatformService.ts" # Worker context logging + "src/services/platforms/CapacitorPlatformService.ts" # Platform-specific logging + "src/services/platforms/ElectronPlatformService.ts" # Electron-specific logging + "src/services/QRScanner/.*" # QR Scanner services + "src/utils/logger.ts" # Logger utility itself + "src/utils/LogCollector.ts" # Log collection utilities + "scripts/.*" # Build and utility scripts + "test-.*/.*" # Test directories + ".*\.test\..*" # Test files + ".*\.spec\..*" # Spec files +) + # Logging level (debug, info, warn, error) LOG_LEVEL="info" diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit index 6239c7fc..f783c953 100755 --- a/scripts/git-hooks/pre-commit +++ b/scripts/git-hooks/pre-commit @@ -18,6 +18,11 @@ DEFAULT_DEBUG_PATTERNS=( "