Introduction
With every release of the Android SDK, new API is introduced for developers. If you want to use the new API, then you may have to drop support for some older SDK versions. If you wish to continue work with an older SDK version, then you have to use Java reflection to access newer APIs.
Background
My web browser & feed reader app can save web pages to image files. Recently, I wanted to add the ability to save the images to PDF - just for the heck of it.
This meant that I needed to use the android.graphics.pdf.PdfDocument
class introduced in KitKat (SDK v19). My project had always been linked to Donut (SDK v4) JAR file and this API was out of reach for my code. So, I added a new Java reflection class to my public domain AndroidWithoutStupid Java library and used it in the app.
You can use the new class MvReflection.java individually or use the whole JAR.
Using the Code
The code for saving a bitmap of an image to PDF was pretty straightforward.
void saveBitmapAsPDF(Bitmap aoBitmap, String asPDF) {
FileOutputStream fos;
try {
fos = new FileOutputStream(asPDF);
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
PdfDocument document = new PdfDocument();
PageInfo pageInfo = new PageInfo.Builder(aoBitmap.getWidth(), aoBitmap.getHeight(), 1).create();
Page page = document.startPage(pageInfo);
page.getCanvas().drawBitmap(aoBitmap, 0, 0, null);
document.finishPage(page);
try {
document.writeTo(fos);
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
document.close();
}
Rewriting even this simple code using Java reflection wouldn't be easy unless the API lookup strategy was moved to another class. (Java reflection was not fun to say the least.) The MvReflection.java
class has three methods:
getNewInstance(String asClassName, Object... aoConstructorArguments)
- to access a specified class and create a new instance from it. invokeMethod(Object aoInvokingInstance, String asMethodName, Object... aoMethodArguments)
- to call a specified method using the new instance created from the first method. getNewNestedInstance(Object oEnclosingInstance, String asClassName, Object... aoConstructorArguments)
- to create an instance of a nested class using the enclosing instance created using the first method.
With the rewritten code, the number of lines increased but it would have become unreadable if the Java reflection part was all inline. If a method fails, it returns null
. On older devices, it does return null
, as expected, and the code exits.
boolean saveBitmapAsPDF(Bitmap aoBitmap, String asPdfPath) {
Canvas oCanvas;
Object oPdfDocumentInstance, oPdfDocumentPageInfoBuilderInstance,
oPdfDocumentPageInfoInstance, oPdfPageInstance;
if (Build.VERSION.SDK_INT >= 19) {
oPdfDocumentInstance =
MvReflection.getNewInstance("android.graphics.pdf.PdfDocument");
if (oPdfDocumentInstance != null) {
oPdfDocumentPageInfoBuilderInstance =
MvReflection.getNewNestedInstance(
oPdfDocumentInstance,
"android.graphics.pdf.PdfDocument$PageInfo$Builder",
aoBitmap.getWidth(),
aoBitmap.getHeight(), 1);
if (oPdfDocumentPageInfoBuilderInstance != null) {
oPdfDocumentPageInfoInstance =
MvReflection.invokeMethod(
oPdfDocumentPageInfoBuilderInstance,
"create");
if (oPdfDocumentPageInfoInstance != null) {
oPdfPageInstance =
MvReflection.invokeMethod(oPdfDocumentInstance,
"startPage",
oPdfDocumentPageInfoInstance);
if (oPdfPageInstance != null) {
oCanvas =
(Canvas) MvReflection.invokeMethod(
oPdfPageInstance,
"getCanvas");
oCanvas.drawBitmap(aoBitmap, 0, 0, null);
MvReflection.invokeMethod(oPdfDocumentInstance,
"finishPage",
oPdfPageInstance);
try {
OutputStream os = new FileOutputStream(asPdfPath);
MvReflection.invokeMethod(oPdfDocumentInstance,
"writeTo",
(OutputStream) os);
os.flush();
os.close();
MvReflection.invokeMethod(oPdfDocumentInstance,
"close");
} catch (FileNotFoundException e) {
MvMessages.logMessage(e.getClass().getCanonicalName());
} catch (IOException e) {
MvMessages.logMessage(e.getClass().getCanonicalName());
}
MvMessages.logMessage("PDF saved");
}
}
}
}
} else {
MvMessages.logMessage(
"android.graphics.pdf API is not available in this SDK version - " +
Build.VERSION.SDK_INT);
}
return(false);
}
Now my app is able to access APIs in Android Lollipop or Marshmallow while continuing to maintain support for Android Donut.
Points of Interest
A serious issue I encountered was with derived class types. Methods such as PdfDocument.writeTo()
required an exact match. A FileOutputStream
instance that my code had would not pass for an OutputStream
type that the method signature wanted. A brute force tactic worked - iterating through all overloads to find the one that worked with a given set of argument types. It looks risky but will work all right if the overloads are limited.
The MvReflection.java
class could be used in other forms of Java too, not just Android Java.