com.gpfcomics.android.cryptnos.ImportExportHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.gpfcomics.android.cryptnos.ImportExportHandler.java

Source

/* ImportExportHandler.java
 * 
 * PROGRAMMER:    Jeffrey T. Darlington
 * DATE:          March 26, 2010
 * PROJECT:       Cryptnos (Android)
 * ANDROID V.:     1.1
 * REQUIRES:      
 * REQUIRED BY:   
 * 
 * This class provides a self-contained interface for importing and exporting
 * Cryptnos parameter data to and from an encrypted file on the device SD
 * card.  It does not directly interface with the user; rather, it is called
 * by the UI activities, which then pass it the necessary parameters to do its
 * work.  Note that it updates UI elements and requires references back to the
 * calling activity and a ProgressDialog it controls.
 * 
 * UPDATES FOR 1.1:  Moved the old Importer internal class to OldFormatImporter,
 * then added the XMLFormat1Importer and XMLHandler classes.  The old format
 * importer will allow us to import files exported by Cryptnos 1.0, while the
 * XML importer will import files created in the new XML-based cross-platform
 * format.  In addition, the Exporter class has been modified to export to the
 * new XML-format.  Note that exporting to the old format is no longer an
 * option; we'll only export to the new format.  The new XML format should be
 * readable by any version of Cryptnos on any platform.
 * 
 * UPDATES FOR 1.2.0:  Attempting to fix some text encoding issues.  XML-format
 * import/export files now force UTF-8; originally, we declared such in the XML
 * header but didn't actually enforce that. The Windows client should be using
 * UTF-8, so this bring us in line.  Note that the one place we *CAN'T* do this
 * is the old format stuff, which requires us to use whatever default the system
 * uses.
 * 
 * UPDATES FOR 1.2.3:  Fix for Issue 3, "Out Of Memory Error in Import/Export
 * Handler".  An out of memory issue was being generated by the XML importer,
 * specifically where we attempt to allocate memory to hold the decrypted XML.
 * This likely occurred because we have to hold both the encrypted and decrypted
 * data in memory at the same time and the import file may have been too large
 * to work with.  Added a check to see if there's enough RAM available to hold
 * both the encrypted and decrypted data, and throw an error if there isn't.
 * 
 * UPDATES FOR 1.3.0  Changes to support new import functionality to let the
 * user pick and choose which sites from a file to import.
 * 
 * This program is Copyright 2011, Jeffrey T. Darlington.
 * E-mail:  android_support@cryptnos.com
 * Web:     http://www.cryptnos.com/
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of
 * the GNU General Public License as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See theGNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
*/
package com.gpfcomics.android.cryptnos;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.security.MessageDigest;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.engines.RijndaelEngine;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.MemoryInfo;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.widget.Toast;

/**
 * This class provides a self-contained interface for importing and exporting
 * Cryptnos parameter data to and from an encrypted file on the device SD
 * card.  It does not directly interface with the user; rather, it is called
 * by the UI activities, which then pass it the necessary parameters to do its
 * work.  Note that it updates UI elements and requires references back to the
 * calling activity and a ProgressDialog it controls.
 * @author Jeffrey T. Darlington
 * @version 1.3.0
 * @since 1.0
 */
public class ImportExportHandler {

    // Private Constants **************************************************

    /** The number of iterations used for salt generation.  For the encryption
     *  used in this class, we'll derive our salt from the user's password;
     *  not ideal, of course, but definitely portable.  This constant will set
     *  the number of times we'll hash the user's password with the selected
     *  hash algorithm to generate our salt. */
    private static final int SALT_ITERATION_COUNT = 10;

    /** The number of iterations used for key generation. */
    private static final int KEY_ITERATION_COUNT = 100;

    /** The size of the AES encryption key in bits */
    private static final int KEY_SIZE = 256;

    /** The size of the AES encryption intialization vector (IV) in bits */
    private static final int IV_SIZE = 128;

    // Private Variables **************************************************

    /** A reference to our top-level application */
    private CryptnosApplication theApp = null;
    /** The calling activity, so we can refer back to it. */
    private Activity caller = null;
    /** The listener waiting to hear about sites we may have imported. */
    private ImportListener importListener = null;
    /** The parameter DB adapter from the caller. */
    private ParamsDbAdapter DBHelper = null;
    /** The caller's ProgressDialog, which we'll help control.*/
    private ProgressDialog progressDialog = null;
    /** The caller's ProgressDialog ID number, so we can close the dialog
     *  when we're done. */
    private int progressDialogID = 0;
    /** The private Exporter class that does the grunt work of exporting data. */
    private Exporter exporter = null;
    /** The private OldFormatImporter class that does the grunt work of
     *  importing data from the old Android format. */
    private OldFormatImporter oldFormatImporter = null;
    /** The private XMLFormat1Importer class that does the grunt work of
     *  importing data from the new XML-based, cross-platform format. */
    private XMLFormat1Importer xmlFormatImporter = null;
    /** The full path to the file to import from */
    private String importFilename = null;
    /** The import password in plain text */
    private String importPassword = null;
    /** An Object array containing the list of site parameters imported
     *  from a file.  Note that this is an Object array and not an array
     *  of SiteParameter objects. */
    private Object[] importedSites = null;

    // Constructor **************************************************

    /**
     * The ImportExportHandler in intended to be a self-contained class for
     * writing Cryptnos import/export files.  Use the exportToFile() and
     * importFromFile() methods to perform these tasks.
     * @param caller The calling activity, used as a back-reference to
     * communicate back to the user
     * @param progressDialog A ProgressDialog, owned by the caller Activity,
     * that will be updated as the import/export process is performed
     * @param progressDialogID The ID of the ProgressDialog, internal to the
     * caller Activity. This is pulled out as another parameter because I
     * can't find a better way to get at it.
     */
    public ImportExportHandler(Activity caller, ProgressDialog progressDialog, int progressDialogID) {
        this.caller = caller;
        this.progressDialog = progressDialog;
        this.progressDialogID = progressDialogID;
        theApp = (CryptnosApplication) caller.getApplication();
        DBHelper = theApp.getDBHelper();
    }

    // Public Methods **************************************************

    /**
     * Export the parameters of the specified site tokens to an encrypted
     * file.  Note that starting with Cryptnos 1.1, this only export files
     * in the new XML-based, cross-platform format, not the original 1.0
     * platform-specific format.
     * @param filename The full path of the export file.
     * @param password The password used to encrypt the file.
     * @param sites An array of Strings containing the site tokens to export.
     */
    public void exportToFile(String filename, String password, String[] sites) {
        // Simple enough:  Make sure all the inputs appear to be valid, then
        // create the Exporter thread to do the grunt work.
        if (filename != null && password != null && sites != null && sites.length > 0) {
            exporter = new Exporter(caller, handler, sites, password, filename, theApp);
            exporter.start();
        }
        // If any of the inputs were invalid, inform the user:
        else {
            Toast.makeText(caller, R.string.error_bad_export_params, Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Import site parameters from the specified file.  Note that if any
     * site tokens in the file already exist in the database, the values in
     * the database will be overwritten with the values from the file.  This
     * method supports both the original platform-specific export format and
     * the new XML-based, cross-platform format, and should transparently
     * handle which export format the file was saved in.
     * @param filename The full path to the import file.
     * @param password The password used to decrypt the file:
     * @param importListener An ImportListener to notify once the import
     * is complete
     */
    public void importFromFile(String filename, String password, ImportListener importListener) {
        // As long as we've got inputs, we'll assume for now they've been
        // vetted by the caller and start the importer thread:
        if (filename != null && password != null) {
            // Take note of our import listener:
            this.importListener = importListener;
            // This is probably horribly inefficient, but we'll try and open
            // the file as an XML-based, cross-platform file first.  If that
            // doesn't work, then we'll fall back to the old format.
            // Unfortunately, this won't be pretty, as we'll have to let the
            // Handler below launch the old format attempt.
            importFilename = filename;
            importPassword = password;
            xmlFormatImporter = new XMLFormat1Importer(handler, password, filename, caller);
            xmlFormatImporter.start();
        }
        // If any of the inputs were invalid, inform the user:
        else {
            Toast.makeText(caller, R.string.error_bad_import_params, Toast.LENGTH_LONG).show();
        }
    }

    // Private Static Methods ***********************************************

    /**
     * Create the cipher used to encrypt or decrypt site parameter data for
     * the old platform-specific export format.
     * @param password A "secret" String value, usually the derived site
     * "key".  This is specified as an input parameter rather than using the
     * member variable because this method will be needed for one of the
     * constructors.
     * @param mode The Cipher encrypt/decryption mode.  This should be either
     * Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE.
     * @return A Cipher suitable for the encryption/decryption task.
     * @throws Exception Thrown if the mode is invalid or if any error occurs
     * while creating the cipher.
     */
    private static Cipher createOldFormatCipher(String password, int mode) throws Exception {
        // Asbestos underpants:
        try {
            // Generate our encryption salt.  This has to be something
            // semi-random but not tied to the device, so we can't use the
            // unique device ID like CryptnosApplication.PARAMETER_SALT.
            // We'll derive one from the password by hashing it multiple times.
            // Note that since this is the *OLD* format cipher, we can't use
            // the CryptnosApplication.getTextEncoding() here; this
            // *MUST* be the default character encoding for the platform for
            // it to be backward-compatible.
            byte[] salt = password.getBytes();
            MessageDigest hasher = MessageDigest.getInstance("SHA-512");
            for (int i = 0; i < CryptnosApplication.SALT_ITERATION_COUNT; i++)
                salt = hasher.digest(salt);

            // I had a devil of a time getting this to work, but I eventually
            // peeked at the Google "Secrets" application source code to get
            // to this setup.  The Password Based Key (PBE) spec lets us
            // specify a password to generate keys from.  We'll use the key
            // passed in (most likely a "site key" from the site parameters)
            // as that password, salting it with the device's unique ID to
            // give it some uniqueness from device to device.
            PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt,
                    CryptnosApplication.KEY_ITERATION_COUNT, CryptnosApplication.KEY_LENGTH);
            // Next we'll need a key factory to actually build the key:
            SecretKeyFactory keyFac = SecretKeyFactory.getInstance(CryptnosApplication.KEY_FACTORY);
            // The key is generated from the key factory:
            SecretKey key = keyFac.generateSecret(pbeKeySpec);
            // The cipher needs some parameter specs to know how to use
            // the key:
            AlgorithmParameterSpec aps = new PBEParameterSpec(salt, CryptnosApplication.KEY_ITERATION_COUNT);
            // Now that we have all of this information, actually start
            // creating the cipher:
            Cipher cipher = Cipher.getInstance(CryptnosApplication.KEY_FACTORY);
            // For our purposes, we're combining the creation of encryption
            // and decryption ciphers into one method.  So take the mode
            // passed in and initialize the cipher based on that mode.  Note
            // that the key and parameter specs are being pulled in at this
            // point, making the cipher complete.  If we get an invalid mode
            // type, throw an error.
            switch (mode) {
            case Cipher.ENCRYPT_MODE:
                cipher.init(Cipher.ENCRYPT_MODE, key, aps);
                break;
            case Cipher.DECRYPT_MODE:
                cipher.init(Cipher.DECRYPT_MODE, key, aps);
                break;
            default:
                throw new Exception("Invalid cipher mode");
            }
            // By now our cipher *should* be ready.  Go ahead an return it:
            return cipher;
        }
        // If anything blew up, throw it back out:
        catch (Exception e) {
            throw e;
        }
    }

    /** Given the user's password, generate a salt which will be mixed with
     *  the password when setting up the encryption parameters
     * @param password A string containing the user's password
     * @return An array of bytes containing the raw salt value
     * @throws Exception Thrown if the salt-generating hash is unavailable
     */
    private static byte[] generateSaltFromPassword(String password, CryptnosApplication theApp) throws Exception {
        // Get the password as a series of bytes.  Note that we're forcing UTF-8
        // here, regardless of what the user's preferred encoding might be. 
        byte[] salt = password.getBytes(CryptnosApplication.TEXT_ENCODING_UTF8);
        // Try to hash password multiple times using a really strong hash.
        // This should give us some really random-ish data for the salt.
        MessageDigest hasher = MessageDigest.getInstance("SHA-512");
        for (int i = 0; i < SALT_ITERATION_COUNT; i++) {
            // Java notes:  This is a lot easier than in .NET.  We
            // don't have to initialize the hash engine each time it's
            // used.  Just pass in the old salt to get the new.
            salt = hasher.digest(salt);
        }
        return salt;
    }

    /**
     * Create the cipher to handle encryption and decryption for the XML-based
     * cross-platform file format.
     * @param password A String containing the password, which will be used
      * to derive all our encryption parameters
     * @param encrypt A boolean value specifying whether we should go into
      * encryption mode (true) or decryption mode (false)
     * @return A BufferedBlockCipher in the specified mode
     * @throws Exception Thrown whenever anything bad happens
     */
    private static BufferedBlockCipher createXMLFormatCipher(String password, boolean encrypt,
            CryptnosApplication theApp) throws Exception {
        // I tried a dozen different things, none of which seemed to work
        // all that well.  I finally resorted to doing everyting the Bouncy
        // Castle way, simply because it brought things a lot closer to being
        // consistent.  Trying to do things entirely within .NET or Java just
        // wasn't cutting it.  There are, however, differences between the
        // implementations, which are denoted below.
        try {
            // Get the password's raw bytes.  Note that we're using UTF-8 here,
            // regardless of what the user's preferred encoding might be.
            byte[] pwd = password.getBytes(CryptnosApplication.TEXT_ENCODING_UTF8);
            byte[] salt = generateSaltFromPassword(password, theApp);
            // From the BC JavaDoc: "Generator for PBE derived keys and IVs as
            // defined by PKCS 5 V2.0 Scheme 2. This generator uses a SHA-1
            // HMac as the calculation function."  This is apparently a standard,
            // which makes my old .NET SecureFile class seem a bit embarrassing.
            PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator();
            // Initialize the generator with our password and salt.  Note the
            // iteration count value.  Examples I found around the net set this
            // as a hex value, but I'm not sure why advantage there is to that.
            // I changed it to decimal for clarity.  1000 iterations may seem
            // a bit excessive, and I saw some real sluggishness on the Android
            // emulator that could be caused by this.  In the final program,
            // this should probably be set in a global app constant.
            generator.init(pwd, salt, KEY_ITERATION_COUNT);
            // Generate our parameters.  We want to do AES-256, so we'll set
            // that as our key size.  That also implies a 128-bit IV.  Note
            // that the 2-int method used here is considered deprecated in the
            // .NET library, which could be a problem in the long term.  This
            // is where .NET and Java diverge in BC; this is the only method
            // available in Java, and the comparable method is deprecated in
            // .NET.  I'm not sure how this will work going forward.  We need
            // to watch this, as this could be a failure point down the road.
            ParametersWithIV iv = ((ParametersWithIV) generator.generateDerivedParameters(KEY_SIZE, IV_SIZE));
            // Create our AES (i.e. Rijndael) engine and create the actual
            // cipher object from it.  We'll use CBC padding.
            RijndaelEngine engine = new RijndaelEngine();
            BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(engine));
            // Pick our mode, encryption or decryption:
            cipher.init(encrypt, iv);
            // Return the cipher:
            return cipher;
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * Given a cipher and the size of an input file, determine whether or not we have
     * enough memory on hand to encrypt or decrypt the data.  Since we have to do all
     * our cryptography in memory, we can only work with the amount of memory
     * currently available.
     * @param cipher The BufferedBlockCipher we'll be using to encrypt/decrypt
     * @param fileSize The size of the input file in bytes
     * @param caller The calling activity
     * @return True if there's sufficient memory to decrypt the file, false otherwise
     */
    private static boolean haveSufficientMemory(BufferedBlockCipher cipher, boolean encrypting, long fileSize,
            Activity caller) {
        // There are other factors that should eliminate this before we get to this
        // step, but we can't deal with a file larger than 2GB.  There aren't any
        // current Android devices with that much RAM anyway.
        if (fileSize > (long) Integer.MAX_VALUE)
            return false;
        // As an error check, make sure the cipher and caller objects are not null:
        if (cipher == null || caller == null)
            return false;
        // Now put on our asbestos underpants:
        try {
            // Get the memory information from the activity service:
            MemoryInfo mi = new MemoryInfo();
            ActivityManager activityManager = (ActivityManager) caller.getSystemService(Context.ACTIVITY_SERVICE);
            activityManager.getMemoryInfo(mi);
            // In order to decrypt the file, we'll need at least as many bytes as
            // the encrypted and decrypted data combined.  In order to get this,
            // we'll add the size of the input file to the output size of the data
            // after it passes through the cipher.  Note that it doesn't matter
            // whether we're doing encryption or decryption at this point; the cipher
            // object knows and the output size will be appropriate for the output
            // mode.  If the sum of these two values is less than the available
            // memory, we should be good to go.
            //return mi.availMem > fileSize + (long)cipher.getOutputSize((int)fileSize);
            if (encrypting)
                return mi.availMem > fileSize + (long) cipher.getOutputSize((int) fileSize);
            else
                return mi.availMem > (long) cipher.getBlockSize() + (long) cipher.getOutputSize((int) fileSize);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Given a cipher and the size of an input file, determine whether or not we have
     * enough memory on hand to encrypt or decrypt the data.  Since we have to do all
     * our cryptography in memory, we can only work with the amount of memory
     * currently available.
     * @param cipher The Cipher we'll be using to encrypt/decrypt
     * @param fileSize The size of the input file in bytes
     * @param caller The calling activity
     * @return True if there's sufficient memory to decrypt the file, false otherwise
     */
    private static boolean haveSufficientMemory(Cipher cipher, long fileSize, Activity caller) {
        // This is essentially the exact same method as above, only we're looking at
        // a javax.crypto.Cipher object rather than a Bouncy Castle one.  The code
        // is exactly the same, but the different class means we need a different
        // input signature and thus a different method.
        if (fileSize > (long) Integer.MAX_VALUE)
            return false;
        if (cipher == null || caller == null)
            return false;
        try {
            MemoryInfo mi = new MemoryInfo();
            ActivityManager activityManager = (ActivityManager) caller.getSystemService(Context.ACTIVITY_SERVICE);
            activityManager.getMemoryInfo(mi);
            return mi.availMem > fileSize + (long) cipher.getOutputSize((int) fileSize);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * This handler receives messages from the various worker threads and
     * updates the calling Activity's ProgessDialog with their status.  If
     * the status is 100%, this closes the progress dialog and shuts down
     * the thread.  Negative "percentage" statuses usually indicate some
     * sort of error.
     * @author Jeffrey T. Darlington
     * @version 1.1
     * @since 1.0
     */
    private final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            // Get our percent done and update the progress dialog:
            int total = msg.getData().getInt("percent_done");
            if (total >= 0)
                progressDialog.setProgress(total);
            int count = msg.getData().getInt("site_count");
            // If we reach 100%, it's time to close up shop:
            if (total >= 100) {
                // Originally, we used dismissDialog() here to close the
                // dialog.  The difference is that dismiss keeps the dialog
                // around in memory, which is more efficient.  The caveat is
                // that the next time we need it, our ListBuilderThread won't
                // work properly.  So instead we'll remove the dialog once
                // we're done, forcing Android to rebuild it and refresh the
                // list every time.  Less efficient, but at least it works.
                caller.removeDialog(progressDialogID);
                // Create our success message:
                String message = null;
                // Check to see if one of the importers was being used.  If
                // so, we'll want to show the import complete message:
                if (oldFormatImporter != null || xmlFormatImporter != null) {
                    // Send the list of imported sites back to the listener waiting
                    // to receive them:
                    importListener.onSitesImported(importedSites);
                    // If we didn't use one of the importers, we must have used
                    // the exporter.  Show the export complete message:
                } else {
                    message = caller.getResources().getString(R.string.export_complete_message);
                    message = message.replace(caller.getResources().getString(R.string.meta_replace_token),
                            String.valueOf(count));
                    Toast.makeText(caller, message, Toast.LENGTH_LONG).show();
                    // Close the calling activity:
                    caller.finish();
                }
                // If we got a "percentage" of -1, that indicates a bad import file
                // or bad import password.  Warn the user as such and kill the
                // dialog:
            } else if (total == -1) {
                caller.removeDialog(progressDialogID);
                Toast.makeText(caller, R.string.error_bad_import_file_or_password, Toast.LENGTH_LONG).show();
                // A "percentage" of -2 indicates that the import file could not
                // be found, couldn't be read, or wasn't really a file (maybe it
                // was a directory).  Warn the user and close the dialog:
            } else if (total == -2) {
                caller.removeDialog(progressDialogID);
                Toast.makeText(caller, R.string.error_bad_import_file, Toast.LENGTH_LONG).show();
                // A "percentage" of -3 indicates a general error during the export
                // process.  Warn the user and close the dialog:
            } else if (total == -3) {
                caller.removeDialog(progressDialogID);
                Toast.makeText(caller, R.string.error_bad_export, Toast.LENGTH_LONG).show();
                // A "percentage" of -4 indicates the user's export parameters
                // weren't up to snuff.  Warn the user and close the dialog:
            } else if (total == -4) {
                caller.removeDialog(progressDialogID);
                Toast.makeText(caller, R.string.error_bad_export_params, Toast.LENGTH_LONG).show();
                // A "percentage" of -5 indicates there is insufficient memory to
                // encrypt or decrypt the file.  Warn the user and close the dialog:
            } else if (total == -5) {
                caller.removeDialog(progressDialogID);
                Toast.makeText(caller, R.string.error_insufficient_memory, Toast.LENGTH_LONG).show();
                // A "percentage" of -1000 indicates something blew up while
                // trying to import the file in the cross-platform format.  Now
                // it's time to fall back and punt with the old format.  Create
                // the old format importer and let it have a go:
            } else if (total == -1000) {
                oldFormatImporter = new OldFormatImporter(handler, importPassword, importFilename, caller);
                oldFormatImporter.start();
            }
        }
    };

    // Private Worker Threads ************************************************

    /**
     * This Thread performs the grunt work of the Cryptnos export process.
     * Note that this class has changed starting with 1.1 to export only to
     * the new XML-based cross-platform format.
     * @author Jeffrey T. Darlington
     * @version 1.1
     * @since 1.0
     */
    private class Exporter extends Thread {
        /** The Handler to update our status to */
        private Handler mHandler;
        /** Our calling Activity */
        private Activity mCaller;
        /** The password used to encrypt the file */
        private String mPassword;
        /** The full path to the export file */
        private String mFilename;
        /** An array of Strings containing the site tokens of the parameters
         * to export */
        private String[] mSites = null;
        private CryptnosApplication theApp = null;

        /**
         * The Exporter constructor
         * @param caller The calling Activity
         * @param handler The Handler to update our status to
         * @param sites An array of Strings[] containing the site tokens of
         * the parameters to export
         * @param password The password used to encrypt the file
         * @param filename The full path to the export file
         */
        Exporter(Activity caller, Handler handler, String[] sites, String password, String filename,
                CryptnosApplication app) {
            mCaller = caller;
            mHandler = handler;
            mSites = sites;
            mPassword = password;
            mFilename = filename;
            theApp = app;
        }

        @Override
        public void run() {
            // Get us started:
            Message msg = null;
            Bundle b = null;
            // Assuming there are sites to export:
            if (mSites.length > 0) {
                try {
                    // We want to write out our data to memory so we can
                    // subsequently encrypt it.  This is, as far as I can tell,
                    // the best equivalent to .NET's MemoryStream I can find:
                    ByteArrayOutputStream ms = new ByteArrayOutputStream();
                    // Neither Android nor Java have any conveniences for
                    // *writing* XML, so we'll have to do it by hand.  Create
                    // a PrintStream to conveniently write our text out, and
                    // then pipe that through a GZIPOutputStream to compress
                    // it.  All of this chains into the ByteArrayOutputStream
                    // above, so we'll ultimately end up with a byte array
                    // that contains the compressed XML.  Note that we encode
                    // this with UTF-8 regardless of any user preference.
                    PrintStream out = new PrintStream(new GZIPOutputStream(ms), true,
                            CryptnosApplication.TEXT_ENCODING_UTF8);
                    // Print our our XML header info.  Note that we're writing
                    // a version 1 export file; later changes to the format
                    // may require us to update that.  Also note that we'll
                    // try to get the package info and extract the friendly
                    // version number code to put in the <generator> tag.
                    // If that blows up for some reason, we won't write out
                    // the tag; it's optional but strongly recommended.  (At
                    // least it will help with debugging.)
                    out.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
                    out.println("<cryptnos xmlns=\"http://www.cryptnos.com/\">");
                    out.println("\t<version>1</version>");
                    try {
                        PackageInfo info = theApp.getPackageManager().getPackageInfo(theApp.getPackageName(),
                                PackageManager.GET_META_DATA);
                        out.println("\t<generator>Cryptnos for Android v" + info.versionName + "</generator>");
                    } catch (Exception e) {
                    }
                    // Print out the site count tag.  We'll assume for now
                    // that every site we've been passed in will get exported,
                    // which should always be the case unless something is
                    // seriously wrong.
                    out.println("\t<siteCount>" + String.valueOf(mSites.length) + "</siteCount>");
                    // Now we'll begin our <sites> block.  From here on out,
                    // we'll need to iterate over our sites to export:
                    out.println("\t<sites>");
                    // Declare our cursor:
                    Cursor cursor = null;
                    // Step through the sites in our list.  It might be more
                    // efficient to just get all the sites in the DB and
                    // compare the two lists, but for now we'll pull each
                    // site from the DB individually.
                    for (int i = 0; i < mSites.length; i++) {
                        // Get the site from the DB:
                        cursor = DBHelper.fetchRecord(SiteParameters.generateKeyFromSite(mSites[i], theApp));
                        cursor.moveToFirst();
                        if (cursor.getCount() == 1) {
                            // Convert it to a SiteParamemters object: 
                            SiteParameters params = new SiteParameters(theApp, cursor.getString(1),
                                    cursor.getString(2));
                            // Generate the XML tags from the SiteParameters
                            // object.  There's not much to comment on here,
                            // aside from the fact that we'll HTML-encode
                            // the text fields to make sure they go through
                            // without a problem.
                            out.println("\t\t<site>");
                            out.println(
                                    "\t\t\t<siteToken>" + TextUtils.htmlEncode(params.getSite()) + "</siteToken>");
                            out.println("\t\t\t<hash>" + TextUtils.htmlEncode(params.getHash()) + "</hash>");
                            out.println("\t\t\t<iterations>" + String.valueOf(params.getIterations())
                                    + "</iterations>");
                            out.println(
                                    "\t\t\t<charTypes>" + String.valueOf(params.getCharTypes()) + "</charTypes>");
                            out.println(
                                    "\t\t\t<charLimit>" + String.valueOf(params.getCharLimit()) + "</charLimit>");
                            out.println("\t\t</site>");
                        }
                        // Update the progress dialog by sending a message to
                        // the handler.  Note that we're only going up to 90%
                        // here, as we'll estimate the rest of the work will
                        // encompass the remaining 10%.
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", (int) (Math.floor(((double) i / (double) mSites.length * 90.0d))));
                        b.putInt("site_count", mSites.length);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                        // Close the cursor for the next run:
                        cursor.close();
                    }
                    // Close out the <sites> block and the rest of the file:
                    out.println("\t</sites>");
                    out.println("</cryptnos>");
                    out.flush();
                    out.close();
                    // Now our memory stream should contain the raw binary
                    // data of our compressed XML.  Grab that and put it into
                    // a byte array:
                    byte[] plaintext = ms.toByteArray();
                    ms.close();
                    ms = null;
                    out = null;
                    // Create our cipher.  Note that we're using the
                    // encryption mode, and that we're passing in the
                    // password:
                    BufferedBlockCipher cipher = createXMLFormatCipher(mPassword, true, theApp);
                    // In order to encrypt the data, we need to be able to hold both
                    // the plain text and cipher text in memory at the same time.
                    // Make sure we've got enough RAM on hand to do that first:
                    if (ImportExportHandler.haveSufficientMemory(cipher, true, (long) plaintext.length, mCaller)) {
                        // Create our ciphertext container.  Note that we call the
                        // cipher's getOutputSize() method, which tells us how big
                        // the resulting ciphertext should be.  In practice, this
                        // has always been the same size as the plaintext, but we
                        // can't take that for granted.
                        byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)];
                        // Do the encryption.  Note that the Java version is a bit
                        // different from the .NET version.  Here, we need to process
                        // most of the data with processBytes() first, then do a final
                        // call to doFinal() to finish things off.  Note that we also
                        // have to keep track of the number of bytes processed in
                        // the first step so we can pass it to the final step.  That
                        // was a major gotcha that caused me a lot of headaches.
                        int bytesSoFar = cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
                        cipher.doFinal(ciphertext, bytesSoFar);
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", 95);
                        b.putInt("site_count", mSites.length);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                        plaintext = null;
                        // Write the ciphertext to the export file:
                        FileOutputStream fos = new FileOutputStream(mFilename);
                        fos.write(ciphertext);
                        fos.flush();
                        fos.close();
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", 100);
                        b.putInt("site_count", mSites.length);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                        ciphertext = null;
                        fos = null;
                        // Insufficient memory to encrypt:
                    } else {
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", -5);
                        b.putInt("site_count", 0);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                    }
                }
                // We should probably provide more detailed information here,
                // but for now just tell the user that the export failed.
                catch (Exception e) {
                    msg = mHandler.obtainMessage();
                    b = new Bundle();
                    b.putInt("percent_done", -3);
                    b.putInt("site_count", 0);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                }
                // This should have already been covered by the caller, but if
                // if we got bad inputs, complain:
            } else {
                msg = mHandler.obtainMessage();
                b = new Bundle();
                b.putInt("percent_done", -4);
                b.putInt("site_count", 0);
                msg.setData(b);
                mHandler.sendMessage(msg);
            }
        }
    }

    /**
      * This Thread performs the grunt work of the Cryptnos import process if
      * the file is in the old platform-specific format.
     * @author Jeffrey T. Darlington
     * @version 1.3.0
     * @since 1.0
     */
    private class OldFormatImporter extends Thread {
        /** The Handler to update our status to */
        private Handler mHandler;
        /** The password used to decrypt the file */
        private String mPassword;
        /** The full path to the import file */
        private String mFilename;
        /** The calling activity, passed down from ImportExportHandler */
        private Activity mActivity;

        /**
         * The Importer constructor
         * @param handler The Handler to update our status to
         * @param password The password used to decrypt the file
         * @param filename The full path to the import file
         * @param filename The calling activity, passed down from ImportExportHandler
         */
        OldFormatImporter(Handler handler, String password, String filename, Activity activity) {
            mHandler = handler;
            mPassword = password;
            mFilename = filename;
            mActivity = activity;
        }

        @Override
        public void run() {
            Message msg = null;
            Bundle b = null;

            try {
                // Try to get the specified file and make sure it exists, it
                // actually is a file, it's readable, and it's size does not
                // exceed the maximum size of an integer (if it's too big,
                // we can't read it):
                File file = new File(mFilename);
                if (file.exists() && file.isFile() && file.canRead() && file.length() <= (long) Integer.MAX_VALUE) {
                    // Create our cipher for decrpting:
                    Cipher cipher = createOldFormatCipher(mPassword, Cipher.DECRYPT_MODE);
                    // Check to make sure we have enough memory before trying the
                    // decryption:
                    if (ImportExportHandler.haveSufficientMemory(cipher, file.length(), mActivity)) {
                        // There's no way around this, but we need to allocate a
                        // full-size buffer to contain the entire decrypted XML:
                        byte[] plaintext = new byte[cipher.getOutputSize((int) file.length())];
                        // Next, declare a few variables for the decryption.  We
                        // need to keep track of the total number of bytes read,
                        // as well as how many bytes we read in a given pass.
                        // We'll also grab the block size of the cipher and the
                        // file length so we don't have to call object methods and
                        // cast longs to ints all the time.
                        int bytesSoFar = 0;
                        int bytesRead = 0;
                        int blockSize = cipher.getBlockSize();
                        int fileLength = (int) file.length();
                        // Allocate a buffer to read in one block of the encrypted
                        // data at a time.  This will prevent us from having to
                        // slurp the entire file just to decrypt it.
                        byte[] buffer = new byte[blockSize];
                        // Open the file, then start looping through the data, one
                        // block at a time:
                        FileInputStream fis = new FileInputStream(file);
                        while (bytesSoFar < fileLength) {
                            // Read a block into the buffer.  If we didn't read
                            // any data, we're done and we'll break the loop.
                            bytesRead = fis.read(buffer, 0, blockSize);
                            if (bytesRead <= 0)
                                break;
                            // Run the block through the cipher and put the decrypted
                            // data into the "plain text" buffer:
                            bytesRead = cipher.update(buffer, 0, bytesRead, plaintext, bytesSoFar);
                            // Bump up the total number of bytes read:
                            bytesSoFar += bytesRead;
                            // Send a message to the handler to update the progress
                            // dialog.  We'll assume that decrypting the data is
                            // half the work, so scale the percentage to 1-50%
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done",
                                    (int) (Math.floor(((double) bytesSoFar / (double) fileLength * 50.0d))));
                            b.putInt("site_count", 0);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                        }
                        // By now we should have exhausted the file.  Close the
                        // stream and null out it and the buffer so that memory
                        // can be reclaimed.
                        fis.close();
                        fis = null;
                        buffer = null;
                        // Do the final pass on the cipher.  After this step, the
                        // "plain text" array should have the fully decrypted data.
                        cipher.doFinal(plaintext, bytesSoFar);
                        // Convert the plain text bytes into a string:
                        String unencryptedData = new String(plaintext);
                        // Now free up some memory by nulling out the byte array
                        // and releasing the cipher:
                        plaintext = null;
                        cipher = null;
                        // Split the decrypted data based on newlines:
                        String[] sites = unencryptedData.split("\n");
                        // Note that we're using the length of the site list minus
                        // one as our stopping point.  This was added in version
                        // 1.2.3 because I discovered that the original exporter
                        // code added an extra, empty line at the end of the file,
                        // resulting in an empty final string here.  Thus, whenever
                        // an old format file was being imported, it would blow up,
                        // stating that the file was invalid, while all the sites
                        // were actually imported successfully.  Skipping the last
                        // string in the array should eliminate this problem.
                        int siteCount = sites.length - 1;
                        // If there are any sites in the file to import:
                        if (siteCount > 0) {
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done", 50);
                            b.putInt("site_count", siteCount);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                            // Declare our array to hold the imported sites:
                            importedSites = new Object[siteCount];
                            // Loop through the sites:
                            for (int i = 0; i < siteCount; i++) {
                                // Try to recreate the site parameters object from the
                                // data.  Note that this will blow up if the data is
                                // invalid.
                                SiteParameters params = new SiteParameters(theApp, sites[i]);
                                // Stuff the new set of parameters into the array:
                                importedSites[i] = (Object) params;
                                // Update the progress dialog.  Note that we're at the tail
                                // end of the process here, so we're saying that reading the
                                // file and decrypting the data amounts to half of the work.
                                // We're doing the remaining 50%, so scale what we've done
                                // to 1-50 and add the remaining 50% on top.
                                msg = mHandler.obtainMessage();
                                b = new Bundle();
                                b.putInt("percent_done",
                                        (int) (Math.floor(((double) i / (double) siteCount * 50.0d))) + 50);
                                b.putInt("site_count", siteCount);
                                msg.setData(b);
                                mHandler.sendMessage(msg);
                            }
                            // Just to make sure, force the progress dialog to say we're at
                            // 100%:
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done", 100);
                            b.putInt("site_count", siteCount);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                            // There were no sites in the file, so say it was
                            // invalid:
                        } else {
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done", -1);
                            b.putInt("site_count", 0);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                        }
                        // Insufficient memory to decrypt:
                    } else {
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", -5);
                        b.putInt("site_count", 0);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                    }
                    // The file could not be read, didn't exist, or wasn't a
                    // file at all:
                } else {
                    msg = mHandler.obtainMessage();
                    b = new Bundle();
                    b.putInt("percent_done", -2);
                    b.putInt("site_count", 0);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                }
            }
            // Any number of things may have occurred to make the above
            // explode.  We should probably test for every option, but for
            // now we'll just assume the file wasn't valid and let it go.
            // Send a message back to the handler effectively saying as such:
            catch (Exception e) {
                msg = mHandler.obtainMessage();
                b = new Bundle();
                b.putInt("percent_done", -1);
                b.putInt("site_count", 0);
                msg.setData(b);
                mHandler.sendMessage(msg);
            }
        }
    }

    /**
     * This XML SAX handler  will process parsing the new XML-based export
     * format, ultimately building a list of site parameters to return to
     * the importer.
     * @author Jeffrey T. Darlington
     * @version 1.1
     * @since 1.1
     */
    private class XMLHandler extends DefaultHandler {
        /** An ArrayList of SiteParameters holding the current list sites
         *  we have successfully parsed from the file */
        private ArrayList<SiteParameters> siteList = null;
        /** The current set of working parameters we are actively building */
        private SiteParameters currentSite = null;
        /** A StringBuilder to let us gather the tag values piecemeal if
         *  necessary */
        private StringBuilder builder = null;
        /** The count of sites in the file, as reported by the
         *  &lt;siteCount&gt; tag */
        private int siteCount = 0;
        /** Whether or not we are currently inside the &lt;cryptnos&gt; tag */
        private boolean inCryptnosTag = false;
        /** Whether or not we are currently inside the &lt;version&gt; tag */
        private boolean inVersionTag = false;
        /** Whether or not we are currently inside either  the
         *  &lt;generator&gt; or &lt;comment&gt; tags, which are currently
         *  ignored */
        private boolean inIgnoredTag = false;
        /** Whether or not we are currently inside the &lt;siteCount&gt; tag */
        private boolean inSiteCountTag = false;
        /** Whether or not we are currently inside the &lt;sites&gt; tag */
        private boolean inSitesTag = false;
        /** Whether or not we are currently inside a &lt;site&gt; tag */
        private boolean inSiteTag = false;
        /** Whether or not we are currently in one of the parameter tags */
        private boolean inParamTag = false;
        /** A reference back to the Handler that updates the GUI of our
         *  progress, so we can update the progress dialog */
        private Handler topHandler = null;
        /** A Message to pass back to the progress dialog */
        private Message msg = null;
        /** A Bundle for communicating with the progress dialog */
        private Bundle b = null;

        /**
         * The XMLHandler constructor
         * @param topHandler A reference back to the caller's handler, so
         * we can update the progress dialog
         */
        XMLHandler(Handler topHandler) {
            super();
            this.topHandler = topHandler;
        }

        @Override
        public void startDocument() throws SAXException {
            // Start off by letting the super do its work, then initialize
            // our site list and StringBuilder to get things started:
            super.startDocument();
            siteList = new ArrayList<SiteParameters>();
            builder = new StringBuilder();
        }

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            // Let the super do its work first:
            super.startElement(uri, localName, name, attributes);
            // Take a look at the string builder's current contents.  If all
            // we can find is whitespace, chop that out and ignore it.  This
            // is to get around some problems I encountered where whitespace
            // was making its way into values where it shouldn't.  We don't
            // care about whitespace *between* tags, just whitespace that may
            // be *part* of tags, such as whitespace inside the <siteToken>
            // tag value.
            if (builder.toString().matches("^\\s+$"))
                builder.setLength(0);
            // Are we entering the <cryptnos> tag?  Note this must be at the
            // root of everything, so this tag cannot be inside any other
            // tag, including a nested <cryptnos> tag.
            if (localName.equalsIgnoreCase("cryptnos") && !inVersionTag && !inIgnoredTag && !inSitesTag
                    && !inSiteTag && !inParamTag && !inSiteCountTag && !inCryptnosTag) {
                inCryptnosTag = true;
                // Are we entering the <version> tag?  Note this should only be
                // valid if we're inside the <cryptnos> tag as well.
            } else if (localName.equalsIgnoreCase("version") && inCryptnosTag && !inIgnoredTag && !inSitesTag
                    && !inSiteTag && !inParamTag && !inVersionTag && !inSiteCountTag) {
                inVersionTag = true;
                // Are we entering the <siteCount> tag?  Note this should only be
                // valid if we're inside the <cryptnos> tag as well.
            } else if (localName.equalsIgnoreCase("siteCount") && inCryptnosTag && !inIgnoredTag && !inSitesTag
                    && !inSiteTag && !inParamTag && !inVersionTag && !inSiteCountTag) {
                inSiteCountTag = true;
                // Are we entering the <sites> tag?  Note this should only be
                // valid if we're inside the <cryptnos> tag as well.
            } else if (localName.equalsIgnoreCase("sites") && inCryptnosTag && !inVersionTag && !inIgnoredTag
                    && !inSiteTag && !inParamTag && !inSitesTag && !inSiteCountTag) {
                inSitesTag = true;
                msg = topHandler.obtainMessage();
                b = new Bundle();
                b.putInt("percent_done", 15);
                b.putInt("site_count", 0);
                msg.setData(b);
                topHandler.sendMessage(msg);
                // Are we entering the <generator> or <comment> tags?  These
                // should only be valid if we're inside the <cryptnos> tag.  Also
                // note we don't really care what's in them; they are just going
                // to be ignored for now.
            } else if ((localName.equalsIgnoreCase("generator") || localName.equalsIgnoreCase("comment"))
                    && inCryptnosTag && !inVersionTag && !inSitesTag && !inSiteTag && !inParamTag && !inIgnoredTag
                    && !inSiteCountTag) {
                inIgnoredTag = true;
                // Are we entering a <site> tag?  We must be inside the <cryptnos>
                // and <sites> tag for this to be valid.  This is the main place
                // we actually do work on a start tag, because we need to
                // initialize the current site object before continuing.
            } else if (localName.equalsIgnoreCase("site") && inCryptnosTag && inSitesTag && !inVersionTag
                    && !inIgnoredTag && !inSiteTag && !inParamTag && !inSiteCountTag) {
                this.currentSite = new SiteParameters(theApp);
                inSiteTag = true;
                // Are we entering one of the parameter tags?  We have to be
                // deeply nested inside a <site> tag and all its hierarchy for
                // this to be valid.  We won't bother being persnickety about
                // keeping track of which tag we're in.  We probably *should* but
                // for now this is more complex than I really wanted anyway.
            } else if (inCryptnosTag && inSitesTag && inSiteTag && !inVersionTag && !inIgnoredTag && !inParamTag
                    && !inSiteCountTag
                    && (localName.equalsIgnoreCase("siteToken") || localName.equalsIgnoreCase("hash")
                            || localName.equalsIgnoreCase("iterations") || localName.equalsIgnoreCase("charTypes")
                            || localName.equalsIgnoreCase("charLimit"))) {
                inParamTag = true;
                // If *ANYTHING* blew up above, then our XML is not well formed
                // and doesn't follow the schema.  Blow up:
            } else
                throw new SAXException("Unexpected tag or invalid tag order");
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            // Let the super do its work, then pull out the characters we
            // read and stuff them in the StringBuilder.  Hopefully, this
            // we be a value we can use later.
            super.characters(ch, start, length);
            builder.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            // Let the super do its work:
            super.endElement(uri, localName, name);
            try {
                // Note that in all of these cases, we don't get as anal as
                // we did about tag nesting as when we were processing the
                // start tags.  The way I figure it, that did most of our
                // schema checking for us, so there's no need to do it again.
                // Here, we're mostly checking to close out our status and do
                // anything that needs to be done by closing the tag, such as
                // processing the value.  In each case, though, we need to
                // make sure that the tag we're closing has already been
                // opened.

                // We encountered a closing <cryptnos> tag:
                if (localName.equalsIgnoreCase("cryptnos") && inCryptnosTag) {
                    inCryptnosTag = false;
                    // We encountered a closing <version> tag.  If that's the case,
                    // pull the value from the StringBuilder and attempt to parse
                    // it to an integer.  If the value is 1 (the only valid file
                    // version we currently support), then the file is valid;
                    // otherwise, blow up:
                } else if (localName.equalsIgnoreCase("version") && inVersionTag) {
                    inVersionTag = false;
                    int version = Integer.parseInt(builder.toString().trim());
                    if (version != 1)
                        throw new Exception();
                    // We encountered a closing <generator> or <comment> tag:
                } else if ((localName.equalsIgnoreCase("generator") || localName.equalsIgnoreCase("comment"))
                        && inIgnoredTag) {
                    inIgnoredTag = false;
                    // We encountered a closing <siteCount> tag.  Parse the value
                    // and make sure it is an integer greater than zero; we cannot
                    // have an export file that does not have at least one site.
                } else if (localName.equalsIgnoreCase("siteCount") && inSiteCountTag) {
                    inSiteCountTag = false;
                    siteCount = Integer.parseInt(builder.toString().trim());
                    if (siteCount <= 0)
                        throw new Exception();
                    // We encountered a closing <sites> tag:
                } else if (localName.equalsIgnoreCase("sites") && inSitesTag) {
                    inSitesTag = false;
                    // We encountered a closing <siteToken> tag.  Note that this
                    // and the rest of the tags below are only valid if we're (a)
                    // inside a <site> tag and (b) the current site object is
                    // valid.
                } else if (localName.equalsIgnoreCase("siteToken") && inSiteTag && inParamTag
                        && currentSite != null) {
                    currentSite.setSite(builder.toString().trim());
                    inParamTag = false;
                    // We encountered a closing <hash> tag:
                } else if (localName.equalsIgnoreCase("hash") && inSiteTag && inParamTag && currentSite != null) {
                    currentSite.setHash(builder.toString().trim());
                    inParamTag = false;
                    // We encountered a closing <iterations> tag.  Note that we
                    // need to parse the value into an integer for this one and
                    // the next two, so if we didn't get an integer, this will
                    // blow up.
                } else if (localName.equalsIgnoreCase("iterations") && inSiteTag && inParamTag
                        && currentSite != null) {
                    currentSite.setIterations(Integer.parseInt(builder.toString().trim()));
                    inParamTag = false;
                    // We encountered a closing <charTypes> tag:
                } else if (localName.equalsIgnoreCase("charTypes") && inSiteTag && inParamTag
                        && currentSite != null) {
                    currentSite.setCharTypes(Integer.parseInt(builder.toString().trim()));
                    inParamTag = false;
                    // We encountered a closing <charLimit> tag:
                } else if (localName.equalsIgnoreCase("charLimit") && inSiteTag && inParamTag
                        && currentSite != null) {
                    currentSite.setCharLimit(Integer.parseInt(builder.toString().trim()));
                    inParamTag = false;
                    // We encountered a closing <site> tag.  Again, we have to be
                    // in a <site> tag and the current site must be valid for this
                    // to work.  Take the site parameters object and stuff it into
                    // the site list.
                } else if (localName.equalsIgnoreCase("site") && inSiteTag && currentSite != null) {
                    siteList.add(currentSite);
                    inSiteTag = false;
                    // For our percent done, we're scaling this part of the
                    // process one third of the work, or 34-66%.  The
                    // first 33% is the reading and decrypting of the data,
                    // while the remaining 33% will be updating the database.
                    msg = topHandler.obtainMessage();
                    b = new Bundle();
                    b.putInt("percent_done",
                            (int) (Math.floor(((double) siteList.size() / (double) siteCount * 33.0d))) + 33);
                    b.putInt("site_count", 0);
                    b.putInt("site_count", siteCount);
                    msg.setData(b);
                    topHandler.sendMessage(msg);
                    // If we didn't encounter any of the above conditions, the
                    // schema must not be valid:
                } else
                    throw new Exception();
                // In pretty much every case, we'll need to clear out the
                // StringBuilder anyway, so we'll just do it once here:
                builder.setLength(0);
            }
            // If we caught any exceptions, throw a SAXException here to
            // indicate that our parsing was invalid:
            catch (Exception ex) {
                throw new SAXException("Invalid data type");
            }
        }

        /**
         * Get the list of SiteParameters parsed from the XML
         * @return An array of SiteParameters objects
         */
        Object[] getSites() {
            // This only makes sense if the site list is populated.  If it
            // isn't, return null:
            if (siteList == null || siteList.isEmpty())
                return null;
            else
                return siteList.toArray();
        }
    }

    /**
     * This Thread performs the grunt work of the Cryptnos import process if
      * the file is in the new XML-based cross-platform format.
     * @author Jeffrey T. Darlington
     * @version 1.3.0
     * @since 1.1
     */
    private class XMLFormat1Importer extends Thread {
        /** The Handler to update our status to */
        private Handler mHandler;
        /** The password used to decrypt the file */
        private String mPassword;
        /** The full path to the import file */
        private String mFilename;
        /** The calling activity, passed down from ImportExportHandler */
        private Activity mActivity;

        /**
         * The XMLFormat1Importer constructor
         * @param handler The Handler to update our status to
         * @param password The password used to decrypt the file
         * @param filename The full path to the import file
         * @param filename The calling activity, passed down from ImportExportHandler
         */
        XMLFormat1Importer(Handler handler, String password, String filename, Activity activity) {
            mHandler = handler;
            mPassword = password;
            mFilename = filename;
            mActivity = activity;
        }

        @Override
        public void run() {
            Message msg = null;
            Bundle b = null;

            try {
                // Try to get the specified file and make sure it exists, it
                // actually is a file, it's readable, and it's size does not
                // exceed the maximum size of an integer (if it's too big,
                // we can't read it):
                File file = new File(mFilename);
                if (file.exists() && file.isFile() && file.canRead() && file.length() < (long) Integer.MAX_VALUE) {
                    // Create our cipher in decrypt mode:
                    BufferedBlockCipher cipher = createXMLFormatCipher(mPassword, false, theApp);
                    // Given the cipher and file length, check to see if we have
                    // enough memory on hand to decrypt the data:
                    if (ImportExportHandler.haveSufficientMemory(cipher, false, file.length(), mActivity)) {
                        // There's no way around this, but we need to allocate a
                        // full-size buffer to contain the entire decrypted XML:
                        byte[] plaintext = new byte[cipher.getOutputSize((int) file.length())];
                        // Next, declare a few variables for the decryption.  We
                        // need to keep track of the total number of bytes read,
                        // as well as how many bytes we read in a given pass.
                        // We'll also grab the block size of the cipher and the
                        // file length so we don't have to call the object
                        // methods and cast longs to ints repeatedly.
                        int bytesSoFar = 0;
                        int bytesRead = 0;
                        int blockSize = cipher.getBlockSize();
                        int fileLength = (int) file.length();
                        // Allocate a buffer to read in one block of the encrypted
                        // data at a time.  This will prevent us from having to
                        // slurp the entire file just to decrypt it.
                        byte[] buffer = new byte[blockSize];
                        // Open the file, then start looping through the data, one
                        // block at a time:
                        FileInputStream fis = new FileInputStream(file);
                        while (bytesSoFar < fileLength) {
                            // Read a block into the buffer.  If we didn't read
                            // any data, we're done and we'll break the loop.
                            bytesRead = fis.read(buffer, 0, blockSize);
                            if (bytesRead <= 0)
                                break;
                            // Run the block through the cipher and put the decrypted
                            // data into the "plain text" buffer:
                            bytesRead = cipher.processBytes(buffer, 0, bytesRead, plaintext, bytesSoFar);
                            // Bump up the total number of bytes read:
                            bytesSoFar += bytesRead;
                            // Update the progress bar by sending a message to the
                            // handler.  We'll assume that decrypting the file is
                            // 33% of the work, so we'll scale it appropriately.
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done",
                                    (int) (Math.floor(((double) bytesRead / (double) fileLength * 33.0d))));
                            b.putInt("site_count", 0);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                        }
                        // By now we should have exhausted the file.  Close the
                        // stream and null out it and the buffer so that memory
                        // can be reclaimed.
                        fis.close();
                        fis = null;
                        buffer = null;
                        // Do the final pass on the cipher.  After this step, the
                        // "plain text" array should have the fully decrypted data.
                        cipher.doFinal(plaintext, bytesSoFar);
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", 33);
                        b.putInt("site_count", 0);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                        // Now that we're done decrypting, release the cipher
                        // and free up memory:
                        cipher = null;
                        // Now we need to parse the decrypted data.  To do that,
                        // we'll need to unzip it and parse the XML.  We'll chain
                        // some streams together here to get things started.
                        BufferedInputStream in = new BufferedInputStream(
                                new GZIPInputStream(new ByteArrayInputStream(plaintext)));
                        // Create a handler to do the grunt work of dealing with
                        // the XML.  While the parser tokenizes things for us,
                        // it doesn't do any logic with the data.
                        XMLHandler xmlHandler = new XMLHandler(mHandler);
                        // Set up a SAX parser and feed it both the unzipped data
                        // and the handler.  It will internally build the list of
                        // site parameters if successful.
                        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
                        parser.parse(in, xmlHandler);
                        // Now try to get the site parameters from the handler
                        // and close the streams:
                        importedSites = xmlHandler.getSites();
                        in.close();
                        // At this point, we shouldn't need the plaintext array
                        // anymore either, nor the XML handler:
                        plaintext = null;
                        xmlHandler = null;
                        in = null;
                        // If we got any useful data, we'll proceed from here:
                        if (importedSites != null && importedSites.length > 0) {
                            // If we get to here, everything must have gone A-OK.
                            // Explicitly send a 100% complete here to close out
                            // the progress dialog.  (I originally left this out,
                            // which resulted in the program hanging on the dialog
                            // and no way to close it.  Oops.)
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done", 100);
                            //b.putInt("site_count", sites.length);
                            b.putInt("site_count", importedSites.length);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                            // If we couldn't get any useful sites from the file,
                            // complain:
                        } else {
                            msg = mHandler.obtainMessage();
                            b = new Bundle();
                            b.putInt("percent_done", -1000);
                            b.putInt("site_count", 0);
                            msg.setData(b);
                            mHandler.sendMessage(msg);
                        }
                        // The file didn't exist, wasn't a file, couldn't be read, or
                        // was too long to read:
                    } else {
                        msg = mHandler.obtainMessage();
                        b = new Bundle();
                        b.putInt("percent_done", -1000);
                        b.putInt("site_count", 0);
                        msg.setData(b);
                        mHandler.sendMessage(msg);
                    }
                    // Insufficient memory to decrypt:
                } else {
                    msg = mHandler.obtainMessage();
                    b = new Bundle();
                    b.putInt("percent_done", -5);
                    b.putInt("site_count", 0);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                }
            }
            // Any number of things may have occurred to make the above
            // explode.  We should probably test for every option, but for
            // now we'll just assume the file wasn't valid and let it go.
            // Send a message back to the handler effectively saying as such:
            catch (Exception e) {
                msg = mHandler.obtainMessage();
                b = new Bundle();
                b.putInt("percent_done", -1000);
                b.putInt("site_count", 0);
                msg.setData(b);
                mHandler.sendMessage(msg);
            }
        }
    }

}