Build a Cordova plugin for Android
Main post
This post forms part of this tutorial:
You might want to check that post first before continuing with this one. You will learn how to:
- Prepare the base plugin configuration
- Create the JavaScript interface
- Use the plugin in a Cordova app
You could find the source code of this tutorial on GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example
Start
We are going to implement in Java the 4 methods defined in our JavaScript interface www/CordovaPluginExample.js
:
- greeting
- countdownTimer
- writeFile
- bitcoinCurrentPrice
Create a new directory android
inside src
.
Then create the files CordovaPluginExample.java
and WriteFileHelper.java
inside android
.
π cordova-plugin-example
βββ π src
βββ π android
βββ π CordovaPluginExample.java
βββ π WriteFileHelper.java
WriteFileHelper.java
is a helper class to keep our code in CordovaPluginExample.java
cleaner.
External dependencies
We are going to use okhttp to obtain Bitcoin’s current price from https://api.coindesk.com/v1/bpi/currentprice.json . We could do this with JavaScript in the browser, but it’s a simple and easy example on how to use external dependencies.
We can add external dependencies in 2 ways:
- Using <framework> in
plugin.xml
. - Using <framework> in
plugin.xml
and a Gradle filebuild.gradle
.
<framework> in plugin.xml
Dependencies declared by this way, will be downloaded from Maven Central.
plugin.xml
<platform name="android">
...
<framework src="com.squareup.okhttp3:okhttp:4.9.0" />
...
</platform>
<framework> and a Gradle file
If you want more control and use other repositories, we need a Gradle file. Create the file build.gradle
inside src/android
.
π cordova-plugin-example
βββ π src
β βββ π android
β βββ π build.gradle
β βββ π CordovaPluginExample.java
β βββ π WriteFileHelper.java
βββ π plugin.xml
For okhttp
we only need to write this:
build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.9.0")
}
You can add any other repository that you need:
repositories {
mavenCentral()
jcenter()
maven { url "https://jitpack.io" }
...
}
Add this build.gradle
file as a <framework>
:
plugin.xml
<platform name="android">
...
<framework src="src/android/build.gradle" custom="true" type="gradleReference" />
...
</platform>
You can declare multiple <framework>
. For this example we only need one. If you decide to use a Gradle file, you could declare all your dependencies inside build.gradle
instead of using multiple <framework>
. That way you will have all your dependencies at one place.
Edit AndroidManifest.xml
We can use <config-file> tag to add new configurations and <edit-config> tag to edit existing configurations in AndroidManifest.xml
.
Declare permissions
For this plugin example, we only need the WRITE_EXTERNAL_STORAGE
permission. We have to add the permission at the root of the manifest, we use parent="/*"
for that.
<platform name="android">
...
<config-file parent="/*" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</config-file>
...
</platform>
You can add any XML inside the
<config-file>
tags, not just permissions.
Edit main activity
We don’t need to edit any configuration for this plugin, but I leave you an example:
<platform name="android">
...
<edit-config file="AndroidManifest.xml" target="/manifest/application/activity[@android:name='MainActivity']" mode="merge">
<activity android:name="MainActivity" android:label="@string/app_name">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter>
</activity>
</edit-config>
...
</platform>
With this configuration we tell Cordova to read a tag <activity>
with the property android:name="MainActivity"
inside the tags <manifest> > <application>
. Since we are using mode="merge"
, it will merge existing configuration with our configuration (modify matches and add the configuration without matches).
You could check if your XML configuration works, by checking the
AndroidManifest.xml
generated by your Cordova app atplatforms/android/app/src/main/AndroidManifest.xml
.
Final plugin.xml configuration
For this plugin example, this is our final plugin.xml configuration for Android:
<!-- android -->
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="CordovaPluginExample">
<param name="android-package" value="com.cordova.plugin.example.CordovaPluginExample" />
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</config-file>
<source-file src="src/android/CordovaPluginExample.java" target-dir="src/com/cordova/plugin/example" />
<source-file src="src/android/WriteFileHelper.java" target-dir="src/com/cordova/plugin/example" />
<framework src="com.squareup.okhttp3:okhttp:4.9.0" />
<!-- <framework src="src/android/build.gradle" custom="true" type="gradleReference" /> -->
</platform>
The first <config-file>
is used to edit a cordova-android
related file (res/xml/config.xml
) used to register our plugin main class in Cordova. You could find this file in your Cordova app, at the path platforms/android/app/src/main/res/xml/config.xml
<source-file>
is used to copy the source files to the app (platforms/android
). We will use target-dir
as our Java files package name (package com.cordova.plugin.example;
). We could copy directories too:
<source-file src="src/android/<directory-name>" target-dir="src/com/cordova/plugin/example/<directory-name>" />
Android source code structure
π cordova-plugin-example
βββ π src
β βββ π android
β β βββ π build.gradle
β β βββ π CordovaPluginExample.java
β β βββ π WriteFileHelper.java
β βββ π ios
β βββ π electron
βββ π www
β βββ π CordovaPluginExample.js
βββ π .gitignore
βββ π package.json
βββ π plugin.xml
βββ π README.md
Java source code
CordovaPluginExample.java (Main plugin class)
package com.cordova.plugin.example;
// imports...
public class CordovaPluginExample extends CordovaPlugin {
private final String TAG = "CordovaPluginExample";
private OkHttpClient http;
private WriteFileHelper writeFileHelper;
// This method will be called on Cordova initialization.
// Use it to initialize everything you may need later.
// In this example, we initialize the HTTP client OkHttpClient
// and our WriteFileHelper class.
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
Log.d(TAG, "Initializing Cordova plugin example");
super.initialize(cordova, webView);
http = new OkHttpClient();
writeFileHelper = new WriteFileHelper(this);
}
// This method will be called by the JavaScript interface.
// Remember what we wrote on it (www/CordovaPluginExample.js)
//
// Example:
// exports.greeting = function (successCallback, errorCallback, name) {
// exec(successCallback, errorCallback, PLUGIN_NAME, 'greeting', [name]);
// };
//
// Return "true" if you have an implementation for the received "action"
// Return "false" otherwise
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("greeting")) {
greeting(callbackContext, args);
return true;
}
if (action.equals("countdownTimer")) {
countdownTimer(callbackContext, args);
return true;
}
if (action.equals("writeFile")) {
writeFile(callbackContext, args);
return true;
}
if (action.equals("bitcoinCurrentPrice")) {
bitcoinCurrentPrice(callbackContext);
return true;
}
return false;
}
// This is our simplest example.
// Returns the string "Hello {name}!"
// or "Hello!" if a name is not received.
//
// callbackContext.success() can return any primitive
// value or a JSONObject or JSONArray.
private void greeting(final CallbackContext callbackContext, final JSONArray args) throws JSONException {
String response = (args.isNull(0))
? "Hello!"
: "Hello " + args.getString(0) + "!";
callbackContext.success(response);
}
// Returns a number every second, from "seconds" parameter value to 0.
//
// With this example we will see how can we return
// multiple values over time, like an Observable.
//
// With the Runnable class we are creating
// something similar to the setTimeout
// JavaScript function.
//
// To return multiple values, we need the PluginResult class
// and the method callbackContext.sendPluginResult().
// To keep the "connection" open, just set the property
// "keepCallback" of PluginResult to true
private void countdownTimer(final CallbackContext callbackContext, final JSONArray args) throws JSONException {
final Integer seconds = (!args.isNull(0) && args.getInt(0) > 0)
? args.getInt(0)
: 10;
Handler handler = new Handler(Looper.getMainLooper());
Runnable runnable = new Runnable() {
Integer secondsLeft = seconds;
@Override
public void run() {
boolean keepCallback = secondsLeft > 0;
final PluginResult result = new PluginResult(PluginResult.Status.OK, secondsLeft);
result.setKeepCallback(keepCallback);
callbackContext.sendPluginResult(result);
if (keepCallback) {
secondsLeft--;
handler.postDelayed(this, 1000);
} else {
handler.removeCallbacks(this);
}
}
};
handler.postDelayed(runnable, 0);
}
// With this method we can create and write a file in:
// - (Android 9-) Phone Documents directory
// - (Android 10+) User can choose the directory
private void writeFile(final CallbackContext callbackContext, final JSONArray args) throws JSONException {
writeFileHelper.writeFile(callbackContext, args);
}
// We will listen to Intent results and handle
// the "create file" intent result (Android 10+ only)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == WriteFileHelper.CREATE_FILE_CODE) {
writeFileHelper.onCreateFileActivityResult(resultCode, data);
}
}
// This example is just to show how can we use
// the external dependency we declared previously
// in the plugin.xml file with <framework>,
// in this case, OkHttpClient
private void bitcoinCurrentPrice(final CallbackContext callbackContext) {
Request request = new Request.Builder()
.url("https://api.coindesk.com/v1/bpi/currentprice.json")
.build();
try (Response response = http.newCall(request).execute()) {
JSONObject jsonObject = new JSONObject(response.body().string());
callbackContext.success(jsonObject);
} catch (IOException | JSONException e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
callbackContext.error("REQUEST_ERROR");
}
}
}
WriteFileHelper.java
package com.cordova.plugin.example;
// imports...
public class WriteFileHelper {
// Code used with the create file Intent
public static Integer CREATE_FILE_CODE = 1;
private CordovaPlugin plugin;
private CallbackContext intentCallbackContext;
private String intentText;
// We need to pass as a parameter our main class to be
// able to access the main Android Activity later on
public WriteFileHelper(CordovaPlugin plugin) {
this.plugin = plugin;
}
// This is the main method of this helper class
// We will have different implementations for
// Android 9 and lower and Android 10 and higher
public void writeFile(final CallbackContext callbackContext, final JSONArray args) throws JSONException {
if (args.isNull(0) || args.isNull(1)) {
callbackContext.error("BAD_ARGS");
return;
}
String fileName = args.getString(0);
String text = args.getString(1);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) {
writeFileAndroid9AndLower(callbackContext, fileName, text);
} else {
writeFileAndroid10AndHigher(callbackContext, fileName, text);
}
}
// getExternalStoragePublicDirectory is deprecated in newer versions of Android
// https://developer.android.com/reference/android/os/Environment#getExternalStoragePublicDirectory(java.lang.String)
private void writeFileAndroid9AndLower(
final CallbackContext callbackContext,
final String fileName,
final String text
) {
try {
File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, fileName);
if (!file.exists()) {
if (!file.createNewFile()) {
throw new IOException("Error creating file");
}
}
FileWriter fileWriter = new FileWriter(file);
fileWriter.append(text);
callbackContext.success();
} catch (IOException e) {
callbackContext.error("WRITE_ERROR");
}
}
// For Android 10 and higher, we are going to create
// files using Android Intents.
// We will create the file with an Intent, and after
// the file is created we will write on it.
// https://developer.android.com/training/data-storage/shared/documents-files#create-file
@TargetApi(Build.VERSION_CODES.O)
private void writeFileAndroid10AndHigher(
final CallbackContext callbackContext,
final String fileName,
final String text
) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
intent.putExtra("text", text);
// Optionally, specify a URI for the directory that should be opened by
// the system's file picker when your app creates the document.
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, "");
// We need to save the callbackContext and text
// to use it in onCreateFileActivityResult()
intentCallbackContext = callbackContext;
intentText = text;
plugin.cordova.setActivityResultCallback(plugin);
plugin.cordova.getActivity().startActivityForResult(intent, WriteFileHelper.CREATE_FILE_CODE);
}
// This method handles the Intent result.
// It obtains the created file URI, open an OutputStream
// and write the text passed as parameter in the writeFile method
public void onCreateFileActivityResult(int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
try {
Uri uri = data.getData();
ContentResolver contentResolver = plugin.cordova.getActivity().getContentResolver();
OutputStream outputStream = contentResolver.openOutputStream(uri);
outputStream.write(intentText.getBytes(StandardCharsets.UTF_8));
outputStream.close();
intentCallbackContext.success();
} catch (FileNotFoundException e) {
e.printStackTrace();
intentCallbackContext.error("FILE_NOT_FOUND");
} catch (IOException e) {
e.printStackTrace();
intentCallbackContext.error("CLOSE_IO_EXCEPTION");
}
} else {
intentCallbackContext.error("INTENT_ERROR");
}
intentCallbackContext = null;
intentText = null;
}
}
Threading
For heavy tasks or blocking calls, you should create a new thread with getThreadPool()
.
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
// ...
if (action.equals("heavyTask")) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
heavyTask(callbackContext, args);
}
});
return true;
}
// ...
}