forked from trent_larson/crowd-funder-for-time-pwa
Add new platform service methods for direct file saving alongside existing share functionality: - Add saveToDevice() and saveAs() methods to PlatformService interface - Implement cross-platform support in WebPlatformService, ElectronPlatformService, and CapacitorPlatformService - Update DataExportSection UI to provide Share Contacts and Save to Device buttons - Add AndroidFileSaver plugin architecture with fallback implementation - Include comprehensive documentation for native Android plugin implementation This addresses the Android simulator file sharing limitation by providing users with clear choices between app-to-app sharing and direct device storage, while maintaining backward compatibility across all platforms. - CapacitorPlatformService: Add MediaStore/SAF support with graceful fallbacks - UI Components: Replace single download button with dual-action interface - Documentation: Add AndroidFileSaver plugin implementation guide - Type Safety: Maintain interface consistency across all platform services
7.3 KiB
7.3 KiB
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:
saveToDownloads: Direct save to Downloads folder using MediaStore (API 29+)saveAs: User-chosen location using Storage Access Framework (SAF)
Implementation Requirements
1. Plugin Structure
// 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+)
@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
@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
@CapacitorPlugin(name = "AndroidFileSaver")
public class AndroidFileSaverPlugin extends Plugin {
// Implementation methods here
}
Integration Steps
1. Create Plugin Project
# Create new Capacitor plugin
npx @capacitor/cli plugin:generate android-file-saver
cd android-file-saver
2. Add to Android Project
# In your main project
npm install ./android-file-saver
npx cap sync android
3. Update Android Manifest
Ensure the following permissions are present:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
Fallback Behavior
When the plugin is not available, the system falls back to:
- Web: Browser download mechanism
- Electron: Native file save via IPC
- Capacitor: Share dialog (existing behavior)
Testing
1. Plugin Availability
// Check if plugin is available
if (AndroidFileSaver) {
// Plugin available - use native methods
} else {
// Plugin not available - use fallback
}
2. Error Handling
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
- Content Validation: Validate file content before saving
- MIME Type Verification: Ensure MIME type matches file content
- Permission Handling: Request storage permissions appropriately
- Error Logging: Log errors without exposing sensitive data
Future Enhancements
- Progress Callbacks: Add progress reporting for large files
- Batch Operations: Support saving multiple files
- Custom Locations: Allow saving to app-specific directories
- File Compression: Add optional file compression