9 changed files with 1176 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