You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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:

  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

// 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:

  1. Web: Browser download mechanism
  2. Electron: Native file save via IPC
  3. 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

  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