Tuesday, July 12, 2011

PAW - Dynamic Handlers and Filters

In addition to dynamically loading DEXed classes the latest PAW version provides the possibility to use external so called dynamic Handlers and Filters.

Handlers and Filters are components of the underlying Brazil framework. Handlers are responsible for handling HTTP request. Filters in addition can process the output provided by an handler. In principle PAW consists of a number of handlers that are called in a row until a Handler feels responsible to handle the request.
There is a special Handler, the so called FilterHandler. This Handler can wrap a Handler and directs the output of that Handler to a list of Filters which process the response before it is provided to a Web Browser.

I'll demonstrate this on a small but useful filter that stamps all images inside a defined directory with the PAW logo. Such a technique is often used on sites that provide screenshots and want to automatically decorate them with a logo.

Image without and with Filter applied.

Note:
In this example only the AndroidDynamicFilter is used. There is also  a AndroidDynamicHandler available which can be used if a custom Handler is needed. For this example the AndroidDynamicHandler class is not needed.

Note on Honeycomb: As mentioned in an earlier post, this might not work on Android 3.x because of a bug in Honeycomb.

Prerequisites

What you will need to build the Filter:
Creating a Java Project

When Eclipse is running, we can create a standard Java project.
The Java build path should include the brazil.jar (I've renamed it from brazil-2.3.jar to brazil.jar) and the android.jar file. The android.jar files can be found in the installation directory of the Android SDK (platforms/android-*).

Java Build Path

The Filter Class

After creating the project create a source folder (if not available) and create a new package called dextest.filter.
Inside that package we will create the StampFilter class.

The structure should look something like this:

Project Structure
Before creating the StampFilter class just a view words how a filter works.
A Filter has three important methods:

  • init() - This initializes the Filter and is called on Filter startup (when PAW starts). This returns true on success and false on failure.
  • shouldFilter() - Is called on each Handler output and decides weather the response is to be filtered or not. Returns true if the filter() method should be called, false otherwise.
  • filter() - This is the actual filter method. It processes the content form the Handler and returns it.
Below is the code of the Filter class:

package dextest.filter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

import sunlabs.brazil.filter.Filter;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.util.http.MimeHeaders;

public class StampFilter implements Filter {
 private String prefix;

 private static final String STAMP_FILE = "stampFile";

 private static final String IMAGE_DIRECTORY_URL = "imageDirctoryUrl";

 private String stampFile;
 private String imageDirectoryUrl;

 public boolean init(Server server, String prefix) {
  this.prefix = prefix;

  Properties props = server.props;

  stampFile = props.getProperty(prefix + STAMP_FILE, null);
  imageDirectoryUrl = props.getProperty(prefix + IMAGE_DIRECTORY_URL, null);

  if (stampFile != null && imageDirectoryUrl != null) {
   if (!new File(stampFile).exists()) {
    server.log(Server.LOG_ERROR, prefix, STAMP_FILE
      + " does not exist!");
    return false;
   }

   if (!new File(imageDirectoryUrl).isDirectory()) {
    server.log(Server.LOG_ERROR, prefix, IMAGE_DIRECTORY_URL
      + " is not a directory!");
    return false;
   }

   return true;
  } else {
   server.log(Server.LOG_ERROR, prefix, "Missing parameter(s)");
   return false;
  }
 }

 public boolean shouldFilter(Request request, MimeHeaders headers) {
  String type = headers.get("Content-Type");
  return type != null && type.startsWith("image/png") && new File(request.url).getParent().equals(imageDirectoryUrl);
 }

 public byte[] filter(Request request, MimeHeaders headers, byte[] content) {
  try {
   return stampImage(content);
  }
  catch(Exception e) {
   request.log(Server.LOG_ERROR, prefix, "Exception: " + e.getMessage());
   return content;
  }
 }

 /**
  * This is the request object before the content was fetched
  */
 public boolean respond(Request request) throws IOException {
  return false;
 }

 byte[] getImageBytes(String image) throws IOException {
  File f = new File(image);
  byte[] bBitmap = new byte[(int) f.length()];
  FileInputStream fis = new FileInputStream(f);
  fis.read(bBitmap);
  fis.close();
  return bBitmap;
 }

 private byte[] stampImage(byte[] imageBytes) throws IOException {
  byte[] stampBytes = getImageBytes(stampFile);
  Bitmap stampBitmap = BitmapFactory.decodeByteArray(stampBytes, 0, stampBytes.length);

  Bitmap imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
  imageBitmap = imageBitmap.copy(imageBitmap.getConfig(), true);

  Canvas canvas = new Canvas(imageBitmap);
  canvas.drawBitmap(stampBitmap, imageBitmap.getWidth() - stampBitmap.getWidth(), imageBitmap.getHeight() - stampBitmap.getHeight(), null);

  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  imageBitmap.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos);

  return bos.toByteArray();

 }
} 

In the init() method you can see that the Filter excepts two parameters. One is called stampFile, the other imageDirectoryUrl. The stampFile parameter specifies the file (absolute path) of the image that is placed on top of the original image. The imageDirectoryUrl parameter specifies the URL for which the filter is applied.
The init() method does check if the parameters are present and if they are valid. The parameters are read from the configuration by using props.getProperty(). The prefix is basically the name of the Filter.

The shouldFilter() method checks if the file requested is a PNG and if the file is served from the URL that should be filtered.

Filtering is actually done in the filter() method. This method will not be discussed in detail, because it uses standard Android functionality to do the stamping.

After compiling the Filter, create a JAR file called filterTest.jar.

Creating the DEXed Filter JAR

Now we can use the dx command from the Android SDK to create a DEXed JAR file:
./dx --dex --output=/tmp/filterTest_dex.jar --positions=lines filterTest.jar

Now copy the resulting filterTest_dex.jar JAR file over to your Android device and store ist directly on the SD Card (/sdcard).

Here is the ready to use JAR file:  filterTest_dex.jar

The Stamp Image

In principle you can use any PNG image for stamping the images.
Here is an example:
Store it directly under /sdcard/paw_powered.png.

Modifying the PAW Configuration

The next step is to modify the PAW configuration. The two important files are handler.xml and filter.xml. Both files are located inside the paw/conf directory.
The handler.xml defines the Handles and the filter.xml defines the Filters used by PAW.
We will start with the filter configuration. For that open the filter.xml file and add the following Filter definition inside the <filters> </filters> tags:

<filter status="active" type="custom">
    <name>Stamp Filter</name>
    <description>Stamp Filter</description>
    <removable>true</removable>
    <id>stamp</id>
    <files />
    <params>
      <param name="stamp.class" value="org.paw.filter.AndroidDynamicFilter" />
      <param name="stamp.filterClass" value="dextest.filter.StampFilter" />
      <param name="stamp.filterJars" value="/sdcard/filterTest_dex.jar" />
      <param name="stamp.stampFile" value="/sdcard/paw_powered.png" />
      <param name="stamp.imageDirctoryUrl" value="/stamp" />
    </params>
  </filter>

PAW knows two filter types httpProxy and custom. HttpProxy filters are only important when PAW is used as a proxy server. In our case the StampFilter is a custom Filter.
The parameters define the Filter class to use, the location of the DEX file, the stamp image and the URL (the directory) where the images that should be stamped are located (we will create hat directory below).


Now comes the Handler part. Our new Handler definition will replace the existing File Handler definition and use the defined StampFilter. So edit the file handle.xml and replace the existing File Handler with the following XML code:
<handler status="active">
    <name>File Handler</name>
    <description></description>
    <removable>true</removable>
    <id>filehandlerWrapper</id>
    <files />
    <params>
      <param name="filehandlerWrapper.class" value="sunlabs.brazil.filter.FilterHandler" />
      <param name="filehandlerWrapper.handler" value="filehandler" />
      <param name="filehandlerWrapper.filters" value="stamp" />

      <param name="filehandler.class" value="sunlabs.brazil.server.FileHandler" />
      <param name="filehandler.root" value="/sdcard/paw/html" />
      <param name="filehandler.defaults" value="index.html" />
    </params>
  </handler>

After that restart the PAW app and let's start testing :)

Using the Filter

For testing, let's create the directory for the images that should be stamped as defined in the Filter definition. For that create a directory called /sdcard/paw/html/stamp and place some PNG images inside it. For a test, you can also put some other image files (e.g. JPG) in that directory. These should not be modified.
Now enter the Url http://<ip number>:8080/stamp into the address bar of your browser and select a PNG image.

The resulting image should include the stamp image in the lower right corner.

3 comments:

  1. If i compiled the above in java eclipse it got an error .

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
    at sunlabs.brazil.util.Base64.main(Base64.java:164)

    How to solve this?..

    ReplyDelete
    Replies
    1. Could you please send me the complete stack trace via mail.

      Delete
    2. For redirection i have tried to make active the handler by changing the handlers.xml file and given the url in redirect file but its is not working.

      Given url is :
      ^http://192.168.1.4:8080/test.jpg http://192.168.1.4:8080/temp.txt

      Placed the temp.txt file in html folder...

      Is there any file to change...

      Delete