I'm designing a custom plugin for Cordova as an educational demonstration. The plugin integrates with the Android PdfRenderer class and exposes the API to the Cordova ecosystem.
My Custom Plugin is compiling without error, and I've added the plugin to my test project. When I build and run the test project, however, the application crashes on startup and I'm met with the following message:
java.lang.ClassNotFoundException: com.dev.plugin.PdfRendererPlugin
I've checked my platforms/android/
folder, and the plugin class is found at src/com/dev/plugin/PdfRenderPlugin.java
as expected.
What else should I be looking for in this situation? It seems like my application should be working just fine now, if the plugin compiles and is added to the project without errors.
Here's some code to work with:
plugin.xml
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="cordova-android-pdf-renderer-plugin" version="0.2.3">
<name>PdfRendererPlugin</name>
<description>Cordova PDF Renderer Plugin</description>
<license>MIT</license>
<keywords>cordova,pdf,renderer</keywords>
<platform name="android">
<js-module src="www/js/PdfRendererPlugin.js" name="PdfRendererPlugin">
<runs/>
<clobbers target="PdfRendererPlugin" />
</js-module>
<config-file target="config.xml" parent="/*">
<feature name="com.dev.plugin.PdfRendererPlugin">
<param name="android-package" value="com.dev.plugin.PdfRendererPlugin"/>
<param name="onload" value="true" />
</feature>
</config-file>
<source-file src="src/android/PdfRendererPlugin.java" target-dir="src/com/dev/plugin/" />
</platform>
</plugin>
Plugin Class (PdfRendererService)
package com.dev.plugin.PdfRendererService;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.pdf.PdfRenderer;
import android.graphics.pdf.PdfRenderer.Page;
import android.os.ParcelFileDescriptor;
import android.util.Log;
/**
* This class handles a pdf file called from JavaScript and converts a selected
page (default is first) to a byte array representing a bitmap.
*/
public class PdfRendererPlugin extends CordovaPlugin {
private static final String LOG_TAG = "PdfRendererPlugin";
private ParcelFileDescriptor fileDescriptor = null;
private PdfRenderer renderer = null;
private Page currentPage = null;
private int mWidth = 400, mHeight = 600;
private String mRenderMode = "display";
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView){
Log.d(LOG_TAG, "initialize");
super.initialize(cordova, webView);
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
Log.d(LOG_TAG, "execute");
//No Switch -> src 1.6
if(action.equals("open")){
return executeOpen(args, callbackContext);
}
else if(action.equals("renderPage")){
return executeRenderPage(args, callbackContext);
}
else if(action.equals("pageCount")){
callbackContext.success(this.getPageCount());
return true;
}
else if(action.equals("close")){
this.closeRenderer();
callbackContext.success();
return true;
}
return false;
}
private boolean executeOpen(JSONArray args, CallbackContext callbackContext){
Log.d(LOG_TAG, "executeOpen");
String filePath = "";
try{
if(args.length() < 1){
Log.e(LOG_TAG, "No arguments provided. Exiting process.");
callbackContext.error("No arguments provided. Exiting process.");
return true;
}
else if(args.length() < 2){
Log.e(LOG_TAG, "Insufficient arguments provided. Exiting process.");
callbackContext.error("Insufficient arguments provided. Exiting process.");
return true;
}
if(args.length() > 3){
mWidth = args.getInt(2);
mHeight = args.getInt(3);
}
filePath = args.getString(0);
mRenderMode = args.getString(1);
}
catch(JSONException je){
String msg = je.getMessage();
if(msg == null)
msg = "Unknown JSONException has occurred";
Log.e(LOG_TAG, msg);
}
this.initializeRenderer(filePath, callbackContext);
boolean isPageOpen = this.openPage(0, callbackContext);
if(isPageOpen){
Bitmap bitmap = getBitmap(mWidth, mHeight);
this.sendBitmapAsBytes(0, bitmap, callbackContext);
}
return true;
}
private boolean executeRenderPage(JSONArray args, CallbackContext callbackContext){
Log.d(LOG_TAG, "executeRenderPage");
int pageNo = -1;
try {
if (args.length() < 1) {
Log.e(LOG_TAG, "No arguments provided. Exiting process.");
callbackContext.error("No arguments provided. Exiting process.");
return true;
}
if (args.length() > 1) {
mRenderMode = args.getString(1);
}
if (args.length() > 3) {
mWidth = args.getInt(2);
mHeight = args.getInt(3);
}
pageNo = args.getInt(0);
}
catch(JSONException je){
String msg = je.getMessage();
if(msg == null)
msg = "Unknown JSONException has occurred";
Log.e(LOG_TAG, msg);
}
if(pageNo < 0)
return false;
boolean isPageOpen = this.openPage(pageNo, callbackContext);
if(isPageOpen) {
Bitmap bitmap = getBitmap(mWidth, mHeight);
this.sendBitmapAsBytes(pageNo, bitmap, callbackContext);
}
return true;
}
/*
// Requests the permission to read from external storage if not already available
private void validatePermissions(){
Log.d(LOG_TAG, "validatePermissions");
if(!cordova.hasPermission(READ_EXTERNAL_STORAGE)){
Log.i(LOG_TAG, "Requesting External Storage Read Permission...");
cordova.requestPermission(this, CODE_READ_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE);
}
}
*/
private int getPageCount() {
Log.d(LOG_TAG, "getPageCount");
if(renderer == null)
return 0;
return renderer.getPageCount();
}
private void initializeWriteFileDescriptor(String filePath, CallbackContext callbackContext) throws FileNotFoundException, FileFormatException {
Log.d(LOG_TAG, "initializeWriteFileDescriptor");
fileDescriptor = null;
if(filePath == null || filePath.length() < 1)
throw new FileNotFoundException("The file path provided is not a valid file path.");
String[] pathArr = filePath.split(".");
int numSections = pathArr.length;
String ext = pathArr[numSections - 1];
if(!ext.equals("pdf"))
throw new FileFormatException("Invalid File Extension provided to Pdf Render Service: " + ext);
fileDescriptor = getWriteFileDescriptor(filePath);
}
private void initializeRenderer(String filePath, CallbackContext callbackContext){
Log.d(LOG_TAG, "initializeRenderer");
renderer = null;
try {
initializeWriteFileDescriptor(filePath, callbackContext);
renderer = new PdfRenderer(fileDescriptor);
}
catch(IOException io){
String msg = io.getMessage();
if(msg == null)
msg = "An error has occurred while loading the requested file.";
Log.e(LOG_TAG, msg);
callbackContext.error(msg);
}
}
private void closeRenderer() {
Log.d(LOG_TAG, "closeRenderer");
if(renderer == null) {
Log.w(LOG_TAG, "Attempted to close null renderer. Skipping operation.");
return;
}
renderer.close();
}
private boolean openPage(int index, CallbackContext callbackContext){
Log.d(LOG_TAG, "openPage");
currentPage = null;
int pageCount = getPageCount();
if(pageCount < 1) {
Log.e(LOG_TAG, "Requested document has no pages to display.");
callbackContext.error("Requested document has no pages to display.");
return false;
}
if(index >= pageCount || index < 0) {
Log.e(LOG_TAG, String.format("No page was found at page number %d/%d", index, pageCount));
callbackContext.error(String.format("No page was found at page number %d/%d", index, pageCount));
return false;
}
currentPage = renderer.openPage(index);
return true;
}
private void sendBitmapAsBytes(int index, Bitmap bitmap, CallbackContext callbackContext){
Log.d(LOG_TAG, "sendBitmapAsBytes");
if(renderer == null) {
Log.e(LOG_TAG, "Renderer was not properly initialized.");
callbackContext.error("Renderer was not properly initialized.");
return;
}
if(currentPage == null) {
Log.e(LOG_TAG, "Requested page could not be rendered.");
callbackContext.error("Requested page could not be rendered.");
return;
}
int renderMode = mRenderMode.equals("print") ? Page.RENDER_MODE_FOR_PRINT : Page.RENDER_MODE_FOR_DISPLAY;
currentPage.render(bitmap, null, null, renderMode);
byte[] output = toByteArray(bitmap);
if(output == null || output.length < 1) {
Log.e(LOG_TAG, "Bitmap Error has occurred: Invalid Output Format Detected");
callbackContext.error("Bitmap Error has occurred: Invalid Output Format Detected");
}
else {
Log.i(LOG_TAG, "Bitmap Conversion Successful");
callbackContext.success(output);
}
}
private static byte[] toByteArray(Bitmap bitmap){
Log.d(LOG_TAG, "toByteArray");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
return stream.toByteArray();
}
private static Bitmap getBitmap(int width, int height){
Log.d(LOG_TAG, "getBitmap");
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
private static ParcelFileDescriptor getWriteFileDescriptor(String filePath) throws FileNotFoundException {
Log.d(LOG_TAG, "getWriteFileDescriptor");
File file = new File(filePath);
final int fileMode = ParcelFileDescriptor.MODE_TRUNCATE |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_WRITE_ONLY;
return ParcelFileDescriptor.open(file, fileMode);
}
class FileFormatException extends IOException {
FileFormatException(String msg){
super(msg);
}
}
}
Plugin JS Interface
var PLUGIN_NAME = "PdfRendererPlugin";
var SERVICE_OPEN = "open";
var SERVICE_CLOSE = "close";
var SERVICE_PAGE_COUNT = "pageCount";
var SERVICE_RENDER_PAGE = "renderPage";
var RENDER_MODE_DISPLAY = "display";
var RENDER_MODE_PRINT = "print";
var PdfRendererPlugin = {
display: function(filePath, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_OPEN, [filePath, RENDER_MODE_DISPLAY]);
},
displayWithDimensions: function(filePath, width, height, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_OPEN, [filePath, RENDER_MODE_DISPLAY, width, height]);
},
print: function(filePath, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_OPEN, [filePath, RENDER_MODE_PRINT]);
},
printWithDimensions: function(filePath, width, height, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_OPEN, [filePath, RENDER_MODE_PRINT, width, height]);
},
renderPage: function(pageNo, callback){
cordova.exec(callback, function(err){
//console.log(err);
}, PLUGIN_NAME, SERVICE_RENDER_PAGE, [pageNo]);
},
renderPageForDisplay: function(pageNo, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_RENDER_PAGE, [pageNo, RENDER_MODE_DISPLAY]);
},
renderPageForDisplayWithDimensions: function(pageNo, width, height, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_RENDER_PAGE, [pageNo, RENDER_MODE_DISPLAY, width, height]);
},
renderPageForPrint: function(pageNo, callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_RENDER_PAGE, [pageNo, RENDER_MODE_PRINT]);
},
renderPageForPrintWithDimensions: function(pageNo, width, height, callback){
cordova.exec(callback, function(err){
//console.log(err);
}, PLUGIN_NAME, SERVICE_RENDER_PAGE, [pageNo, RENDER_MODE_PRINT, width, height]);
},
close: function(callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_CLOSE, []);
},
getPageCount: function(callback){
cordova.exec(callback, function(err){
// console.log(err);
}, PLUGIN_NAME, SERVICE_PAGE_COUNT, []);
}
};
Test Application index.html
<!DOCTYPE html>
<html>
<head>
<title>Cordova PDF Generator Plugin Test</title>
<meta name="viewport" content="user-scalable=no, initial-scale=1,
maximum-scale=1, minimum-scale=1, width=device-width,
height=device-height" />
</head>
<body>
<div class="app">
<h1>Cordova PDF Generation Plugin Test</h1>
<div>
<button id="display-button" onclick="display()">Display (View)</button>
<button id="print-button" onclick="print()">Display (Print)</button>
</div>
</div>
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/index.js"></script>
</body>
</html>
Test Application index.js
var testFilePath = 'assets/test-file.pdf';
var app = {
// Application Constructor
initialize: function() {
document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
},
// deviceready Event Handler
//
// Bind any cordova events here. Common events are:
// 'pause', 'resume', etc.
onDeviceReady: function() {
display();
}
};
var display = function(){
PdfRendererPlugin.display(testFilePath, function(data){
console.log('Bitmap Bytes');
console.log(data);
});
};
var print = function(){
PdfRendererPlugin.print(testFilePath, function(data){
console.log('Bitmap Bytes');
console.log(data);
});
};
app.initialize();
I feel silly. The problem was with the package declaration. I added the classname to the package declaration without thinking about it (it must have been because I copy and pasted the path from the plugin.xml, which includes the classname).