# 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)