com.phonegap.plugins.blinkid.BlinkIdScanner.java Source code

Java tutorial

Introduction

Here is the source code for com.phonegap.plugins.blinkid.BlinkIdScanner.java

Source

/**
 * PhoneGap is available under *either* the terms of the modified BSD license *or* the
 * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
 *
 * Copyright (c) Matt Kane 2010
 * Copyright (c) 2011, IBM Corporation
 * Copyright (c) 2013, Maciej Nux Jaros
 */
package com.phonegap.plugins.blinkid;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Parcel;
import android.util.Base64;
import android.util.Log;

import com.microblink.activity.ScanCard;
import com.microblink.image.Image;
import com.microblink.image.ImageListener;
import com.microblink.image.ImageType;
import com.microblink.metadata.MetadataSettings;
import com.microblink.recognizers.BaseRecognitionResult;
import com.microblink.recognizers.IResultHolder;
import com.microblink.recognizers.RecognitionResults;
import com.microblink.recognizers.blinkbarcode.BarcodeType;
import com.microblink.recognizers.blinkbarcode.bardecoder.BarDecoderRecognizerSettings;
import com.microblink.recognizers.blinkbarcode.bardecoder.BarDecoderScanResult;
import com.microblink.recognizers.blinkbarcode.pdf417.Pdf417RecognizerSettings;
import com.microblink.recognizers.blinkbarcode.pdf417.Pdf417ScanResult;
import com.microblink.recognizers.blinkbarcode.usdl.USDLRecognizerSettings;
import com.microblink.recognizers.blinkbarcode.usdl.USDLScanResult;
import com.microblink.recognizers.blinkbarcode.zxing.ZXingRecognizerSettings;
import com.microblink.recognizers.blinkbarcode.zxing.ZXingScanResult;
import com.microblink.recognizers.blinkid.malaysia.MyKadRecognitionResult;
import com.microblink.recognizers.blinkid.malaysia.MyKadRecognizerSettings;
import com.microblink.recognizers.blinkid.mrtd.MRTDRecognitionResult;
import com.microblink.recognizers.blinkid.mrtd.MRTDRecognizerSettings;
import com.microblink.recognizers.blinkid.eudl.EUDLCountry;
import com.microblink.recognizers.blinkid.eudl.EUDLRecognitionResult;
import com.microblink.recognizers.blinkid.eudl.EUDLRecognizerSettings;
import com.microblink.recognizers.settings.RecognitionSettings;
import com.microblink.recognizers.settings.RecognizerSettings;
import com.microblink.results.barcode.BarcodeDetailedData;
import com.microblink.results.date.DateResult;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class BlinkIdScanner extends CordovaPlugin {

    private static final int REQUEST_CODE = 1337;

    // keys for recognizer types
    private static final String PDF417_TYPE = "PDF417";
    private static final String USDL_TYPE = "USDL";
    private static final String BARDECODER_TYPE = "Bar Decoder";
    private static final String ZXING_TYPE = "Zxing";
    private static final String MRTD_TYPE = "MRTD";
    private static final String UKDL_TYPE = "UKDL";
    private static final String MYKAD_TYPE = "MyKad";

    // keys for result types
    private static final String PDF417_RESULT_TYPE = "Barcode result";
    private static final String USDL_RESULT_TYPE = "USDL result";
    private static final String BARDECODER_RESULT_TYPE = "Barcode result";
    private static final String ZXING_RESULT_TYPE = "Barcode result";
    private static final String MRTD_RESULT_TYPE = "MRTD result";
    private static final String UKDL_RESULT_TYPE = "UKDL result";
    private static final String MYKAD_RESULT_TYPE = "MyKad result";

    private static final String SCAN = "scan";
    private static final String CANCELLED = "cancelled";

    private static final String RESULT_LIST = "resultList";
    private static final String RESULT_IMAGE = "resultImage";
    private static final String RESULT_TYPE = "resultType";
    private static final String TYPE = "type";
    private static final String DATA = "data";
    private static final String FIELDS = "fields";
    private static final String RAW_DATA = "raw";

    private static final int COMPRESSED_IMAGE_QUALITY = 90;

    private static final String IMAGE_SUCCESSFUL_SCAN_STR = "IMAGE_SUCCESSFUL_SCAN";
    private static final String IMAGE_CROPPED_STR = "IMAGE_CROPPED";

    private static final int IMAGE_NONE = 0;
    private static final int IMAGE_SUCCESSFUL_SCAN = 1;
    private static final int IMAGE_CROPPED = 2;

    private static final String LOG_TAG = "BlinkIdScanner";

    private int mImageType = IMAGE_NONE;
    private CallbackContext callbackContext;

    /**
     * Constructor.
     */
    public BlinkIdScanner() {
    }

    /**
     * Executes the request.
     * 
     * This method is called from the WebView thread. To do a non-trivial amount
     * of work, use: cordova.getThreadPool().execute(runnable);
     * 
     * To run on the UI thread, use:
     * cordova.getActivity().runOnUiThread(runnable);
     * 
     * @param action
     *            The action to execute.
     * @param args
     *            The exec() arguments.
     * @param callbackContext
     *            The callback context used when calling back into JavaScript.
     * @return Whether the action was valid.
     * 
     * @sa 
     *     https://github.com/apache/cordova-android/blob/master/framework/src/org
     *     /apache/cordova/CordovaPlugin.java
     */
    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
        this.callbackContext = callbackContext;

        if (action.equals(SCAN)) {
            Set<String> types = new HashSet<String>();

            JSONArray typesArg = args.optJSONArray(0);
            for (int i = 0; i < typesArg.length(); ++i) {
                types.add(typesArg.optString(i));
            }

            String imageTypeStr = args.optString(1);
            if (imageTypeStr.equals(IMAGE_CROPPED_STR)) {
                mImageType = IMAGE_CROPPED;
            } else if (imageTypeStr.equals(IMAGE_SUCCESSFUL_SCAN_STR)) {
                mImageType = IMAGE_SUCCESSFUL_SCAN;
            }

            // ios license key is at index 2 in args

            String licenseKey = null;
            if (!args.isNull(3)) {
                licenseKey = args.optString(3);
            }
            scan(types, licenseKey);
        } else {
            return false;
        }
        return true;
    }

    /**
     * Starts an intent from provided class to scan and return result.
     */
    public void scan(Set<String> types, String license) {

        Context context = this.cordova.getActivity().getApplicationContext();
        FakeR fakeR = new FakeR(this.cordova.getActivity());

        Intent intent = new Intent(context, ScanCard.class);

        // set the license key - obtain your key at
        // http://help.microblink.com.
        if (license != null) {
            intent.putExtra(ScanCard.EXTRAS_LICENSE_KEY, license);
        }

        List<RecognizerSettings> recSett = new ArrayList<RecognizerSettings>();
        for (String type : types) {
            try {
                recSett.add(buildRecognizerSettings(type));
            } catch (IllegalArgumentException ex) {
                this.callbackContext.error(ex.getMessage());
                return;
            }
        }

        // finally, when you have defined settings for each recognizer you want to use,
        // you should put them into array held by global settings object

        RecognitionSettings recognitionSettings = new RecognitionSettings();
        RecognizerSettings[] settingsArray = new RecognizerSettings[recSett.size()];
        settingsArray = recSett.toArray(settingsArray);
        recognitionSettings.setRecognizerSettingsArray(settingsArray);

        // additionally, there are generic settings that are used by all recognizers or the
        // whole recognition process

        // set this to true to enable returning of multiple scan results from single camera frame
        // default is false, which means that as soon as first barcode is found (no matter which type)
        // its contents will be returned.
        recognitionSettings.setAllowMultipleScanResultsOnSingleImage(true);

        // finally send that settings object over intent to scan activity
        // use ScanCard.EXTRAS_RECOGNITION_SETTINGS to set recognizer settings
        intent.putExtra(ScanCard.EXTRAS_RECOGNITION_SETTINGS, recognitionSettings);

        // set image metadata settings to define which images will be obtained as metadata during scan process
        MetadataSettings.ImageMetadataSettings ims = new MetadataSettings.ImageMetadataSettings();
        if (mImageType == IMAGE_CROPPED) {
            // enable obtaining of dewarped(cropped) images
            ims.setDewarpedImageEnabled(true);
        } else if (mImageType == IMAGE_SUCCESSFUL_SCAN) {
            // enable obtaining of successful frames
            ims.setSuccessfulScanFrameEnabled(true);
        }
        // pass prepared image metadata settings to scan activity
        intent.putExtra(ScanCard.EXTRAS_IMAGE_METADATA_SETTINGS, ims);

        // pass image listener to scan activity
        intent.putExtra(ScanCard.EXTRAS_IMAGE_LISTENER, new ScanImageListener(mImageType));

        // If you want sound to be played after the scanning process ends, 
        // put here the resource ID of your sound file. (optional)
        intent.putExtra(ScanCard.EXTRAS_BEEP_RESOURCE, fakeR.getId("raw", "beep"));

        this.cordova.startActivityForResult((CordovaPlugin) this, intent, REQUEST_CODE);
    }

    private RecognizerSettings buildRecognizerSettings(String type) {
        if (type.equals(PDF417_TYPE)) {
            return buildPDF417Settings();
        } else if (type.equals(USDL_TYPE)) {
            return buildUsdlSettings();
        } else if (type.equals(BARDECODER_TYPE)) {
            return buildBardecoderSettings();
        } else if (type.equals(ZXING_TYPE)) {
            return buildZXingSettings();
        } else if (type.equals(MRTD_TYPE)) {
            return buildMrtdSettings();
        } else if (type.equals(UKDL_TYPE)) {
            return buildUkdlSettings();
        } else if (type.equals(MYKAD_TYPE)) {
            return buildMyKadSettings();
        }
        throw new IllegalArgumentException("Recognizer type not supported: " + type);
    }

    private MRTDRecognizerSettings buildMrtdSettings() {
        // prepare settings for Machine Readable Travel Document (MRTD) recognizer
        MRTDRecognizerSettings mrtd = new MRTDRecognizerSettings();
        // Set this to true to allow obtaining results that have not been parsed by SDK.
        // By default this is off. The reason for this is that we want to ensure best possible
        // data quality when returning results.
        mrtd.setAllowUnparsedResults(false);
        if (mImageType == IMAGE_CROPPED) {
            mrtd.setShowFullDocument(true);
        }
        return mrtd;
    }

    private EUDLRecognizerSettings buildUkdlSettings() {
        // To specify we want to perform EUDL (EU Driver's License) recognition,
        // prepare settings for EUDL recognizer. Pass country as parameter to EUDLRecognizerSettings
        // constructor. Here we choose UK.
        EUDLRecognizerSettings ukdl = new EUDLRecognizerSettings(EUDLCountry.EUDL_COUNTRY_UK);
        // Defines if issue date should be extracted. Default is true
        ukdl.setExtractIssueDate(true);
        // Defines if expiry date should be extracted. Default is true.
        ukdl.setExtractExpiryDate(true);
        // Defines if address should be extracted. Default is true.
        ukdl.setExtractAddress(true);
        if (mImageType == IMAGE_CROPPED) {
            ukdl.setShowFullDocument(true);
        }
        return ukdl;
    }

    private MyKadRecognizerSettings buildMyKadSettings() {
        // prepare settings for Malaysian MyKad ID document recognizer
        MyKadRecognizerSettings myKad = new MyKadRecognizerSettings();
        if (mImageType == IMAGE_CROPPED) {
            myKad.setShowFullDocument(true);
        }
        return myKad;
    }

    private USDLRecognizerSettings buildUsdlSettings() {
        // prepare settings for US Driver's Licence recognizer
        USDLRecognizerSettings usdl = new USDLRecognizerSettings();
        // By setting this to true, you will enable scanning of non-standard elements,
        // but there is no guarantee that all data will be read. This option is used when multiple
        // rows are missing (e.g. not whole barcode is printed). Default is false.
        usdl.setUncertainScanning(false);
        // By setting this to true, you will allow scanning barcodes which don't have quiet zone
        // surrounding it (e.g. text concatenated with barcode). This option can significantly
        // increase recognition time. Default is true.
        usdl.setNullQuietZoneAllowed(true);
        // Some driver's licenses contain 1D Code39 and Code128 barcodes alongside PDF417 barcode.
        // These barcodes usually contain only reduntant information and are therefore not read by
        // default. However, if you feel that some information is missing, you can enable scanning
        // of those barcodes by setting this to true.
        usdl.setScan1DBarcodes(true);
        return usdl;
    }

    private Pdf417RecognizerSettings buildPDF417Settings() {
        // prepare settings for PDF417 barcode recognizer
        Pdf417RecognizerSettings pdf417 = new Pdf417RecognizerSettings();
        // By setting this to true, you will enable scanning of non-standard elements, but there
        // is no guarantee that all data will be read. This option is used when multiple rows are
        // missing (e.g. not whole barcode is printed). Default is false.
        pdf417.setUncertainScanning(false);
        // By setting this to true, you will allow scanning barcodes which don't have quiet zone
        // surrounding it (e.g. text concatenated with barcode). This option can significantly
        // increase recognition time. Default is false.
        pdf417.setNullQuietZoneAllowed(false);
        // By setting this to true, you will enable scanning of barcodes with inverse intensity
        // values (i.e. white barcodes on dark background). This option can significantly increase
        // recognition time. Default is false.
        pdf417.setInverseScanning(false);
        return pdf417;
    }

    private BarDecoderRecognizerSettings buildBardecoderSettings() {
        // prepare settings for 1D barcode recognizer
        BarDecoderRecognizerSettings bar1d = new BarDecoderRecognizerSettings();
        // Method activates or deactivates the scanning of Code128 1D barcodes.
        // Default (initial) value is false.
        bar1d.setScanCode128(true);
        // Method activates or deactivates the scanning of Code39 1D barcodes.
        // Default (initial) value is false.
        bar1d.setScanCode39(true);
        // By setting this to true, you will enable scanning of barcodes with inverse intensity
        // values (i.e. white barcodes on dark background). This option can significantly increase
        // recognition time. Default is false.
        bar1d.setInverseScanning(false);
        // By setting this to true, you will enabled scanning of lower resolution barcodes at cost
        // of additional processing time. This option can significantly increase recognition time.
        // Default is false.
        bar1d.setTryHarder(false);
        return bar1d;
    }

    private ZXingRecognizerSettings buildZXingSettings() {
        // prepare settings for ZXing barcode recognizer
        ZXingRecognizerSettings zxing = new ZXingRecognizerSettings();
        // disable or enable scanning of various barcode types, by default all barcode types are
        // disabled
        zxing.setScanQRCode(true);
        zxing.setScanAztecCode(false);
        zxing.setScanCode128(true);
        zxing.setScanCode39(true);
        zxing.setScanDataMatrixCode(false);
        zxing.setScanEAN13Code(true);
        zxing.setScanEAN8Code(true);
        zxing.setScanITFCode(false);
        zxing.setScanUPCACode(true);
        zxing.setScanUPCECode(true);

        // By setting this to true, you will enable scanning of barcodes with inverse intensity
        // values (i.e. white barcodes on dark background). This option can significantly increase
        // recognition time. Default is false.
        zxing.setInverseScanning(false);
        // Use this method to enable slower, but more thorough scan procedure when scanning barcodes.
        // By default, this option is turned on.
        zxing.setSlowThoroughScan(true);
        return zxing;
    }

    /**
     * Called when the scanner intent completes.
     * 
     * @param requestCode
     *            The request code originally supplied to
     *            startActivityForResult(), allowing you to identify who this
     *            result came from.
     * @param resultCode
     *            The integer result code returned by the child activity through
     *            its setResult().
     * @param data
     *            An Intent, which can return result data to the caller (various
     *            data can be attached to Intent "extras").
     */
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {

        if (requestCode == REQUEST_CODE) {

            if (resultCode == ScanCard.RESULT_OK) {

                // First, obtain recognition result
                RecognitionResults results = data.getParcelableExtra(ScanCard.EXTRAS_RECOGNITION_RESULTS);
                // Get scan results array. If scan was successful, array will contain at least one element.
                // Multiple element may be in array if multiple scan results from single image were allowed in settings.
                BaseRecognitionResult[] resultArray = results.getRecognitionResults();

                // Each recognition result corresponds to active recognizer. There are 7 types of
                // recognizers available (PDF417, USDL, Bardecoder, ZXing, MRTD, UKDL and MyKad),
                // so there are 7 types of results available.

                JSONArray resultsList = new JSONArray();

                for (BaseRecognitionResult res : resultArray) {
                    try {
                        if (res instanceof Pdf417ScanResult) { // check if scan result is result of Pdf417 recognizer
                            resultsList.put(buildPdf417Result((Pdf417ScanResult) res));
                        } else if (res instanceof BarDecoderScanResult) { // check if scan result is result of BarDecoder recognizer
                            resultsList.put(buildBarDecoderResult((BarDecoderScanResult) res));
                        } else if (res instanceof ZXingScanResult) { // check if scan result is result of ZXing recognizer
                            resultsList.put(buildZxingResult((ZXingScanResult) res));
                        } else if (res instanceof MRTDRecognitionResult) { // check if scan result is result of MRTD recognizer
                            resultsList.put(buildMRTDResult((MRTDRecognitionResult) res));
                        } else if (res instanceof USDLScanResult) { // check if scan result is result of US Driver's Licence recognizer
                            resultsList.put(buildUSDLResult((USDLScanResult) res));
                        } else if (res instanceof EUDLRecognitionResult) { // check if scan result is result of EUDL recognizer
                            resultsList.put(buildUKDLResult((EUDLRecognitionResult) res));
                        } else if (res instanceof MyKadRecognitionResult) { // check if scan result is result of MyKad recognizer
                            resultsList.put(buildMyKadResult((MyKadRecognitionResult) res));
                        }
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "Error parsing " + res.getClass().getName());
                    }
                }

                try {
                    JSONObject root = new JSONObject();
                    root.put(RESULT_LIST, resultsList);
                    if (mImageType != IMAGE_NONE) {
                        Image resultImage = ImageHolder.getInstance().getLastImage();
                        if (resultImage != null) {
                            Bitmap resultImgBmp = resultImage.convertToBitmap();
                            if (resultImgBmp != null) {
                                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                                boolean success = resultImgBmp.compress(Bitmap.CompressFormat.JPEG,
                                        COMPRESSED_IMAGE_QUALITY, byteArrayOutputStream);
                                if (success) {
                                    String resultImgBase64 = Base64
                                            .encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT);
                                    root.put(RESULT_IMAGE, resultImgBase64);
                                }
                                try {
                                    byteArrayOutputStream.close();
                                } catch (IOException ignorable) {
                                }
                            }
                            ImageHolder.getInstance().clear();
                        }
                    }
                    root.put(CANCELLED, false);
                    this.callbackContext.success(root);
                } catch (JSONException e) {
                    Log.e(LOG_TAG, "This should never happen");
                }

            } else if (resultCode == ScanCard.RESULT_CANCELED) {
                JSONObject obj = new JSONObject();
                try {
                    obj.put(CANCELLED, true);

                } catch (JSONException e) {
                    Log.e(LOG_TAG, "This should never happen");
                }
                this.callbackContext.success(obj);

            } else {
                this.callbackContext.error("Unexpected error");
            }
        }
    }

    private JSONObject buildPdf417Result(Pdf417ScanResult res) throws JSONException {
        // getStringData getter will return the string version of barcode contents
        String barcodeData = res.getStringData();
        // getRawData getter will return the raw data information object of barcode contents
        BarcodeDetailedData rawData = res.getRawData();
        // BarcodeDetailedData contains information about barcode's binary layout, if you
        // are only interested in raw bytes, you can obtain them with getAllData getter
        byte[] rawDataBuffer = rawData.getAllData();

        JSONObject result = new JSONObject();
        result.put(RESULT_TYPE, PDF417_RESULT_TYPE);
        result.put(TYPE, "PDF417");
        result.put(DATA, barcodeData);
        result.put(RAW_DATA, byteArrayToHex(rawDataBuffer));
        return result;
    }

    private JSONObject buildBarDecoderResult(BarDecoderScanResult res) throws JSONException {
        // with getBarcodeType you can obtain barcode type enum that tells you the type of decoded barcode
        BarcodeType type = res.getBarcodeType();
        // as with PDF417, getStringData will return the string contents of barcode
        String barcodeData = res.getStringData();

        JSONObject result = new JSONObject();
        result.put(RESULT_TYPE, BARDECODER_RESULT_TYPE);
        result.put(TYPE, type.name());
        result.put(DATA, barcodeData);
        return result;
    }

    private JSONObject buildZxingResult(ZXingScanResult res) throws JSONException {
        // with getBarcodeType you can obtain barcode type enum that tells you the type of decoded barcode
        BarcodeType type = res.getBarcodeType();

        // as with PDF417, getStringData will return the string contents of barcode
        String barcodeData = res.getStringData();

        JSONObject result = new JSONObject();
        result.put(RESULT_TYPE, ZXING_RESULT_TYPE);
        result.put(TYPE, type.name());
        result.put(DATA, barcodeData);
        return result;
    }

    private JSONObject buildUSDLResult(USDLScanResult res) throws JSONException {
        return buildKeyValueResult(res, USDL_RESULT_TYPE);
    }

    private JSONObject buildMyKadResult(MyKadRecognitionResult res) throws JSONException {
        return buildKeyValueResult(res, MYKAD_RESULT_TYPE);
    }

    private JSONObject buildUKDLResult(EUDLRecognitionResult res) throws JSONException {
        return buildKeyValueResult(res, UKDL_RESULT_TYPE);
    }

    private JSONObject buildMRTDResult(MRTDRecognitionResult res) throws JSONException {
        return buildKeyValueResult(res, MRTD_RESULT_TYPE);
    }

    private JSONObject buildKeyValueResult(BaseRecognitionResult res, String resultType) throws JSONException {
        JSONObject fields = new JSONObject();
        IResultHolder resultHolder = res.getResultHolder();
        for (String key : resultHolder.keySet()) {
            Object value = resultHolder.getObject(key);
            if (value instanceof String) {
                fields.put(key, (String) value);
            } else if (value instanceof DateResult) {
                fields.put(key, ((DateResult) value).getOriginalDateString());
            } else {
                Log.d(LOG_TAG, "Ignoring non string key '" + key + "'");
            }
        }
        JSONObject result = new JSONObject();
        result.put(RESULT_TYPE, resultType);
        result.put(FIELDS, fields);
        return result;
    }

    private String byteArrayToHex(byte[] data) {
        StringBuilder sb = new StringBuilder();
        for (byte b : data) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static class ScanImageListener implements ImageListener {

        private int mImageType;

        public ScanImageListener(int imageType) {
            mImageType = imageType;
        }

        public ScanImageListener() {
            mImageType = IMAGE_NONE;
        }

        /**
         * Called when library has image available.
         */
        @Override
        public void onImageAvailable(Image image) {
            switch (mImageType) {
            case IMAGE_CROPPED:
                if (image.getImageType() == ImageType.DEWARPED) {
                    ImageHolder.getInstance().setImage(image.clone());
                }
                break;
            case IMAGE_SUCCESSFUL_SCAN:
                if (image.getImageType() == ImageType.SUCCESSFUL_SCAN) {
                    ImageHolder.getInstance().setImage(image.clone());
                }
                break;
            }
        }

        /**
         * ImageListener interface extends Parcelable interface, so we also need to implement
         * that interface. The implementation of Parcelable interface is below this line.
         */

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mImageType);
        }

        public static final Creator<ScanImageListener> CREATOR = new Creator<ScanImageListener>() {
            @Override
            public ScanImageListener createFromParcel(Parcel source) {
                return new ScanImageListener(source.readInt());
            }

            @Override
            public ScanImageListener[] newArray(int size) {
                return new ScanImageListener[size];
            }
        };
    }

    public static class ImageHolder {

        private static ImageHolder sInstance = new ImageHolder();
        private Image mLastImage = null;

        private ImageHolder() {

        }

        public static ImageHolder getInstance() {
            return sInstance;
        }

        public void setImage(Image image) {
            if (mLastImage != null) {
                mLastImage.dispose();
            }
            mLastImage = image;
        }

        public Image getLastImage() {
            return mLastImage;
        }

        public void clear() {
            if (mLastImage != null) {
                mLastImage.dispose();
            }
            mLastImage = null;
        }
    }

}