Skip to content

Commit

Permalink
Plugin implementation for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Sep 25, 2021
1 parent e952652 commit dce365c
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 0 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# cordova-plugin-save-dialog

This Cordova plugin for Android displays the native Save dialog which allows users to store a file in the selected location. The plugin utilizes the Storage Access Framework to save a file in a user-selected location as described in the [Android developer guide](https://developer.android.com/training/data-storage/shared/documents-files#create-file). Platforms other than Android are not supported currently.

## Installation

```
cordova plugin add cordova-plugin-save-dialog --save
```

## API

The plugin’s functionality is accessible through the object `cordova.plugins.saveDialog`.

### saveFile

Call this method to open the Save dialog and store raw contents in a file. The method accepts two arguments:

* file contents as a Blob instance,
* optional file name to display on default (the user may change it manually though).

To construct a Blob representation for a file contents, either use the [Blob constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob) directly:

```javascript
let blob = new Blob(["file contents"], {type: "text/plain"});
let fileName = "my-file.txt";
cordova.plugins.saveDialog.saveFile(blob, fileName).then(() => {
console.info("The file has been successfully saved");
}).catch(reason => {
console.warn(reason);
});
```

or apply other methods of blob generation (such as [Response.blob()](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob) for a network-fetched content):

```javascript
try {
let response = await fetch(`https://avatars.dicebear.com/api/avataaars/${Math.random()}.svg`);
let blob = await response.blob();
await cordova.plugins.saveDialog.saveFile(blob, "random-avatar.svg");
} catch (e) {
console.error(e);
}
```
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "cordova-plugin-save-dialog",
"version": "0.1.0",
"description": "Cordova plugin for Android to display the native Save dialog and store a file in the selected location",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Amphiluke/cordova-plugin-save-dialog.git"
},
"keywords": [
"ecosystem:cordova",
"cordova-android",
"save",
"dialog"
],
"author": "Amphiluke",
"license": "MIT",
"bugs": {
"url": "https://github.com/Amphiluke/cordova-plugin-save-dialog/issues"
},
"homepage": "https://github.com/Amphiluke/cordova-plugin-save-dialog#readme"
}
19 changes: 19 additions & 0 deletions plugin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="cordova-plugin-save-dialog" version="0.1.0">
<name>Save Dialog</name>
<description>Cordova plugin for Android to display the native Save dialog and store a file in the selected location</description>
<license>MIT</license>
<keywords>cordova,save,dialog</keywords>
<js-module src="www/save-dialog.js" name="SaveDialog">
<clobbers target="cordova.plugins.saveDialog" />
</js-module>

<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="SaveDialog" >
<param name="android-package" value="io.github.amphiluke.SaveDialog"/>
</feature>
</config-file>
<source-file src="src/android/SaveDialog.java" target-dir="src/io/github/amphiluke" />
</platform>
</plugin>
86 changes: 86 additions & 0 deletions src/android/SaveDialog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.github.amphiluke;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
// import android.provider.DocumentsContract;
import android.util.Base64;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;

import org.json.JSONArray;
import org.json.JSONException;

import java.io.FileOutputStream;

public class SaveDialog extends CordovaPlugin {
private static final int LOCATE_FILE = 1;

private CallbackContext callbackContext;

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
this.callbackContext = callbackContext;
if (action.equals("locateFile")) {
this.locateFile(args.getString(0), args.getString(1));
} else if (action.equals("saveFile")) {
this.saveFile(Uri.parse(args.getString(0)), args.getString(1));
} else {
return false;
}
return true;
}

private void locateFile(String type, String name) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(type);
intent.putExtra(Intent.EXTRA_TITLE, name);
// TODO Optionally, specify a URI for the directory that should be opened in
// the system file picker when your app creates the document.
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
cordova.startActivityForResult(this, intent, SaveDialog.LOCATE_FILE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == SaveDialog.LOCATE_FILE && this.callbackContext != null) {
if (resultCode == Activity.RESULT_CANCELED) {
this.callbackContext.error("The dialog has been cancelled");
} else if (resultCode == Activity.RESULT_OK && resultData != null) {
Uri uri = resultData.getData();
this.callbackContext.success(uri.toString());
} else {
this.callbackContext.error("Unknown error");
}
}
}

public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
this.callbackContext = callbackContext;
}

private void saveFile(Uri uri, String data) {
try {
byte[] rawData = Base64.decode(data, Base64.DEFAULT);
ParcelFileDescriptor pfd = cordova.getActivity().getContentResolver().openFileDescriptor(uri, "w");
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
try {
fileOutputStream.write(rawData);
this.callbackContext.success();
} catch (Exception e) {
this.callbackContext.error(e.getMessage());
e.printStackTrace();
} finally {
fileOutputStream.close();
pfd.close();
}
} catch (Exception e) {
this.callbackContext.error(e.getMessage());
e.printStackTrace();
}
}
}
23 changes: 23 additions & 0 deletions www/save-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
let exec = require("cordova/exec");
let moduleMapper = require("cordova/modulemapper");
let FileReader = moduleMapper.getOriginalSymbol(window, "FileReader") || window.FileReader;

module.exports = {
saveFile(blob, name = "") {
return new Promise((resolve, reject) => {
exec(resolve, reject, "SaveDialog", "locateFile", [blob.type || "application/octet-stream", name]);
}).then(uri => {
let reader = new FileReader();
reader.onload = () => {
exec(resolve, reject, "SaveDialog", "saveFile", [uri, reader.result]);
};
reader.onerror = () => {
reject(reader.error);
};
reader.onabort = () => {
reject("Blob reading has been aborted");
};
reader.readAsArrayBuffer(blob);
});
}
};

0 comments on commit dce365c

Please sign in to comment.