Browse Source
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 servicesandroid-file-save
7 changed files with 800 additions and 20 deletions
@ -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 |
|||
<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) |
Loading…
Reference in new issue