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
252 lines
7.3 KiB
Markdown
252 lines
7.3 KiB
Markdown
# 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
|
|
<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:
|
|
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)
|