in.neoandroid.neoupdate.neoUpdate.java Source code

Java tutorial

Introduction

Here is the source code for in.neoandroid.neoupdate.neoUpdate.java

Source

package in.neoandroid.neoupdate;

/*
neoUpdate Android SDK: neoUpdate.java
    
Copyright (C) 2013-2014 Neophyte Technologies LLP & Respective Contributors
See contributors.txt for complete list of contributors.
    
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 3 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 the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.DigestInputStream;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.GZIPInputStream;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONObject;

import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.Settings;
import android.util.Base64;
import android.util.Log;
import android.webkit.MimeTypeMap;
import static android.provider.BaseColumns._ID;

public class neoUpdate extends AsyncTask<Void, Float, String> {
    private final double neoUpdateVersion = 1.0f;
    private final static boolean enableDebug = true;
    private final static String TAG = "[neoUpdate]";
    private final String serverUrl = "https://neoupdate-in.appspot.com";
    private String baseUrl;
    private int nConnections;
    /**< if 0 -> assumes local filesystem */
    private ArrayList<NewAsset> filesToDownload;
    private String tmpDir;
    private Boolean stopped;
    private String appToken;
    private String appSecret;
    private int totalFilesToDownload;
    private String deviceID;
    private String serialNo;
    private String macAddress;
    private Context context;
    private PackageInfo packageInfo;
    private NewAsset apkUpdatePath;
    private neoUpdateDB db;
    private ReentrantLock lock;
    private Boolean fromOfflineStorage = false;
    private Boolean fromNPKStorage = false;

    // File list
    private final String metafile = "/neoupdate.json";

    /**
     * file:// implies local filesystem
     * @param baseUrl
     * @param tmpDir
     * @param nSimultaneousConnections
     */
    public neoUpdate(Context c, String baseUrl, String tmpDir, String appToken, String appSecret,
            int nSimultaneousConnections) {
        this.baseUrl = baseUrl;
        this.tmpDir = tmpDir;
        this.appToken = appToken;
        this.appSecret = appSecret;
        nConnections = nSimultaneousConnections;
        context = c;
        if (nConnections <= 0)
            nConnections = 1;
        // Check for local filesystem
        if (baseUrl.startsWith("file:///") || appToken == null || appSecret == null) {
            nConnections = 0;
            this.baseUrl = baseUrl.replace("file:///", "/");
            fromOfflineStorage = true;
            if (baseUrl.endsWith(".npk"))
                fromNPKStorage = true;
        } else {
            this.baseUrl = serverUrl + baseUrl;
            if (this.baseUrl.endsWith("/"))
                this.baseUrl = this.baseUrl.substring(0, this.baseUrl.length() - 1);
            lock = new ReentrantLock();
        }
        filesToDownload = new ArrayList<NewAsset>();
        deviceID = Settings.Secure.getString(c.getContentResolver(), Settings.Secure.ANDROID_ID);
        serialNo = neoUpdate.getSerialNo();
        macAddress = getWifiMac(c);
        if (deviceID == null)
            deviceID = "";
        if (serialNo == null)
            serialNo = "";
        if (macAddress == null)
            macAddress = "";
        try {
            packageInfo = c.getPackageManager().getPackageInfo(c.getPackageName(), 0);
        } catch (Exception e) {
        }
        db = new neoUpdateDB(c);
        stopped = false;
        totalFilesToDownload = 0;
        if (enableDebug) {
            Log.d(TAG, "DeviceID: " + deviceID);
            Log.d(TAG, "serialNo: " + serialNo);
            Log.d(TAG, "MAC Address: " + macAddress);
            Log.d(TAG, "SDCard: " + Environment.getExternalStorageDirectory().getAbsolutePath());
            Log.d(TAG, "Data: " + Environment.getDataDirectory().getAbsolutePath());
        }
    }

    private String getMetaFromNPK() {
        try {
            GZIPInputStream npkFile = new GZIPInputStream(new FileInputStream(baseUrl));
            //FileInputStream npkFile = new FileInputStream(baseUrl);
            TarArchiveInputStream input = new TarArchiveInputStream(npkFile);
            TarArchiveEntry ae;
            while ((ae = input.getNextTarEntry()) != null) {
                if (ae.isDirectory())
                    Log.e("[neoUpdate]", "Dir: " + ae.getName());
                else
                    Log.e("[neoUpdate]", "File: " + ae.getName());
                if (ae.getName().equalsIgnoreCase("neoupdate.json")) {
                    byte buff[] = new byte[(int) ae.getSize()];
                    input.read(buff);
                    input.close();
                    return new String(buff);
                }
            }
            input.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private void startUpdateApk(Uri installerUri) {
        MimeTypeMap myMime = MimeTypeMap.getSingleton();
        String mimeType = myMime.getMimeTypeFromExtension("apk");
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(installerUri, mimeType);//"application/vnd.android.package-archive");
        context.startActivity(intent);
    }

    public static String getDeviceID(Context c) {
        return Settings.Secure.getString(c.getContentResolver(), Settings.Secure.ANDROID_ID);
    }

    public static String getWifiMac(Context c) {
        try {
            WifiManager wifiMgr = (WifiManager) c.getSystemService(Context.WIFI_SERVICE);
            return wifiMgr.getConnectionInfo().getMacAddress();
        } catch (Exception e) {
        }
        return null;
    }

    private boolean checkSignature(String jsonContent, String sign) {
        Log.d(TAG, "JSON: " + jsonContent);

        if (sign == null)
            return false;
        final String publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq+6EG/fAE+zIdh5Wzqnf"
                + "Fo4nCf7t7eJcKyvk1lqX1MdkIi/fUs8HQ4aQ4jWLCO4M1Gkz1FQiXOnheGLV5MXY"
                + "c9GyaglsofvpA/pU5d16FybX2pCevbTzcm39eU+XlwQWOr8gh23tYD8G6uMX6sIJ"
                + "W+1k1FWdud9errMVm0YUScI+J4AV5xzN0IQ29h9IeNp6oFqZ2ByWog6OBMTUDFIW"
                + "q8oRvH0OuPv3zFR5rKwsbTYb5Da8lhUht04dLBA860Y4zeUu98huvS9jQPu2N4ns"
                + "Hf425FfDJ/wae+7eLdQo7uFb+Wvc+PO9U39e6vXQfa8ZkUoXHD0XZN4jsFcKYuJw" + "OwIDAQAB";
        try {
            byte keyBytes[] = Base64.decode(publicKeyStr.getBytes(), Base64.NO_WRAP);

            X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory kf = KeyFactory.getInstance("RSA");
            PublicKey publicKey = kf.generatePublic(publicSpec);

            Signature signer = Signature.getInstance("SHA1withRSA");
            signer.initVerify(publicKey);
            signer.update(jsonContent.getBytes(), 0, jsonContent.length());

            return signer.verify(Base64.decode(sign, Base64.NO_WRAP));
        } catch (Exception e) {
        }
        return false;
    }

    /* -- Works only with Android 2.3+ */
    public static String getSerialNo() {
        //return android.os.SystemProperties.get("ro.serialno", "unknown");
        try {
            Class<?> c = Class.forName("android.os.SystemProperties");
            Method get = c.getMethod("get", String.class, String.class);
            return (String) (get.invoke(c, "ro.serialno", ""));
        } catch (Exception e) {
        }
        return "";
    }

    /**
     * A Simpler HTTPConnection helper.
     * Used currently.
     */
    private static HttpURLConnection getHTTPConnection(String url) throws MalformedURLException, IOException {
        HttpURLConnection c;
        c = (HttpURLConnection) new URL(url).openConnection();
        c.setUseCaches(false);
        c.connect();
        return c;
    }

    public HttpResponse HttpWithPostData(String api, long fromBytes) {
        String url = baseUrl + api;
        try {
            url = baseUrl + URLEncoder.encode(api, "UTF-8");
        } catch (Exception e) {
            if (enableDebug)
                e.printStackTrace();
            url = baseUrl + api;
        }
        // Create a new HttpClient and Post Header
        HttpClient httpclient = new DefaultHttpClient();
        HttpPost httppost = new HttpPost(url);
        Log.d(TAG, "HTTP Fetch: " + url + " with Resume: " + fromBytes);

        if (appToken == null || appSecret == null || appToken.length() == 0 || appSecret.length() == 0)
            return null;
        if (deviceID.length() == 0 && serialNo.length() == 0 && macAddress.length() == 0)
            return null;

        try {
            // For resuming downloads
            if (fromBytes > 0)
                httppost.addHeader(new BasicHeader("Range", "bytes=" + fromBytes + "-"));
            // Add post data
            ArrayList<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
            nameValuePairs.add(new BasicNameValuePair("APP_TOKEN", appToken));
            nameValuePairs.add(new BasicNameValuePair("APP_SECRET", appSecret));
            if (deviceID != null)
                nameValuePairs.add(new BasicNameValuePair("DEVICE_ID", deviceID));
            if (serialNo != null)
                nameValuePairs.add(new BasicNameValuePair("DEVICE_SERIAL", serialNo));
            if (macAddress != null)
                nameValuePairs.add(new BasicNameValuePair("DEVICE_MAC", macAddress));
            httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

            // Execute HTTP Post Request
            return httpclient.execute(httppost);
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String inputStreamToString(InputStream is) {
        String line = "";
        StringBuilder total = new StringBuilder();
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));

        try {
            while ((line = rd.readLine()) != null)
                total.append("\n" + line);
        } catch (Exception e) {
            e.printStackTrace();
        }
        ;

        return total.toString();
    }

    public int totalFilesToDownload() {
        return totalFilesToDownload;
    }

    public int filesToDownload() {
        return filesToDownload.size();
    }

    /**
     * Stops the update procedure.
     * @return Success/failure
     */
    public Boolean stopUpdate() {
        stopped = true;
        return true;
    }

    public static Boolean isDevicePresent(Context c, String device) {
        neoUpdateDB db = new neoUpdateDB(c);
        return db.isDevicePresent(device);
    }

    private InputStream getLocalFile(String path) throws FileNotFoundException {
        FileInputStream file = new FileInputStream(path);
        return file;
    }

    private JSONObject downloadMetafile() {
        // TODO: Check for resuming download (?) - Not necessary for metafiles ?
        try {
            String str = null;
            if (nConnections == 0 || fromOfflineStorage) {
                if (fromNPKStorage)
                    str = getMetaFromNPK();
                else
                    str = inputStreamToString(getLocalFile(baseUrl + metafile));
            } else {
                HttpResponse res = HttpWithPostData(metafile, 0);
                if (res != null)
                    str = inputStreamToString(res.getEntity().getContent());
            }
            if (str == null)
                return null;
            String sign = str.substring(str.lastIndexOf('\n')).trim();
            str = str.substring(0, str.lastIndexOf('\n')).trim();
            if (!checkSignature(str, sign)) {
                Log.e(TAG, "Signature Verification failed!");
                return null;
            }
            return new JSONObject(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private Boolean parseMetafile(JSONObject metafile) {
        double version;
        boolean forceVersion;
        boolean allowed = false;
        try {
            version = metafile.getDouble("version");
            forceVersion = metafile.getBoolean("forceVersion");
            JSONObject appDetails = metafile.getJSONObject("app");
            JSONArray assets = metafile.getJSONArray("assets");
            JSONArray devices = metafile.getJSONArray("allowedDevices");

            int nAssets = assets.length();
            String packageName = appDetails.getString("packageName");
            int versionCode = appDetails.getInt("versionCode");
            String apkPath = appDetails.getString("APK");
            boolean offlineSupport = appDetails.getBoolean("offlineSupport");

            if (enableDebug) {
                Log.d(TAG, "Version: " + version + ":" + neoUpdateVersion);
                Log.d(TAG, "Package Name: " + packageName + ":" + packageInfo.packageName);
                Log.d(TAG, "APK Path: " + apkPath);
            }

            // Check if it is being updated using offline storage
            if (!offlineSupport && fromOfflineStorage) {
                Log.e(TAG, "Updating from offline storage is disabled for this app?");
                return false;
            }

            db.clearDevicesList();
            for (int i = 0; i < devices.length(); i++) {
                String device = devices.getString(i);
                if (device.length() > 0 && deviceID.compareToIgnoreCase(device) == 0)
                    allowed = true;
                db.insertDevice(device);
                if (enableDebug)
                    Log.d(TAG, "Device Allowed: " + device);
            }

            // DeviceID or signature error
            if (!allowed)
                return false;

            apkUpdatePath = null;
            if (version > neoUpdateVersion && forceVersion) {
                Log.e(TAG, "neoUpdate seems to be of older version! Required: " + version + " Current: "
                        + neoUpdateVersion);
                return false;
            }

            if (packageInfo.packageName.compareTo(packageName) != 0) {
                Log.e(TAG, "PackageNames don't seem to match - url for some other app? Provided: " + packageName);
                return false;
            }

            if (packageInfo.versionCode < versionCode) {
                // APK Update Required - Lets first do that
                apkUpdatePath = new NewAsset();
                apkUpdatePath.path = apkPath;
                apkUpdatePath.md5 = appDetails.getString("md5");
                return true;
            }

            // Parse the assets
            for (int i = 0; i < nAssets; i++) {
                JSONObject obj = assets.getJSONObject(i);
                NewAsset asset = new NewAsset();
                asset.path = obj.getString("path");
                asset.md5 = obj.getString("md5");

                // Ignore already downloaded files
                if (db.updateAndGetStatus(asset.path, asset.md5) == neoUpdateDB.UPDATE_STATUS.UPDATE_COMPLETE)
                    continue;
                filesToDownload.add(asset);
                if (enableDebug) {
                    Log.d(TAG, "Enqueued: " + asset.path + " With MD5: " + asset.md5);
                }
            }
            totalFilesToDownload = filesToDownload.size();
        } catch (Exception e) {
            if (enableDebug)
                e.printStackTrace();
            return false;
        }
        return true;
    }

    private String mapPath(String path) {
        String ret = path;
        if (path.startsWith("/sdcard/"))
            path.replaceFirst("/sdcard/", Environment.getExternalStorageDirectory().getAbsolutePath() + "/");
        if (path.startsWith("/data/"))
            path.replaceFirst("/data/", Environment.getDataDirectory().getAbsolutePath() + "/");
        return ret;
    }

    private void createSubDirectories(String path) {
        try {
            File newDir = new File(path.substring(0, path.lastIndexOf('/')));
            newDir.mkdirs();
        } catch (Exception e) {
            if (enableDebug)
                e.printStackTrace();
        }
    }

    private boolean downloadFile(NewAsset asset, String toPath) {
        return downloadFile(asset, toPath, null, null);
    }

    private boolean downloadFile(NewAsset asset, String toPath, TarArchiveInputStream tin, TarArchiveEntry ae) {
        if (enableDebug)
            Log.d(TAG, "Start download: " + asset.path + ":NPK: " + (tin != null));
        boolean resume = (db.updateAndGetStatus(asset.path, asset.md5) == neoUpdateDB.UPDATE_STATUS.UPDATE_RESUME);
        String newPath;
        if (toPath != null)
            newPath = toPath;
        else
            newPath = mapPath(asset.path);
        createSubDirectories(newPath);
        File newFile = new File(newPath);
        long fromBytes = 0;
        if (resume)
            fromBytes = newFile.length();

        try {
            FileOutputStream os = new FileOutputStream(newFile, resume);
            db.setMd5(asset.path, asset.md5);

            if (tin != null && ae != null) {
                // Via NPK
                final int BUFF_SIZE = (8 * 1024); // Buffer size of 8KB
                byte[] buffer = new byte[BUFF_SIZE];
                int n = 0;
                long size = ae.getSize();
                if (resume && fromBytes > 0 && fromBytes < size) {
                    tin.skip(fromBytes);
                    size -= fromBytes;
                }
                while (size > 0) {
                    n = BUFF_SIZE;
                    if (n > size)
                        n = (int) size;
                    n = tin.read(buffer, 0, n);
                    if (n < 0)
                        break;
                    if (n > 0)
                        os.write(buffer, 0, n);
                }
            } else if (nConnections <= 0) {
                // Via Local File System
                FileInputStream is = new FileInputStream(baseUrl + asset.path);
                is.getChannel().transferTo(fromBytes, is.getChannel().size() - fromBytes, os.getChannel());
                is.close();
            } else {
                // Via Internet
                HttpResponse resp = HttpWithPostData(asset.path, fromBytes);
                resp.getEntity().writeTo(os);
            }
            db.setDownloaded(asset.path, true);
            os.close();
        } catch (Exception e) {
            if (enableDebug)
                e.printStackTrace();
            return false;
        }
        return true;
    }

    private String processFromLocalStorage() {
        while (filesToDownload.size() > 0 && !stopped) {
            NewAsset asset = filesToDownload.remove(0);
            if (!downloadFile(asset, null)) {
                Log.e(TAG, "File download failed!");
                return "Unable to find the required file: " + asset.path;
            }
            publishProgress((float) (totalFilesToDownload - filesToDownload.size()) / (float) totalFilesToDownload);
        }
        return "Success";
    }

    private NewAsset findAndGetAsset(String path) {
        for (NewAsset asset : filesToDownload) {
            if (asset.path.equalsIgnoreCase(path)) {
                filesToDownload.remove(asset);
                return asset;
            }
        }
        return null;
    }

    private String processFromNPK() {
        try {
            GZIPInputStream npkFile = new GZIPInputStream(new FileInputStream(baseUrl));
            //FileInputStream npkFile = new FileInputStream(baseUrl);
            TarArchiveInputStream input = new TarArchiveInputStream(npkFile);
            TarArchiveEntry ae;
            while ((ae = input.getNextTarEntry()) != null && filesToDownload.size() > 0 && !stopped) {
                if (ae.isDirectory()) {
                    Log.e("[neoUpdate]", "Dir: " + ae.getName());
                } else {
                    Log.e("[neoUpdate]", "File: " + ae.getName());
                    String filename = ae.getName();
                    NewAsset asset = findAndGetAsset(filename);
                    if (asset != null) {
                        downloadFile(asset, null, input, ae);
                        publishProgress((float) (totalFilesToDownload - filesToDownload.size())
                                / (float) totalFilesToDownload);
                    }
                }
            }
            input.close();
        } catch (Exception e) {
            e.printStackTrace();
            return "Unknown Error: Update Failed!";
        }
        return "Success";
    }

    private boolean downloadAPKFromNPK() {
        try {
            String apkName = apkUpdatePath.path.replace("/", "");
            GZIPInputStream npkFile = new GZIPInputStream(new FileInputStream(baseUrl));
            //FileInputStream npkFile = new FileInputStream(baseUrl);
            TarArchiveInputStream input = new TarArchiveInputStream(npkFile);
            TarArchiveEntry ae;
            while ((ae = input.getNextTarEntry()) != null) {
                if (!ae.isDirectory() && ae.getName().equalsIgnoreCase(apkName)) {
                    String apkPath = tmpDir + apkUpdatePath.path;
                    boolean status = downloadFile(apkUpdatePath, apkPath, input, ae);
                    input.close();
                    return status;
                }
            }
            input.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    @Override
    protected String doInBackground(Void... params) {
        JSONObject metafile = downloadMetafile();
        if (metafile == null)
            return "Could not download meta data. Please check the connection";
        if (!parseMetafile(metafile))
            return "Failed to authenticate the device or Failed to parse metadata.";

        if (apkUpdatePath != null) {
            String apkPath = "";
            if (fromNPKStorage) {
                apkPath = tmpDir + apkUpdatePath.path.replace("/", "");
                if (!downloadAPKFromNPK())
                    return "Unable to save APK!";
            } else if (nConnections <= 0) {
                apkPath = baseUrl + apkUpdatePath.path;
            } else {
                apkPath = tmpDir + apkUpdatePath.path.substring(apkUpdatePath.path.lastIndexOf('/'));
                if (!downloadFile(apkUpdatePath, apkPath)) {
                    return "Unable to download APK!";
                }
            }
            startUpdateApk(Uri.parse("file:///" + apkPath));
            return "APK Updated. Please re-run update after restarting the app to continue update";
        }
        if (nConnections > 0) {
            long waitTime = 1000 / nConnections;
            if (waitTime <= 0)
                waitTime = 1;
            Thread threads[] = new Thread[nConnections];
            int i;
            for (i = 0; i < nConnections; i++) {
                threads[i] = new Thread(new DownloadRunnable());
                threads[i].start();
            }
            while (!stopped) {
                if (totalFilesToDownload > 0) {
                    float nComplete = (totalFilesToDownload - filesToDownload.size() - nConnections);
                    if (nComplete >= 0)
                        publishProgress(nComplete / (float) totalFilesToDownload);
                }
                boolean inprogress = false;
                for (i = 0; i < nConnections; i++) {
                    try {
                        threads[i].join(waitTime);
                    } catch (Exception e) {
                    }
                    inprogress = inprogress || threads[i].isAlive();
                }
                if (!inprogress)
                    break;
                if (stopped && filesToDownload.size() > 0) {
                    lock.lock();
                    filesToDownload.clear();
                    lock.unlock();
                }
            }
        } else {
            if (fromNPKStorage)
                return processFromNPK();
            return processFromLocalStorage();
        }

        return "Success";
    }

    private static class neoUpdateDB extends SQLiteOpenHelper {
        private final static String DB_NAME = "neoUpdateDB";
        private final static int DB_VERSION = 1;
        private final static String TABLE_NAME = "neoUpdate";
        private final static String COL_PATH = "PATH";
        private final static String COL_MD5 = "MD5";
        private final static String COL_DOWNLOADED = "DOWNLOADED";
        private final static String DEVICES_TABLE = "neoDevices";
        private final static String COL_DEVICES = "DEVICES";

        private SQLiteDatabase neoDB;

        public static enum UPDATE_STATUS {
            UPDATE_REQUIRED, UPDATE_RESUME, UPDATE_COMPLETE
        };

        public neoUpdateDB(Context context) {
            super(context, DB_NAME, null, DB_VERSION);
            neoDB = getWritableDatabase();
        }

        public void insertDevice(String device) {
            ContentValues cv = new ContentValues();
            cv.put(COL_DEVICES, device);
            neoDB.insert(DEVICES_TABLE, null, cv);
        }

        public Boolean isDevicePresent(String device) {
            Cursor c = neoDB.query(DEVICES_TABLE, new String[] { COL_DEVICES }, COL_DEVICES + "=?",
                    new String[] { device }, null, null, null, "1");
            if (c != null && c.getCount() > 0) {
                c.close();
                return true;
            }
            return false;
        }

        /**
         * Call this after completion of the download (Or to pause and resume the download later)
         * @param path Path of the file
         * @param status True to set it as complete / False to resume later
         */
        public void setDownloaded(String path, boolean status) {
            ContentValues cv = new ContentValues();
            cv.put(COL_DOWNLOADED, status);
            neoDB.update(TABLE_NAME, cv, COL_PATH + "=?", new String[] { path });
        }

        /**
         * Call this to set the Md5, just after creating an empty file.
         * Note: Make sure not to call this before creating an empty file, 
         * otherwise it might cause the subsequent call to return UPDATE_RESUME wrongly.
         * 
         * @param path   Path of the file
         * @param md5 MD5 sum of the file
         */
        public void setMd5(String path, String md5) {
            ContentValues cv = new ContentValues();
            cv.put(COL_MD5, md5);
            neoDB.update(TABLE_NAME, cv, COL_PATH + "=?", new String[] { path });
        }

        private String getMd5Sum(File file) {
            try {
                byte[] buffer = new byte[8192];
                FileInputStream iStream = new FileInputStream(file);
                MessageDigest md = MessageDigest.getInstance("MD5");
                DigestInputStream dis = new DigestInputStream(iStream, md);
                while (dis.read(buffer) != -1) {
                }
                byte[] data = md.digest();
                StringBuffer md5 = new StringBuffer();
                for (int i = 0; i < data.length; i++) {
                    String hex = Integer.toHexString(0xFF & data[i]);
                    if (hex.length() == 1)
                        md5.append("0");
                    md5.append(hex);
                }
                dis.close();
                return new String(md5);
            } catch (Exception e) {
                if (enableDebug)
                    e.printStackTrace();
            }
            return "";
        }

        /**
         * 
         * @param path
         * @param md5
         * @return
         */
        public UPDATE_STATUS updateAndGetStatus(String path, String md5) {
            boolean latestFileAvailable = false;
            Cursor c = neoDB.query(TABLE_NAME, new String[] { COL_MD5, COL_DOWNLOADED }, COL_PATH + "=?",
                    new String[] { path }, null, null, null, "1");

            int iMd5 = c.getColumnIndex(COL_MD5);
            int iDownloaded = c.getColumnIndex(COL_DOWNLOADED);
            File file = new File(path);

            if (c != null && c.getCount() > 0) {
                c.moveToFirst();
                boolean downloaded = (c.getInt(iDownloaded) > 0);
                boolean md5Match = (md5.compareToIgnoreCase(c.getString(iMd5)) == 0);
                c.close();

                if (!md5Match || !file.exists()) {
                    if (downloaded)
                        setDownloaded(path, false);
                    return UPDATE_STATUS.UPDATE_REQUIRED;
                } else {
                    if (downloaded)
                        return UPDATE_STATUS.UPDATE_COMPLETE;
                    return UPDATE_STATUS.UPDATE_RESUME;
                }
            } else if (file.exists()) {
                // File exists check its MD5
                if (getMd5Sum(file).equalsIgnoreCase(md5))
                    latestFileAvailable = true;
            }

            // New path - insert this
            ContentValues cv = new ContentValues();
            cv.put(COL_PATH, path);
            if (latestFileAvailable) {
                // Latest file is available - but it is not present in the db
                cv.put(COL_MD5, md5);
                cv.put(COL_DOWNLOADED, true);
            } else {
                cv.put(COL_MD5, ""); // Make sure not to input correct md5 - otherwise it might cause
                // the subsequent call to return UPDATE_RESUME wrongly
                cv.put(COL_DOWNLOADED, false);
            }
            neoDB.insert(TABLE_NAME, null, cv);

            return latestFileAvailable ? UPDATE_STATUS.UPDATE_COMPLETE : UPDATE_STATUS.UPDATE_REQUIRED;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_PATH
                    + " TEXT NOT NULL, " + COL_MD5 + " TEXT NOT NULL, " + COL_DOWNLOADED + " INTEGER);");
            db.execSQL("CREATE TABLE " + DEVICES_TABLE + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                    + COL_DEVICES + " TEXT NOT NULL);");
        }

        public void clearDevicesList() {
            neoDB.execSQL("DELETE FROM " + DEVICES_TABLE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
            onCreate(db);
        }

        @Override
        public void finalize() {
            neoDB.close();
        }
    }

    private static class NewAsset {
        public String path;
        public String md5;
    }

    private class DownloadRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                NewAsset asset = null;
                lock.lock();
                try {
                    if (filesToDownload.size() > 0)
                        asset = filesToDownload.remove(0);
                } catch (Exception e) {
                    if (enableDebug)
                        e.printStackTrace();
                }
                lock.unlock();
                if (asset != null)
                    downloadFile(asset, null);
                else
                    return;
            }
        }
    }
}