<NME> Sync.java
<BEF> /*
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

package com.adobe.phonegap.contentsync;

import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

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

import android.app.Activity;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.os.StatFs;
import android.util.Log;
import android.util.Patterns;
import android.webkit.CookieManager;

public class Sync extends CordovaPlugin {
    private static final int STATUS_STOPPED = 0;
    private static final int STATUS_DOWNLOADING = 1;
    private static final int STATUS_EXTRACTING = 2;
    private static final int STATUS_COMPLETE = 3;

    public static final int INVALID_URL_ERROR = 1;
    public static final int CONNECTION_ERROR = 2;
    public static final int UNZIP_ERROR = 3;

    private static final String PROP_LOCAL_PATH = "localPath";
    private static final String PROP_STATUS = "status";
    private static final String PROP_PROGRESS = "progress";
    private static final String PROP_LOADED = "loaded";
    private static final String PROP_TOTAL = "total";
    private static final String PROP_CACHED = "cached";
    // Type
    private static final String TYPE_REPLACE = "replace";
    private static final String TYPE_MERGE = "merge";
    private static final String TYPE_LOCAL = "local";

    private static final String LOG_TAG = "ContentSync";
    public static final String PREVIOUS_VERSION = "PREVIOUS_VERSION";

    private static HashMap<String, ProgressEvent> activeRequests = new HashMap<String, ProgressEvent>();
    private static final int MAX_BUFFER_SIZE = 16 * 1024;

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (action.equals("sync")) {
            sync(args, callbackContext);
            this.target = target;
            this.callbackContext = callbackContext;
        }
        void sendPluginResult(PluginResult pluginResult) {
            synchronized (this) {
                if (!aborted) {
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    if (download(source, target, headers, createProgressEvent("download"), finalContext)) {
                        JSONObject retval = new JSONObject();
                        try {
                            retval.put("archiveURL", target.getAbsolutePath());
                        } catch (JSONException e) {
                            // never happens
                        }
                        finalContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, retval));
                    }
                }
            });
            return true;
        } else if (action.equals("unzip")) {
            String tempPath = args.getString(0);
            if (tempPath.startsWith("file://")) {
                tempPath = tempPath.substring(7);
            }

            return true;
        } else if (action.equals("cancel")) {
        	Log.d(LOG_TAG, "not implemented, yet");
        }
        return false;
    }
            ProgressEvent progress = activeRequests.get(args.getString(0));
            if (progress != null) {
                progress.setAborted(true);
            }
     * the HTTP Content-Length header value from the server.
     */
    private static abstract class TrackingInputStream extends FilterInputStream {
      public TrackingInputStream(final InputStream in) {
        super(in);
      }
        public abstract long getTotalRawBytesRead();
  }

    private static class ExposedGZIPInputStream extends GZIPInputStream {
      public ExposedGZIPInputStream(final InputStream in) throws IOException {
        super(in);
      }
      public Inflater getInflater() {
        return inf;
      }
  }

    /**
     * Provides raw bytes-read tracking for a GZIP input stream. Reports the
        public Inflater getInflater() {
     * number of uncompressed bytes.
     */
    private static class TrackingGZIPInputStream extends TrackingInputStream {
      private ExposedGZIPInputStream gzin;
      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
        super(gzin);
        this.gzin = gzin;
      }
      public long getTotalRawBytesRead() {
        return gzin.getInflater().getBytesRead();
      }
  }

    /**
     * Provides simple total-bytes-read tracking for an existing InputStream
        public long getTotalRawBytesRead() {
            return gzin.getInflater().getBytesRead();
        }
    }

    /**
     * Provides simple total-bytes-read tracking for an existing InputStream
     */
    private static class SimpleTrackingInputStream extends TrackingInputStream {
        private long bytesRead = 0;
        public SimpleTrackingInputStream(InputStream stream) {
            super(stream);
        }

        private int updateBytesRead(int newBytesRead) {
            if (newBytesRead != -1) {
                bytesRead += newBytesRead;
            }
            return newBytesRead;
        }

        @Override
        public int read() throws IOException {
            return updateBytesRead(super.read());
        }

        // Note: FilterInputStream delegates read(byte[] bytes) to the below method,
        // so we don't override it or else double count (CB-5631).
        @Override
        public int read(byte[] bytes, int offset, int count) throws IOException {
            return updateBytesRead(super.read(bytes, offset, count));
        }

        public long getTotalRawBytesRead() {
            return bytesRead;
        }
    }

    private boolean download(final String source, final File file, final JSONObject headers, final ProgressEvent progress, final CallbackContext callbackContext) {
        Log.d(LOG_TAG, "download " + source);

        if (!Patterns.WEB_URL.matcher(source).matches()) {
            sendErrorMessage("Invalid URL", INVALID_URL_ERROR, callbackContext);
            return false;
        }

        final CordovaResourceApi resourceApi = webView.getResourceApi();

        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                if (context.aborted) {
                    return;
                }
                HttpURLConnection connection = null;
                HostnameVerifier oldHostnameVerifier = null;
        }
        HttpURLConnection connection = null;
        PluginResult result = null;
        TrackingInputStream inputStream = null;
        boolean cached = false;
        boolean retval = true;

        OutputStream outputStream = null;
        try {
            OpenForReadResult readResult = null;
            final Uri targetUri = resourceApi.remapUri(Uri.fromFile(file));

            progress.setTargetFile(file);
            progress.setStatus(STATUS_DOWNLOADING);

            Log.d(LOG_TAG, "Download file: " + sourceUri);
            Log.d(LOG_TAG, "Target file: " + file);
            Log.d(LOG_TAG, "size = " + file.length());

            if (isLocalTransfer) {
                readResult = resourceApi.openForRead(sourceUri);
                if (readResult.length != -1) {
                    progress.setTotal(readResult.length);
                }
                inputStream = new SimpleTrackingInputStream(readResult.inputStream);
            } else {
                // connect to server
                // Open a HTTP connection to the URL based on protocol
                connection = resourceApi.createHttpConnection(sourceUri);
                connection.setRequestMethod("GET");

                // TODO: Make OkHttp use this CookieManager by default.
                String cookie = getCookies(sourceUri.toString());

                if(cookie != null)
                {
                    connection.setRequestProperty("cookie", cookie);
                }

                // This must be explicitly set for gzip progress tracking to work.
                connection.setRequestProperty("Accept-Encoding", "gzip");

                // Handle the other headers
                if (headers != null) {
                    addHeadersToRequest(connection, headers);
                }

                connection.connect();
                if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
                    cached = true;
                    connection.disconnect();
                    sendErrorMessage("Resource not modified: " + source, CONNECTION_ERROR, callbackContext, connection.getResponseCode());
                    retval = false;
                    return retval;
                } else {
                    if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
                        // Only trust content-length header if we understand
                        // the encoding -- identity or gzip
                        int connectionLength = connection.getContentLength();
                        if (connectionLength != -1) {
                            if (connectionLength > getFreeSpace()) {
                                cached = true;
                                connection.disconnect();
                        if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
                            cached = true;
                            connection.disconnect();
                            Log.d(LOG_TAG, "Resource not modified: " + source);
                            JSONObject error = new JSONObject();
                            result = new PluginResult(PluginResult.Status.ERROR, error);
                        } else {
                            if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
                                // Only trust content-length header if we understand
                    }
                    inputStream = getInputStream(connection);
                }
                                	if (connectionLength > getFreeSpace()) {
                                		cached = true;
                                		connection.disconnect();
                                        JSONObject error = new JSONObject();
                                        error.put("message", "Not enough free space to download");
                                        result = new PluginResult(PluginResult.Status.ERROR, error);
                                	} else {
                                        progress.setTotal(connectionLength);
                                	}
                            return retval;
                        }
                        //progress.connection = connection;
                    }

                    // write bytes to file
                    if (!cached) {
                        try {
                            synchronized (context) {
                                if (context.aborted) {
                                    return;
                                }
                                context.connection = connection;
                                return retval;
                            }
                        }
                        Log.d(LOG_TAG, "bytes read = " + bytesRead);
                            int bytesRead = 0;
                            outputStream = resourceApi.openOutputStream(targetUri);
                            while ((bytesRead = inputStream.read(buffer)) > 0) {
                                Log.d(LOG_TAG, "bytes read = " + bytesRead);
                                outputStream.write(buffer, 0, bytesRead);
                                // Send a progress event.
                    safeClose(outputStream);
                }
            }
                            }

                            unzip(context.targetFile, id, type, callbackContext);

                        } finally {
                            synchronized (context) {
                                context.connection = null;
            }
        }

                        }
                    }

                } catch (FileNotFoundException e) {
                    JSONObject error = new JSONObject();
                    Log.e(LOG_TAG, error.toString(), e);
                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
                    callbackContext.sendPluginResult(result);
                } catch (IOException e) {
                    JSONObject error = new JSONObject();
                    Log.e(LOG_TAG, error.toString(), e);
                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
                    callbackContext.sendPluginResult(result);
                } catch (JSONException e) {
                    Log.e(LOG_TAG, e.getMessage(), e);
                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
                    callbackContext.sendPluginResult(result);
                } catch (Throwable e) {
                    JSONObject error = new JSONObject();
                    Log.e(LOG_TAG, error.toString(), e);
                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
                    callbackContext.sendPluginResult(result);
                } finally {
//                    synchronized (activeRequests) {
//                        activeRequests.remove(id);
//                    }

                    if (connection != null) {
                        // Revert back to the proper verifier and socket factories
                        if (trustEveryone && useHttps) {
        JSONObject error = new JSONObject();
        try {
            error.put("type", type);
            error.put("responseCode", httpResponseCode);
                    }

                    // Remove incomplete download.
                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
                        file.delete();
                    }
                }
        long blockSize = stat.getBlockSize();
        });
    }

	private long getFreeSpace() {
		File path = Environment.getDataDirectory();
        StatFs stat = new StatFs(path.getPath());
        } else {
            headers = new JSONObject();
        }
        final boolean copyCordovaAssets;
        final boolean copyRootApp = args.getBoolean(5);
        if (copyRootApp) {
            copyCordovaAssets = true;
		Log.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());

		// unzip
		//String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();
		String outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
		outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;
		outputDirectory += id;
		Log.d(LOG_TAG, "output dir = " + outputDirectory);
         * content
         */
        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                webView.clearCache(true);
            }
				try {
					copyFolder(dir, backup);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {
                        return;
			}
		}

		if (unzipSync(targetFile, outputDirectory, callbackContext)) {
			// success, remove backup
			removeFolder(backup);
		} else {
			// failure, revert backup
			removeFolder(dir);
			backup.renameTo(dir);
		}

		// delete temp file
		targetFile.delete();

		// complete
		JSONObject result = new JSONObject();
		result.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));
        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
	}

	private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
                if (!type.equals(TYPE_LOCAL)) {
                    // download file
                    if (download(src, createDownloadFileLocation(id), headers, progress, callbackContext)) {
                        // update progress with zip file
                        File targetFile = progress.getTargetFile();
                        Log.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());

                        // Backup existing directory
                        File backup = backupExistingDirectory(outputDirectory, type, dir);

                        // @TODO: Do we do this even when type is local?
                        if (copyRootApp) {
                            copyRootApp(outputDirectory, manifestFile);
                        }

                        boolean win = false;
                        if (isZipFile(targetFile)) {
                            win = unzipSync(targetFile, outputDirectory, progress, callbackContext);
                        } else {
                            // copy file to ID
                            win = targetFile.renameTo(new File(outputDirectory));
                            progress.setLoaded(1);
                            progress.setTotal(1);
                            progress.setStatus(STATUS_EXTRACTING);
                            progress.updatePercentage();
                        }

                        // delete temp file
                        targetFile.delete();

                        if (copyCordovaAssets) {
                            copyCordovaAssets(outputDirectory);
                        }

                        if (win) {
                            // success, remove backup
                            removeFolder(backup);
                        } else {
                            // failure, revert backup
                            removeFolder(dir);
                            backup.renameTo(dir);
                        }
                    } else {
                        return;
                    }
                }

                // complete
                synchronized (activeRequests) {
                    activeRequests.remove(id);
                }

                // Send last progress event
                progress.setStatus(STATUS_COMPLETE);
                updateProgress(callbackContext, progress);

                // Send completion message
                try {
                    JSONObject result = new JSONObject();
                    result.put(PROP_LOCAL_PATH, outputDirectory);

                    if (dir.list() != null) {
                        Log.d(LOG_TAG, "size of output dir = " + dir.list().length);
                    }
                    boolean cached = false;
                    if (type.equals(TYPE_LOCAL) && dir.exists() && dir.isDirectory() && dir.list() != null && dir.list().length > 0) {
                        Log.d(LOG_TAG, "we have a dir with some files in it.");
                        cached = true;
                    }

                    result.put(PROP_CACHED, cached);
                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
                } catch (JSONException e) {
                    // never happens
                }
            }
        });
    }

    private void savePrefs() {
        SharedPreferences.Editor editor = cordova.getActivity().getSharedPreferences(cordova.getActivity().getPackageName(), 0).edit();
        editor.putInt(PREVIOUS_VERSION, getCurrentAppVersion());
        editor.commit();
    }

    private boolean hasAppBeenUpdated() {
        Activity activity = cordova.getActivity();

        int currentAppVersion = -1;

        SharedPreferences settings = activity.getSharedPreferences(activity.getPackageName(), 0);
        int previousAppVersion = settings.getInt(PREVIOUS_VERSION, -1);

        currentAppVersion = getCurrentAppVersion();

        Log.d(LOG_TAG, "current = " + currentAppVersion);
        Log.d(LOG_TAG, "previous = " + previousAppVersion);

        return currentAppVersion > previousAppVersion ? true : false;
    }

    private int getCurrentAppVersion() {
        Activity activity = cordova.getActivity();
        int currentAppVersion = -1;
        try {
            currentAppVersion = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            // ignore
        }
        return currentAppVersion;
    }

    private boolean isZipFile(File targetFile) {
        boolean success = true;
        try {
            ZipFile zip = new ZipFile(targetFile);
            Log.d(LOG_TAG, "seems like a zip file");
        return a | b << 8 | c << 16 | d << 24;
    }

    private boolean unzipSync(File targetFile, String outputDirectory, CallbackContext callbackContext) {
    	Log.d(LOG_TAG, "unzipSync called");
    	Log.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());
        InputStream inputStream = null;
        ZipFile zip = null;
        boolean anyEntries = false;
        try {
        	zip = new ZipFile(targetFile);

            // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://
            // Accept a path or a URI for the source zip.
    private File createDownloadFileLocation(final String id) {
        File file = null;
        try {
            String tempId = (id.lastIndexOf("/") > -1) ? id.substring(id.lastIndexOf("/")+1, id.length()) : id;

            File tempFile = resourceApi.mapUriToFile(zipUri);
            if (tempFile == null || !tempFile.exists()) {
                String errorMessage = "Zip file does not exist";
                callbackContext.error(errorMessage);
                Log.e(LOG_TAG, errorMessage);
                return false;
            }

    private File backupExistingDirectory(String outputDirectory, String type, File dir) {
            outputDirectory = outputDir.getAbsolutePath();
            outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;
            if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){
                String errorMessage = "Could not create output directory";
                callbackContext.error(errorMessage);
                Log.e(LOG_TAG, errorMessage);
                return false;
            }

                }
            } else {
                dir.renameTo(backup);
            }
        }
        return backup;
    }

    private void copyRootApp(String outputDirectory, String manifestFile) {
        boolean wwwExists = (new File(outputDirectory, "www")).exists();
        boolean copied = false;
        if (manifestFile != null && !"".equals(manifestFile)) {
            Log.d(LOG_TAG, "Manifest copy");
            try {
                copyRootAppByManifest(outputDirectory, manifestFile, wwwExists);
                copied = true;
            } catch (Exception e) {
                Log.e(LOG_TAG, e.getLocalizedMessage(), e);
            }
        }
        if (!copied) {
            Log.d(LOG_TAG, "Long copy");
            try {
                copyAssetFileOrDir(outputDirectory, "www", wwwExists);
            } catch (IOException e) {
                Log.e(LOG_TAG, e.getLocalizedMessage(), e);
            }
        }
    }

    private void copyRootAppByManifest(String outputDirectory, String manifestFile, boolean wwwExists) throws IOException, JSONException {
        File fp = new File(outputDirectory);
            ZipEntry ze;
            byte[] buffer = new byte[32 * 1024];

            while ((ze = zis.getNextEntry()) != null)
            {
                anyEntries = true;
                String compressedName = ze.getName();

        }
    }

    private void copyCordovaAssets(String outputDirectory) {
        try {
            boolean wwwExists = (new File(outputDirectory, "www")).exists();

            // cordova.js
            this.copyAssetFile(outputDirectory, "www/cordova.js", wwwExists);

            // cordova_plugins.js
            this.copyAssetFile(outputDirectory, "www/cordova_plugins.js", wwwExists);

            // plugins folder
            this.copyAssetFileOrDir(outputDirectory, "www/plugins", wwwExists);
        } catch(IOException e) {
            Log.e(LOG_TAG, "Failed to copy asset file", e);
        }
    }

    private void copyAssetFileOrDir(String outputDirectory, String path, boolean wwwExists) throws IOException {
        if (path.contains(".")) {
            try {
                updateProgress(callbackContext, progress);
                zis.closeEntry();
            }

            // final progress = 100%
//            progress.setLoaded(progress.getTotal());
//            updateProgress(callbackContext, progress);
        } catch (Exception e) {
            String errorMessage = "An error occurred while unzipping.";
            callbackContext.error(errorMessage);
            Log.e(LOG_TAG, errorMessage, e);
        } finally {
            if (inputStream != null) {
    private void copyAssetDir(String outputDirectory, String path, boolean wwwExists) throws IOException {
        String assets[] = cordova.getActivity().getAssets().list(path);
        if (assets.length != 0) {
            for (String file : assets) {
                copyAssetFileOrDir(outputDirectory, path + File.separator + file, wwwExists);
            }
        } else {
            this.copyAssetFile(outputDirectory, path, wwwExists);
        }
    }

    private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
        String encoding = conn.getContentEncoding();
        if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
            return new TrackingGZIPInputStream(new ExposedGZIPInputStream(conn.getInputStream()));
        }
        return new SimpleTrackingInputStream(conn.getInputStream());
    }

    private static void safeClose(Closeable stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
            }
        }
    }

    private String getCookies(final String target) {
        boolean gotCookie = false;
        String cookie = null;
        Class webViewClass = webView.getClass();
        try {
            Method gcmMethod = webViewClass.getMethod("getCookieManager");
            Class iccmClass  = gcmMethod.getReturnType();
            Method gcMethod  = iccmClass.getMethod("getCookie", String.class);

            cookie = (String)gcMethod.invoke(
                    iccmClass.cast(
                            gcmMethod.invoke(webView)
                    ), target);

            gotCookie = true;
        } catch (NoSuchMethodException e) {
        } catch (IllegalAccessException e) {
        } catch (InvocationTargetException e) {
        } catch (ClassCastException e) {
        }

        if (!gotCookie) {
            cookie = CookieManager.getInstance().getCookie(target);
        }

        return cookie;
    }

    private static void addHeadersToRequest(URLConnection connection, JSONObject headers) {
        try {
            for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {
                String headerKey = iter.next().toString();
                JSONArray headerValues = headers.optJSONArray(headerKey);
                if (headerValues == null) {
                    headerValues = new JSONArray();
                    headerValues.put(headers.getString(headerKey));
                }
                connection.setRequestProperty(headerKey, headerValues.getString(0));
                for (int i = 1; i < headerValues.length(); ++i) {
                    connection.addRequestProperty(headerKey, headerValues.getString(i));
                }
            }
        } catch (JSONException e1) {
            // No headers to be manipulated!
        }
    }

    /* Unzip code */

    // Can't use DataInputStream because it has the wrong endian-ness.
    private static int readInt(InputStream is) throws IOException {
        int a = is.read();
        int b = is.read();
        int c = is.read();
        int d = is.read();
        return a | b << 8 | c << 16 | d << 24;
    }

    private boolean unzipSync(File targetFile, String outputDirectory, ProgressEvent progress, CallbackContext callbackContext) {
        Log.d(LOG_TAG, "unzipSync called");
        Log.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());
        InputStream inputStream = null;
        ZipFile zip = null;
        boolean anyEntries = false;
        try {
            synchronized (progress) {
                if (progress.isAborted()) {
                    return false;
                }
            }

            zip = new ZipFile(targetFile);

            // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://
            // Accept a path or a URI for the source zip.
            Uri zipUri = getUriForArg(targetFile.getAbsolutePath());
            Uri outputUri = getUriForArg(outputDirectory);

            CordovaResourceApi resourceApi = webView.getResourceApi();

            File tempFile = resourceApi.mapUriToFile(zipUri);
            if (tempFile == null || !tempFile.exists()) {
                sendErrorMessage("Zip file does not exist", UNZIP_ERROR, callbackContext);
            }

            File outputDir = resourceApi.mapUriToFile(outputUri);
            outputDirectory = outputDir.getAbsolutePath();
            outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;
            if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){
                sendErrorMessage("Could not create output directory", UNZIP_ERROR, callbackContext);
            }

            OpenForReadResult zipFile = resourceApi.openForRead(zipUri);
            progress.setStatus(STATUS_EXTRACTING);
            progress.setLoaded(0);
            progress.setTotal(zip.size());
            Log.d(LOG_TAG, "zip file len = " + zip.size());

            inputStream = new BufferedInputStream(zipFile.inputStream);
            inputStream.mark(10);
            int magic = readInt(inputStream);

            if (magic != 875721283) { // CRX identifier
                inputStream.reset();
            } else {
                // CRX files contain a header. This header consists of:
                //  * 4 bytes of magic number
                //  * 4 bytes of CRX format version,
                //  * 4 bytes of public key length
                //  * 4 bytes of signature length
                //  * the public key
                //  * the signature
                // and then the ordinary zip data follows. We skip over the header before creating the ZipInputStream.
                readInt(inputStream); // version == 2.
                int pubkeyLength = readInt(inputStream);
                int signatureLength = readInt(inputStream);

                inputStream.skip(pubkeyLength + signatureLength);
            }

            // The inputstream is now pointing at the start of the actual zip file content.
            ZipInputStream zis = new ZipInputStream(inputStream);
            inputStream = zis;

            ZipEntry ze;
            byte[] buffer = new byte[32 * 1024];

            while ((ze = zis.getNextEntry()) != null) {
                synchronized (progress) {
                    if (progress.isAborted()) {
                        return false;
                    }
                }

                anyEntries = true;
                String compressedName = ze.getName();

                if (ze.getSize() > getFreeSpace()) {
                    return false;
                }

                if (ze.isDirectory()) {
                    File dir = new File(outputDirectory + compressedName);
                    dir.mkdirs();
                } else {
                    File file = new File(outputDirectory + compressedName);
                    file.getParentFile().mkdirs();
                    if(file.exists() || file.createNewFile()){
                        Log.w(LOG_TAG, "extracting: " + file.getPath());
                        FileOutputStream fout = new FileOutputStream(file);
                        int count;
                        while ((count = zis.read(buffer)) != -1)
                        {
                            fout.write(buffer, 0, count);
                        }
                        fout.close();
                    }

                }
                progress.addLoaded(1);
                updateProgress(callbackContext, progress);
                zis.closeEntry();
            }
        } catch (Exception e) {
            String errorMessage = "An error occurred while unzipping.";
            sendErrorMessage(errorMessage, UNZIP_ERROR, callbackContext);
            Log.e(LOG_TAG, errorMessage, e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                }
            }
            if (zip != null) {
                try {
                    zip.close();
                } catch (IOException e) {
                }
            }
        }

        if (anyEntries)
            return true;
        else
            return false;
    }

    private void updateProgress(CallbackContext callbackContext, ProgressEvent progress) {
        try {
            if (progress.getLoaded() != progress.getTotal() || progress.getStatus() == STATUS_COMPLETE) {
                PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
                pluginResult.setKeepCallback(true);
                callbackContext.sendPluginResult(pluginResult);
            }
        } catch (JSONException e) {
            // never happens
        }
    }

    private Uri getUriForArg(String arg) {
        Uri tmpTarget = Uri.parse(arg);
        return webView.getResourceApi().remapUri(
                tmpTarget.getScheme() != null ? tmpTarget : Uri.fromFile(new File(arg)));
    }

    private static class ProgressEvent {
        private long loaded;
        private long total;
        private double percentage;
        private int status;
        private boolean aborted;
        private File targetFile;
        public ProgressEvent() {
            this.status = STATUS_STOPPED;
        }
        public long getLoaded() {
            return loaded;
        }
        public void setLoaded(long loaded) {
            this.loaded = loaded;
            updatePercentage();
        }
        public void addLoaded(long add) {
            this.loaded += add;
            updatePercentage();
        }
        public long getTotal() {
            return total;
        }
        public void setTotal(long total) {
            this.total = total;
            updatePercentage();
        }
        public int getStatus() {
            return status;
        }
        public void setStatus(int status) {
            this.status = status;
        }
        public boolean isAborted() {
            return aborted;
        }
        public void setAborted(boolean aborted) {
            this.aborted = aborted;
        }
        public File getTargetFile() {
            return targetFile;
        }
        public void setTargetFile(File targetFile) {
            this.targetFile = targetFile;
        }
        public JSONObject toJSONObject() throws JSONException {
            JSONObject jsonProgress = new JSONObject();
            jsonProgress.put(PROP_PROGRESS, this.percentage);
            jsonProgress.put(PROP_STATUS, this.getStatus());
            jsonProgress.put(PROP_LOADED, this.getLoaded());
            jsonProgress.put(PROP_TOTAL, this.getTotal());
            return jsonProgress;

        }
        private void updatePercentage() {
            double loaded = this.getLoaded();
            double total = this.getTotal();
            this.percentage = Math.floor((loaded / total * 100) / 2);
            if (this.getStatus() == STATUS_EXTRACTING) {
                this.percentage += 50;
            }
        }
    }

    private void copyFolder(File src, File dest) throws IOException{
        if(src.isDirectory()) {
            if(!dest.exists()){
                dest.mkdir();
            }

            //list all the directory contents
            String files[] = src.list();

            for (String file : files) {
                //recursive copy
                copyFolder(new File(src, file), new File(dest, file));
            }

        } else {
            //if file, then copy it
            copyFile(new FileInputStream(src), new FileOutputStream(dest));
        }
    }

    private void copyAssetFile(String outputDirectory, String filename, boolean wwwExists) throws IOException {
        String targetFile = filename;
        if (!wwwExists) {
            if (targetFile.startsWith("www/")) {
                targetFile = targetFile.substring(4,targetFile.length());
            }
        }

        int lastIndex = targetFile.lastIndexOf("/");
        if (lastIndex > 0) {
            File targetDir = new File(outputDirectory + "/" + targetFile.substring(0, lastIndex));
            if (!targetDir.exists()) {
                targetDir.mkdirs();
            }
        } else {
            File targetDir = new File(outputDirectory);
            if (!targetDir.exists()) {
                targetDir.mkdirs();
            }
        }

        copyFile(cordova.getActivity().getAssets().open(filename), new FileOutputStream(new File(outputDirectory, targetFile)));
    }

    private void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[4096];

        int length;
        //copy the file content in bytes
        while ((length = in.read(buffer)) > 0){
            out.write(buffer, 0, length);
        }

        in.close();
        out.close();
    }

    private void removeFolder(File directory) {
        if (directory.exists() && directory.isDirectory()) {
            File[] files = directory.listFiles();
            if (files != null) {
                for (File file : files) {
                    removeFolder(file);
                }
            }
        }
        directory.delete();
    }
}

<MSG> Implement cancel and multiple download

Able to cancel download
Clean up error handling
Cancel during unzip
formatting

<DFF> @@ -18,7 +18,6 @@ import java.io.BufferedInputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FilterInputStream;
 import java.io.IOException;
@@ -89,6 +88,12 @@ public class Sync extends CordovaPlugin {
             this.target = target;
             this.callbackContext = callbackContext;
         }
+        public boolean isAborted() {
+        	return aborted;
+        }
+        public void setAborted(boolean aborted) {
+        	this.aborted = aborted;
+        }
         void sendPluginResult(PluginResult pluginResult) {
             synchronized (this) {
                 if (!aborted) {
@@ -114,7 +119,11 @@ public class Sync extends CordovaPlugin {
 
             return true;
         } else if (action.equals("cancel")) {
-        	Log.d(LOG_TAG, "not implemented, yet");
+        	String id = args.getString(0);
+            RequestContext context = activeRequests.get(id);
+            if (context != null) {
+            	context.setAborted(true);
+            }
         }
         return false;
     }
@@ -125,20 +134,20 @@ public class Sync extends CordovaPlugin {
      * the HTTP Content-Length header value from the server.
      */
     private static abstract class TrackingInputStream extends FilterInputStream {
-      public TrackingInputStream(final InputStream in) {
-        super(in);
-      }
-        public abstract long getTotalRawBytesRead();
-  }
+    	public TrackingInputStream(final InputStream in) {
+    		super(in);
+    	}
+    	public abstract long getTotalRawBytesRead();
+    }
 
     private static class ExposedGZIPInputStream extends GZIPInputStream {
-      public ExposedGZIPInputStream(final InputStream in) throws IOException {
-        super(in);
-      }
-      public Inflater getInflater() {
-        return inf;
-      }
-  }
+    	public ExposedGZIPInputStream(final InputStream in) throws IOException {
+    		super(in);
+    	}
+    	public Inflater getInflater() {
+    		return inf;
+    	}
+    }
 
     /**
      * Provides raw bytes-read tracking for a GZIP input stream. Reports the
@@ -146,15 +155,15 @@ public class Sync extends CordovaPlugin {
      * number of uncompressed bytes.
      */
     private static class TrackingGZIPInputStream extends TrackingInputStream {
-      private ExposedGZIPInputStream gzin;
-      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
-        super(gzin);
-        this.gzin = gzin;
-      }
-      public long getTotalRawBytesRead() {
-        return gzin.getInflater().getBytesRead();
-      }
-  }
+    	private ExposedGZIPInputStream gzin;
+    	public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
+    		super(gzin);
+    		this.gzin = gzin;
+    	}
+    	public long getTotalRawBytesRead() {
+    		return gzin.getInflater().getBytesRead();
+    	}
+    }
 
     /**
      * Provides simple total-bytes-read tracking for an existing InputStream
@@ -208,8 +217,10 @@ public class Sync extends CordovaPlugin {
 
         cordova.getThreadPool().execute(new Runnable() {
             public void run() {
-                if (context.aborted) {
-                    return;
+                synchronized (context) {
+                    if (context.isAborted()) {
+                        return;
+                    }
                 }
                 HttpURLConnection connection = null;
                 HostnameVerifier oldHostnameVerifier = null;
@@ -279,9 +290,7 @@ public class Sync extends CordovaPlugin {
                         if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
                             cached = true;
                             connection.disconnect();
-                            Log.d(LOG_TAG, "Resource not modified: " + source);
-                            JSONObject error = new JSONObject();
-                            result = new PluginResult(PluginResult.Status.ERROR, error);
+                            sendErrorMessage("Resource not modified: " + source, callbackContext);
                         } else {
                             if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
                                 // Only trust content-length header if we understand
@@ -291,9 +300,7 @@ public class Sync extends CordovaPlugin {
                                 	if (connectionLength > getFreeSpace()) {
                                 		cached = true;
                                 		connection.disconnect();
-                                        JSONObject error = new JSONObject();
-                                        error.put("message", "Not enough free space to download");
-                                        result = new PluginResult(PluginResult.Status.ERROR, error);
+                                        sendErrorMessage("Not enough free space to download", callbackContext);
                                 	} else {
                                         progress.setTotal(connectionLength);
                                 	}
@@ -306,7 +313,7 @@ public class Sync extends CordovaPlugin {
                     if (!cached) {
                         try {
                             synchronized (context) {
-                                if (context.aborted) {
+                                if (context.isAborted()) {
                                     return;
                                 }
                                 context.connection = connection;
@@ -317,6 +324,11 @@ public class Sync extends CordovaPlugin {
                             int bytesRead = 0;
                             outputStream = resourceApi.openOutputStream(targetUri);
                             while ((bytesRead = inputStream.read(buffer)) > 0) {
+                                synchronized (context) {
+                                    if (context.isAborted()) {
+                                        return;
+                                    }
+                                }
                                 Log.d(LOG_TAG, "bytes read = " + bytesRead);
                                 outputStream.write(buffer, 0, bytesRead);
                                 // Send a progress event.
@@ -326,7 +338,6 @@ public class Sync extends CordovaPlugin {
                             }
 
                             unzip(context.targetFile, id, type, callbackContext);
-
                         } finally {
                             synchronized (context) {
                                 context.connection = null;
@@ -336,30 +347,9 @@ public class Sync extends CordovaPlugin {
                         }
                     }
 
-                } catch (FileNotFoundException e) {
-                    JSONObject error = new JSONObject();
-                    Log.e(LOG_TAG, error.toString(), e);
-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-                    callbackContext.sendPluginResult(result);
-                } catch (IOException e) {
-                    JSONObject error = new JSONObject();
-                    Log.e(LOG_TAG, error.toString(), e);
-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-                    callbackContext.sendPluginResult(result);
-                } catch (JSONException e) {
-                    Log.e(LOG_TAG, e.getMessage(), e);
-                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
-                    callbackContext.sendPluginResult(result);
                 } catch (Throwable e) {
-                    JSONObject error = new JSONObject();
-                    Log.e(LOG_TAG, error.toString(), e);
-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-                    callbackContext.sendPluginResult(result);
+                	sendErrorMessage(e.getLocalizedMessage(), callbackContext);
                 } finally {
-//                    synchronized (activeRequests) {
-//                        activeRequests.remove(id);
-//                    }
-
                     if (connection != null) {
                         // Revert back to the proper verifier and socket factories
                         if (trustEveryone && useHttps) {
@@ -370,7 +360,8 @@ public class Sync extends CordovaPlugin {
                     }
 
                     // Remove incomplete download.
-                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
+                    if (!cached && result != null && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
+                    	Log.d(LOG_TAG, "Delete incomplete file");
                         file.delete();
                     }
                 }
@@ -378,6 +369,19 @@ public class Sync extends CordovaPlugin {
         });
     }
 
+    private void sendErrorMessage(String message, CallbackContext callbackContext) {
+        Log.e(LOG_TAG, message);
+
+        JSONObject error = new JSONObject();
+        try {
+			error.put("message", message);
+		} catch (JSONException e) {
+			// never happens
+		}
+
+        callbackContext.error(error);
+    }
+
 	private long getFreeSpace() {
 		File path = Environment.getDataDirectory();
         StatFs stat = new StatFs(path.getPath());
@@ -391,8 +395,10 @@ public class Sync extends CordovaPlugin {
 		Log.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());
 
 		// unzip
-		//String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();
-		String outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
+		// Production
+		String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();
+		// Testing
+		//String outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
 		outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;
 		outputDirectory += id;
 		Log.d(LOG_TAG, "output dir = " + outputDirectory);
@@ -405,7 +411,6 @@ public class Sync extends CordovaPlugin {
 				try {
 					copyFolder(dir, backup);
 				} catch (IOException e) {
-					// TODO Auto-generated catch block
 					e.printStackTrace();
 				}
 			} else {
@@ -413,22 +418,33 @@ public class Sync extends CordovaPlugin {
 			}
 		}
 
-		if (unzipSync(targetFile, outputDirectory, callbackContext)) {
+		RequestContext requestContext = null;
+		synchronized (activeRequests) {
+			requestContext = activeRequests.get(id);
+		}
+
+		boolean win = unzipSync(targetFile, outputDirectory, requestContext, callbackContext);
+
+		// delete temp file
+		targetFile.delete();
+
+		// complete
+		synchronized (activeRequests) {
+			activeRequests.remove(id);
+		}
+
+		if (win) {
 			// success, remove backup
 			removeFolder(backup);
+
+			JSONObject result = new JSONObject();
+			result.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));
+	        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
 		} else {
 			// failure, revert backup
 			removeFolder(dir);
 			backup.renameTo(dir);
 		}
-
-		// delete temp file
-		targetFile.delete();
-
-		// complete
-		JSONObject result = new JSONObject();
-		result.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));
-        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
 	}
 
 	private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
@@ -552,14 +568,20 @@ public class Sync extends CordovaPlugin {
         return a | b << 8 | c << 16 | d << 24;
     }
 
-    private boolean unzipSync(File targetFile, String outputDirectory, CallbackContext callbackContext) {
+    private boolean unzipSync(File targetFile, String outputDirectory, RequestContext requestContext, CallbackContext callbackContext) {
     	Log.d(LOG_TAG, "unzipSync called");
     	Log.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());
         InputStream inputStream = null;
         ZipFile zip = null;
         boolean anyEntries = false;
         try {
-        	zip = new ZipFile(targetFile);
+            synchronized (requestContext) {
+                if (requestContext.isAborted()) {
+                    return false;
+                }
+            }
+
+            zip = new ZipFile(targetFile);
 
             // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://
             // Accept a path or a URI for the source zip.
@@ -570,9 +592,7 @@ public class Sync extends CordovaPlugin {
 
             File tempFile = resourceApi.mapUriToFile(zipUri);
             if (tempFile == null || !tempFile.exists()) {
-                String errorMessage = "Zip file does not exist";
-                callbackContext.error(errorMessage);
-                Log.e(LOG_TAG, errorMessage);
+                sendErrorMessage("Zip file does not exist", callbackContext);
                 return false;
             }
 
@@ -580,9 +600,7 @@ public class Sync extends CordovaPlugin {
             outputDirectory = outputDir.getAbsolutePath();
             outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;
             if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){
-                String errorMessage = "Could not create output directory";
-                callbackContext.error(errorMessage);
-                Log.e(LOG_TAG, errorMessage);
+                sendErrorMessage("Could not create output directory", callbackContext);
                 return false;
             }
 
@@ -621,8 +639,13 @@ public class Sync extends CordovaPlugin {
             ZipEntry ze;
             byte[] buffer = new byte[32 * 1024];
 
-            while ((ze = zis.getNextEntry()) != null)
-            {
+            while ((ze = zis.getNextEntry()) != null) {
+                synchronized (requestContext) {
+                    if (requestContext.isAborted()) {
+                        return false;
+                    }
+                }
+
                 anyEntries = true;
                 String compressedName = ze.getName();
 
@@ -652,13 +675,9 @@ public class Sync extends CordovaPlugin {
                 updateProgress(callbackContext, progress);
                 zis.closeEntry();
             }
-
-            // final progress = 100%
-//            progress.setLoaded(progress.getTotal());
-//            updateProgress(callbackContext, progress);
         } catch (Exception e) {
             String errorMessage = "An error occurred while unzipping.";
-            callbackContext.error(errorMessage);
+            sendErrorMessage(errorMessage, callbackContext);
             Log.e(LOG_TAG, errorMessage, e);
         } finally {
             if (inputStream != null) {

{'commit': 'diff --git a/src/android/Sync.java b/src/android/Sync.java\nindex bb213ef..1068629 100644\n--- a/src/android/Sync.java\n+++ b/src/android/Sync.java\n@@ -18,7 +18,6 @@ import java.io.BufferedInputStream;\n import java.io.Closeable;\n import java.io.File;\n import java.io.FileInputStream;\n-import java.io.FileNotFoundException;\n import java.io.FileOutputStream;\n import java.io.FilterInputStream;\n import java.io.IOException;\n@@ -89,6 +88,12 @@ public class Sync extends CordovaPlugin {\n             this.target = target;\n             this.callbackContext = callbackContext;\n         }\n+        public boolean isAborted() {\n+        \treturn aborted;\n+        }\n+        public void setAborted(boolean aborted) {\n+        \tthis.aborted = aborted;\n+        }\n         void sendPluginResult(PluginResult pluginResult) {\n             synchronized (this) {\n                 if (!aborted) {\n@@ -114,7 +119,11 @@ public class Sync extends CordovaPlugin {\n \n             return true;\n         } else if (action.equals("cancel")) {\n-        \tLog.d(LOG_TAG, "not implemented, yet");\n+        \tString id = args.getString(0);\n+            RequestContext context = activeRequests.get(id);\n+            if (context != null) {\n+            \tcontext.setAborted(true);\n+            }\n         }\n         return false;\n     }\n@@ -125,20 +134,20 @@ public class Sync extends CordovaPlugin {\n      * the HTTP Content-Length header value from the server.\n      */\n     private static abstract class TrackingInputStream extends FilterInputStream {\n-      public TrackingInputStream(final InputStream in) {\n-        super(in);\n-      }\n-        public abstract long getTotalRawBytesRead();\n-  }\n+    \tpublic TrackingInputStream(final InputStream in) {\n+    \t\tsuper(in);\n+    \t}\n+    \tpublic abstract long getTotalRawBytesRead();\n+    }\n \n     private static class ExposedGZIPInputStream extends GZIPInputStream {\n-      public ExposedGZIPInputStream(final InputStream in) throws IOException {\n-        super(in);\n-      }\n-      public Inflater getInflater() {\n-        return inf;\n-      }\n-  }\n+    \tpublic ExposedGZIPInputStream(final InputStream in) throws IOException {\n+    \t\tsuper(in);\n+    \t}\n+    \tpublic Inflater getInflater() {\n+    \t\treturn inf;\n+    \t}\n+    }\n \n     /**\n      * Provides raw bytes-read tracking for a GZIP input stream. Reports the\n@@ -146,15 +155,15 @@ public class Sync extends CordovaPlugin {\n      * number of uncompressed bytes.\n      */\n     private static class TrackingGZIPInputStream extends TrackingInputStream {\n-      private ExposedGZIPInputStream gzin;\n-      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {\n-        super(gzin);\n-        this.gzin = gzin;\n-      }\n-      public long getTotalRawBytesRead() {\n-        return gzin.getInflater().getBytesRead();\n-      }\n-  }\n+    \tprivate ExposedGZIPInputStream gzin;\n+    \tpublic TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {\n+    \t\tsuper(gzin);\n+    \t\tthis.gzin = gzin;\n+    \t}\n+    \tpublic long getTotalRawBytesRead() {\n+    \t\treturn gzin.getInflater().getBytesRead();\n+    \t}\n+    }\n \n     /**\n      * Provides simple total-bytes-read tracking for an existing InputStream\n@@ -208,8 +217,10 @@ public class Sync extends CordovaPlugin {\n \n         cordova.getThreadPool().execute(new Runnable() {\n             public void run() {\n-                if (context.aborted) {\n-                    return;\n+                synchronized (context) {\n+                    if (context.isAborted()) {\n+                        return;\n+                    }\n                 }\n                 HttpURLConnection connection = null;\n                 HostnameVerifier oldHostnameVerifier = null;\n@@ -279,9 +290,7 @@ public class Sync extends CordovaPlugin {\n                         if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {\n                             cached = true;\n                             connection.disconnect();\n-                            Log.d(LOG_TAG, "Resource not modified: " + source);\n-                            JSONObject error = new JSONObject();\n-                            result = new PluginResult(PluginResult.Status.ERROR, error);\n+                            sendErrorMessage("Resource not modified: " + source, callbackContext);\n                         } else {\n                             if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {\n                                 // Only trust content-length header if we understand\n@@ -291,9 +300,7 @@ public class Sync extends CordovaPlugin {\n                                 \tif (connectionLength > getFreeSpace()) {\n                                 \t\tcached = true;\n                                 \t\tconnection.disconnect();\n-                                        JSONObject error = new JSONObject();\n-                                        error.put("message", "Not enough free space to download");\n-                                        result = new PluginResult(PluginResult.Status.ERROR, error);\n+                                        sendErrorMessage("Not enough free space to download", callbackContext);\n                                 \t} else {\n                                         progress.setTotal(connectionLength);\n                                 \t}\n@@ -306,7 +313,7 @@ public class Sync extends CordovaPlugin {\n                     if (!cached) {\n                         try {\n                             synchronized (context) {\n-                                if (context.aborted) {\n+                                if (context.isAborted()) {\n                                     return;\n                                 }\n                                 context.connection = connection;\n@@ -317,6 +324,11 @@ public class Sync extends CordovaPlugin {\n                             int bytesRead = 0;\n                             outputStream = resourceApi.openOutputStream(targetUri);\n                             while ((bytesRead = inputStream.read(buffer)) > 0) {\n+                                synchronized (context) {\n+                                    if (context.isAborted()) {\n+                                        return;\n+                                    }\n+                                }\n                                 Log.d(LOG_TAG, "bytes read = " + bytesRead);\n                                 outputStream.write(buffer, 0, bytesRead);\n                                 // Send a progress event.\n@@ -326,7 +338,6 @@ public class Sync extends CordovaPlugin {\n                             }\n \n                             unzip(context.targetFile, id, type, callbackContext);\n-\n                         } finally {\n                             synchronized (context) {\n                                 context.connection = null;\n@@ -336,30 +347,9 @@ public class Sync extends CordovaPlugin {\n                         }\n                     }\n \n-                } catch (FileNotFoundException e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n-                } catch (IOException e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n-                } catch (JSONException e) {\n-                    Log.e(LOG_TAG, e.getMessage(), e);\n-                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);\n-                    callbackContext.sendPluginResult(result);\n                 } catch (Throwable e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n+                \tsendErrorMessage(e.getLocalizedMessage(), callbackContext);\n                 } finally {\n-//                    synchronized (activeRequests) {\n-//                        activeRequests.remove(id);\n-//                    }\n-\n                     if (connection != null) {\n                         // Revert back to the proper verifier and socket factories\n                         if (trustEveryone && useHttps) {\n@@ -370,7 +360,8 @@ public class Sync extends CordovaPlugin {\n                     }\n \n                     // Remove incomplete download.\n-                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {\n+                    if (!cached && result != null && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {\n+                    \tLog.d(LOG_TAG, "Delete incomplete file");\n                         file.delete();\n                     }\n                 }\n@@ -378,6 +369,19 @@ public class Sync extends CordovaPlugin {\n         });\n     }\n \n+    private void sendErrorMessage(String message, CallbackContext callbackContext) {\n+        Log.e(LOG_TAG, message);\n+\n+        JSONObject error = new JSONObject();\n+        try {\n+\t\t\terror.put("message", message);\n+\t\t} catch (JSONException e) {\n+\t\t\t// never happens\n+\t\t}\n+\n+        callbackContext.error(error);\n+    }\n+\n \tprivate long getFreeSpace() {\n \t\tFile path = Environment.getDataDirectory();\n         StatFs stat = new StatFs(path.getPath());\n@@ -391,8 +395,10 @@ public class Sync extends CordovaPlugin {\n \t\tLog.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());\n \n \t\t// unzip\n-\t\t//String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();\n-\t\tString outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();\n+\t\t// Production\n+\t\tString outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();\n+\t\t// Testing\n+\t\t//String outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();\n \t\toutputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n \t\toutputDirectory += id;\n \t\tLog.d(LOG_TAG, "output dir = " + outputDirectory);\n@@ -405,7 +411,6 @@ public class Sync extends CordovaPlugin {\n \t\t\t\ttry {\n \t\t\t\t\tcopyFolder(dir, backup);\n \t\t\t\t} catch (IOException e) {\n-\t\t\t\t\t// TODO Auto-generated catch block\n \t\t\t\t\te.printStackTrace();\n \t\t\t\t}\n \t\t\t} else {\n@@ -413,22 +418,33 @@ public class Sync extends CordovaPlugin {\n \t\t\t}\n \t\t}\n \n-\t\tif (unzipSync(targetFile, outputDirectory, callbackContext)) {\n+\t\tRequestContext requestContext = null;\n+\t\tsynchronized (activeRequests) {\n+\t\t\trequestContext = activeRequests.get(id);\n+\t\t}\n+\n+\t\tboolean win = unzipSync(targetFile, outputDirectory, requestContext, callbackContext);\n+\n+\t\t// delete temp file\n+\t\ttargetFile.delete();\n+\n+\t\t// complete\n+\t\tsynchronized (activeRequests) {\n+\t\t\tactiveRequests.remove(id);\n+\t\t}\n+\n+\t\tif (win) {\n \t\t\t// success, remove backup\n \t\t\tremoveFolder(backup);\n+\n+\t\t\tJSONObject result = new JSONObject();\n+\t\t\tresult.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));\n+\t        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n \t\t} else {\n \t\t\t// failure, revert backup\n \t\t\tremoveFolder(dir);\n \t\t\tbackup.renameTo(dir);\n \t\t}\n-\n-\t\t// delete temp file\n-\t\ttargetFile.delete();\n-\n-\t\t// complete\n-\t\tJSONObject result = new JSONObject();\n-\t\tresult.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));\n-        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n \t}\n \n \tprivate static TrackingInputStream getInputStream(URLConnection conn) throws IOException {\n@@ -552,14 +568,20 @@ public class Sync extends CordovaPlugin {\n         return a | b << 8 | c << 16 | d << 24;\n     }\n \n-    private boolean unzipSync(File targetFile, String outputDirectory, CallbackContext callbackContext) {\n+    private boolean unzipSync(File targetFile, String outputDirectory, RequestContext requestContext, CallbackContext callbackContext) {\n     \tLog.d(LOG_TAG, "unzipSync called");\n     \tLog.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());\n         InputStream inputStream = null;\n         ZipFile zip = null;\n         boolean anyEntries = false;\n         try {\n-        \tzip = new ZipFile(targetFile);\n+            synchronized (requestContext) {\n+                if (requestContext.isAborted()) {\n+                    return false;\n+                }\n+            }\n+\n+            zip = new ZipFile(targetFile);\n \n             // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://\n             // Accept a path or a URI for the source zip.\n@@ -570,9 +592,7 @@ public class Sync extends CordovaPlugin {\n \n             File tempFile = resourceApi.mapUriToFile(zipUri);\n             if (tempFile == null || !tempFile.exists()) {\n-                String errorMessage = "Zip file does not exist";\n-                callbackContext.error(errorMessage);\n-                Log.e(LOG_TAG, errorMessage);\n+                sendErrorMessage("Zip file does not exist", callbackContext);\n                 return false;\n             }\n \n@@ -580,9 +600,7 @@ public class Sync extends CordovaPlugin {\n             outputDirectory = outputDir.getAbsolutePath();\n             outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n             if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){\n-                String errorMessage = "Could not create output directory";\n-                callbackContext.error(errorMessage);\n-                Log.e(LOG_TAG, errorMessage);\n+                sendErrorMessage("Could not create output directory", callbackContext);\n                 return false;\n             }\n \n@@ -621,8 +639,13 @@ public class Sync extends CordovaPlugin {\n             ZipEntry ze;\n             byte[] buffer = new byte[32 * 1024];\n \n-            while ((ze = zis.getNextEntry()) != null)\n-            {\n+            while ((ze = zis.getNextEntry()) != null) {\n+                synchronized (requestContext) {\n+                    if (requestContext.isAborted()) {\n+                        return false;\n+                    }\n+                }\n+\n                 anyEntries = true;\n                 String compressedName = ze.getName();\n \n@@ -652,13 +675,9 @@ public class Sync extends CordovaPlugin {\n                 updateProgress(callbackContext, progress);\n                 zis.closeEntry();\n             }\n-\n-            // final progress = 100%\n-//            progress.setLoaded(progress.getTotal());\n-//            updateProgress(callbackContext, progress);\n         } catch (Exception e) {\n             String errorMessage = "An error occurred while unzipping.";\n-            callbackContext.error(errorMessage);\n+            sendErrorMessage(errorMessage, callbackContext);\n             Log.e(LOG_TAG, errorMessage, e);\n         } finally {\n             if (inputStream != null) {\n', 'commit_subject': 'Implement cancel and multiple download', 'repo_name': 'phonegap/phonegap-plugin-contentsync', 'committer': {'name': 'Simon MacDonald', 'email': 'ecd731c54c6018074b7e65057ff5c290259d785b@gmail.com', 'time_sec': '1426534943', 'tz_offset': '-240', 'date': {'seconds': '1426534943'}}, 'commit_tree': 'a4272c26a09b763a4223290b2dbbbf81863003f4', 'commit_message': 'Implement cancel and multiple download\n\nAble to cancel download\nClean up error handling\nCancel during unzip\nformatting\n', 'language_name': 'C#', 'license': 'apache-2.0', 'hunks': '@@ -18,7 +18,6 @@ import java.io.BufferedInputStream;\n import java.io.Closeable;\n import java.io.File;\n import java.io.FileInputStream;\n-import java.io.FileNotFoundException;\n import java.io.FileOutputStream;\n import java.io.FilterInputStream;\n import java.io.IOException;\n@@ -89,6 +88,12 @@ public class Sync extends CordovaPlugin {\n             this.target = target;\n             this.callbackContext = callbackContext;\n         }\n+        public boolean isAborted() {\n+        \treturn aborted;\n+        }\n+        public void setAborted(boolean aborted) {\n+        \tthis.aborted = aborted;\n+        }\n         void sendPluginResult(PluginResult pluginResult) {\n             synchronized (this) {\n                 if (!aborted) {\n@@ -114,7 +119,11 @@ public class Sync extends CordovaPlugin {\n \n             return true;\n         } else if (action.equals("cancel")) {\n-        \tLog.d(LOG_TAG, "not implemented, yet");\n+        \tString id = args.getString(0);\n+            RequestContext context = activeRequests.get(id);\n+            if (context != null) {\n+            \tcontext.setAborted(true);\n+            }\n         }\n         return false;\n     }\n@@ -125,20 +134,20 @@ public class Sync extends CordovaPlugin {\n      * the HTTP Content-Length header value from the server.\n      */\n     private static abstract class TrackingInputStream extends FilterInputStream {\n-      public TrackingInputStream(final InputStream in) {\n-        super(in);\n-      }\n-        public abstract long getTotalRawBytesRead();\n-  }\n+    \tpublic TrackingInputStream(final InputStream in) {\n+    \t\tsuper(in);\n+    \t}\n+    \tpublic abstract long getTotalRawBytesRead();\n+    }\n \n     private static class ExposedGZIPInputStream extends GZIPInputStream {\n-      public ExposedGZIPInputStream(final InputStream in) throws IOException {\n-        super(in);\n-      }\n-      public Inflater getInflater() {\n-        return inf;\n-      }\n-  }\n+    \tpublic ExposedGZIPInputStream(final InputStream in) throws IOException {\n+    \t\tsuper(in);\n+    \t}\n+    \tpublic Inflater getInflater() {\n+    \t\treturn inf;\n+    \t}\n+    }\n \n     /**\n      * Provides raw bytes-read tracking for a GZIP input stream. Reports the\n@@ -146,15 +155,15 @@ public class Sync extends CordovaPlugin {\n      * number of uncompressed bytes.\n      */\n     private static class TrackingGZIPInputStream extends TrackingInputStream {\n-      private ExposedGZIPInputStream gzin;\n-      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {\n-        super(gzin);\n-        this.gzin = gzin;\n-      }\n-      public long getTotalRawBytesRead() {\n-        return gzin.getInflater().getBytesRead();\n-      }\n-  }\n+    \tprivate ExposedGZIPInputStream gzin;\n+    \tpublic TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {\n+    \t\tsuper(gzin);\n+    \t\tthis.gzin = gzin;\n+    \t}\n+    \tpublic long getTotalRawBytesRead() {\n+    \t\treturn gzin.getInflater().getBytesRead();\n+    \t}\n+    }\n \n     /**\n      * Provides simple total-bytes-read tracking for an existing InputStream\n@@ -208,8 +217,10 @@ public class Sync extends CordovaPlugin {\n \n         cordova.getThreadPool().execute(new Runnable() {\n             public void run() {\n-                if (context.aborted) {\n-                    return;\n+                synchronized (context) {\n+                    if (context.isAborted()) {\n+                        return;\n+                    }\n                 }\n                 HttpURLConnection connection = null;\n                 HostnameVerifier oldHostnameVerifier = null;\n@@ -279,9 +290,7 @@ public class Sync extends CordovaPlugin {\n                         if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {\n                             cached = true;\n                             connection.disconnect();\n-                            Log.d(LOG_TAG, "Resource not modified: " + source);\n-                            JSONObject error = new JSONObject();\n-                            result = new PluginResult(PluginResult.Status.ERROR, error);\n+                            sendErrorMessage("Resource not modified: " + source, callbackContext);\n                         } else {\n                             if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {\n                                 // Only trust content-length header if we understand\n@@ -291,9 +300,7 @@ public class Sync extends CordovaPlugin {\n                                 \tif (connectionLength > getFreeSpace()) {\n                                 \t\tcached = true;\n                                 \t\tconnection.disconnect();\n-                                        JSONObject error = new JSONObject();\n-                                        error.put("message", "Not enough free space to download");\n-                                        result = new PluginResult(PluginResult.Status.ERROR, error);\n+                                        sendErrorMessage("Not enough free space to download", callbackContext);\n                                 \t} else {\n                                         progress.setTotal(connectionLength);\n                                 \t}\n@@ -306,7 +313,7 @@ public class Sync extends CordovaPlugin {\n                     if (!cached) {\n                         try {\n                             synchronized (context) {\n-                                if (context.aborted) {\n+                                if (context.isAborted()) {\n                                     return;\n                                 }\n                                 context.connection = connection;\n@@ -317,6 +324,11 @@ public class Sync extends CordovaPlugin {\n                             int bytesRead = 0;\n                             outputStream = resourceApi.openOutputStream(targetUri);\n                             while ((bytesRead = inputStream.read(buffer)) > 0) {\n+                                synchronized (context) {\n+                                    if (context.isAborted()) {\n+                                        return;\n+                                    }\n+                                }\n                                 Log.d(LOG_TAG, "bytes read = " + bytesRead);\n                                 outputStream.write(buffer, 0, bytesRead);\n                                 // Send a progress event.\n@@ -326,7 +338,6 @@ public class Sync extends CordovaPlugin {\n                             }\n \n                             unzip(context.targetFile, id, type, callbackContext);\n-\n                         } finally {\n                             synchronized (context) {\n                                 context.connection = null;\n@@ -336,30 +347,9 @@ public class Sync extends CordovaPlugin {\n                         }\n                     }\n \n-                } catch (FileNotFoundException e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n-                } catch (IOException e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n-                } catch (JSONException e) {\n-                    Log.e(LOG_TAG, e.getMessage(), e);\n-                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);\n-                    callbackContext.sendPluginResult(result);\n                 } catch (Throwable e) {\n-                    JSONObject error = new JSONObject();\n-                    Log.e(LOG_TAG, error.toString(), e);\n-                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n-                    callbackContext.sendPluginResult(result);\n+                \tsendErrorMessage(e.getLocalizedMessage(), callbackContext);\n                 } finally {\n-//                    synchronized (activeRequests) {\n-//                        activeRequests.remove(id);\n-//                    }\n-\n                     if (connection != null) {\n                         // Revert back to the proper verifier and socket factories\n                         if (trustEveryone && useHttps) {\n@@ -370,7 +360,8 @@ public class Sync extends CordovaPlugin {\n                     }\n \n                     // Remove incomplete download.\n-                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {\n+                    if (!cached && result != null && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {\n+                    \tLog.d(LOG_TAG, "Delete incomplete file");\n                         file.delete();\n                     }\n                 }\n@@ -378,6 +369,19 @@ public class Sync extends CordovaPlugin {\n         });\n     }\n \n+    private void sendErrorMessage(String message, CallbackContext callbackContext) {\n+        Log.e(LOG_TAG, message);\n+\n+        JSONObject error = new JSONObject();\n+        try {\n+\t\t\terror.put("message", message);\n+\t\t} catch (JSONException e) {\n+\t\t\t// never happens\n+\t\t}\n+\n+        callbackContext.error(error);\n+    }\n+\n \tprivate long getFreeSpace() {\n \t\tFile path = Environment.getDataDirectory();\n         StatFs stat = new StatFs(path.getPath());\n@@ -391,8 +395,10 @@ public class Sync extends CordovaPlugin {\n \t\tLog.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());\n \n \t\t// unzip\n-\t\t//String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();\n-\t\tString outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();\n+\t\t// Production\n+\t\tString outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();\n+\t\t// Testing\n+\t\t//String outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();\n \t\toutputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n \t\toutputDirectory += id;\n \t\tLog.d(LOG_TAG, "output dir = " + outputDirectory);\n@@ -405,7 +411,6 @@ public class Sync extends CordovaPlugin {\n \t\t\t\ttry {\n \t\t\t\t\tcopyFolder(dir, backup);\n \t\t\t\t} catch (IOException e) {\n-\t\t\t\t\t// TODO Auto-generated catch block\n \t\t\t\t\te.printStackTrace();\n \t\t\t\t}\n \t\t\t} else {\n@@ -413,22 +418,33 @@ public class Sync extends CordovaPlugin {\n \t\t\t}\n \t\t}\n \n-\t\tif (unzipSync(targetFile, outputDirectory, callbackContext)) {\n+\t\tRequestContext requestContext = null;\n+\t\tsynchronized (activeRequests) {\n+\t\t\trequestContext = activeRequests.get(id);\n+\t\t}\n+\n+\t\tboolean win = unzipSync(targetFile, outputDirectory, requestContext, callbackContext);\n+\n+\t\t// delete temp file\n+\t\ttargetFile.delete();\n+\n+\t\t// complete\n+\t\tsynchronized (activeRequests) {\n+\t\t\tactiveRequests.remove(id);\n+\t\t}\n+\n+\t\tif (win) {\n \t\t\t// success, remove backup\n \t\t\tremoveFolder(backup);\n+\n+\t\t\tJSONObject result = new JSONObject();\n+\t\t\tresult.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));\n+\t        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n \t\t} else {\n \t\t\t// failure, revert backup\n \t\t\tremoveFolder(dir);\n \t\t\tbackup.renameTo(dir);\n \t\t}\n-\n-\t\t// delete temp file\n-\t\ttargetFile.delete();\n-\n-\t\t// complete\n-\t\tJSONObject result = new JSONObject();\n-\t\tresult.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));\n-        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n \t}\n \n \tprivate static TrackingInputStream getInputStream(URLConnection conn) throws IOException {\n@@ -552,14 +568,20 @@ public class Sync extends CordovaPlugin {\n         return a | b << 8 | c << 16 | d << 24;\n     }\n \n-    private boolean unzipSync(File targetFile, String outputDirectory, CallbackContext callbackContext) {\n+    private boolean unzipSync(File targetFile, String outputDirectory, RequestContext requestContext, CallbackContext callbackContext) {\n     \tLog.d(LOG_TAG, "unzipSync called");\n     \tLog.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());\n         InputStream inputStream = null;\n         ZipFile zip = null;\n         boolean anyEntries = false;\n         try {\n-        \tzip = new ZipFile(targetFile);\n+            synchronized (requestContext) {\n+                if (requestContext.isAborted()) {\n+                    return false;\n+                }\n+            }\n+\n+            zip = new ZipFile(targetFile);\n \n             // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://\n             // Accept a path or a URI for the source zip.\n@@ -570,9 +592,7 @@ public class Sync extends CordovaPlugin {\n \n             File tempFile = resourceApi.mapUriToFile(zipUri);\n             if (tempFile == null || !tempFile.exists()) {\n-                String errorMessage = "Zip file does not exist";\n-                callbackContext.error(errorMessage);\n-                Log.e(LOG_TAG, errorMessage);\n+                sendErrorMessage("Zip file does not exist", callbackContext);\n                 return false;\n             }\n \n@@ -580,9 +600,7 @@ public class Sync extends CordovaPlugin {\n             outputDirectory = outputDir.getAbsolutePath();\n             outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n             if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){\n-                String errorMessage = "Could not create output directory";\n-                callbackContext.error(errorMessage);\n-                Log.e(LOG_TAG, errorMessage);\n+                sendErrorMessage("Could not create output directory", callbackContext);\n                 return false;\n             }\n \n@@ -621,8 +639,13 @@ public class Sync extends CordovaPlugin {\n             ZipEntry ze;\n             byte[] buffer = new byte[32 * 1024];\n \n-            while ((ze = zis.getNextEntry()) != null)\n-            {\n+            while ((ze = zis.getNextEntry()) != null) {\n+                synchronized (requestContext) {\n+                    if (requestContext.isAborted()) {\n+                        return false;\n+                    }\n+                }\n+\n                 anyEntries = true;\n                 String compressedName = ze.getName();\n \n@@ -652,13 +675,9 @@ public class Sync extends CordovaPlugin {\n                 updateProgress(callbackContext, progress);\n                 zis.closeEntry();\n             }\n-\n-            // final progress = 100%\n-//            progress.setLoaded(progress.getTotal());\n-//            updateProgress(callbackContext, progress);\n         } catch (Exception e) {\n             String errorMessage = "An error occurred while unzipping.";\n-            callbackContext.error(errorMessage);\n+            sendErrorMessage(errorMessage, callbackContext);\n             Log.e(LOG_TAG, errorMessage, e);\n         } finally {\n             if (inputStream != null) {\n', 'addition_count': 102, 'deletion_count': 83, 'src_file': 'a/src/android/Sync.java', 'tgt_file': 'b/src/android/Sync.java', 'file_extension': '.java', 'before_file': '/*\n    Licensed under the Apache License, Version 2.0 (the "License");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an "AS IS" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n*/\n\npackage com.adobe.phonegap.contentsync;\n\nimport java.io.BufferedInputStream;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.FilterInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.net.HttpURLConnection;\nimport java.net.URLConnection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.zip.GZIPInputStream;\nimport java.util.zip.Inflater;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\n\nimport org.apache.cordova.CordovaPlugin;\nimport org.apache.cordova.CallbackContext;\nimport org.apache.cordova.CordovaResourceApi;\nimport org.apache.cordova.CordovaResourceApi.OpenForReadResult;\nimport org.apache.cordova.PluginResult;\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport android.app.Activity;\nimport android.content.SharedPreferences;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Environment;\nimport android.os.StatFs;\nimport android.util.Log;\nimport android.util.Patterns;\nimport android.webkit.CookieManager;\n\npublic class Sync extends CordovaPlugin {\n    private static final int STATUS_STOPPED = 0;\n    private static final int STATUS_DOWNLOADING = 1;\n    private static final int STATUS_EXTRACTING = 2;\n    private static final int STATUS_COMPLETE = 3;\n\n    public static final int INVALID_URL_ERROR = 1;\n    public static final int CONNECTION_ERROR = 2;\n    public static final int UNZIP_ERROR = 3;\n\n    private static final String PROP_LOCAL_PATH = "localPath";\n    private static final String PROP_STATUS = "status";\n    private static final String PROP_PROGRESS = "progress";\n    private static final String PROP_LOADED = "loaded";\n    private static final String PROP_TOTAL = "total";\n    private static final String PROP_CACHED = "cached";\n    // Type\n    private static final String TYPE_REPLACE = "replace";\n    private static final String TYPE_MERGE = "merge";\n    private static final String TYPE_LOCAL = "local";\n\n    private static final String LOG_TAG = "ContentSync";\n    public static final String PREVIOUS_VERSION = "PREVIOUS_VERSION";\n\n    private static HashMap<String, ProgressEvent> activeRequests = new HashMap<String, ProgressEvent>();\n    private static final int MAX_BUFFER_SIZE = 16 * 1024;\n\n    @Override\n    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {\n        if (action.equals("sync")) {\n            sync(args, callbackContext);\n            this.target = target;\n            this.callbackContext = callbackContext;\n        }\n        void sendPluginResult(PluginResult pluginResult) {\n            synchronized (this) {\n                if (!aborted) {\n            cordova.getThreadPool().execute(new Runnable() {\n                public void run() {\n                    if (download(source, target, headers, createProgressEvent("download"), finalContext)) {\n                        JSONObject retval = new JSONObject();\n                        try {\n                            retval.put("archiveURL", target.getAbsolutePath());\n                        } catch (JSONException e) {\n                            // never happens\n                        }\n                        finalContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, retval));\n                    }\n                }\n            });\n            return true;\n        } else if (action.equals("unzip")) {\n            String tempPath = args.getString(0);\n            if (tempPath.startsWith("file://")) {\n                tempPath = tempPath.substring(7);\n            }\n\n            return true;\n        } else if (action.equals("cancel")) {\n        \tLog.d(LOG_TAG, "not implemented, yet");\n        }\n        return false;\n    }\n            ProgressEvent progress = activeRequests.get(args.getString(0));\n            if (progress != null) {\n                progress.setAborted(true);\n            }\n     * the HTTP Content-Length header value from the server.\n     */\n    private static abstract class TrackingInputStream extends FilterInputStream {\n      public TrackingInputStream(final InputStream in) {\n        super(in);\n      }\n        public abstract long getTotalRawBytesRead();\n  }\n\n    private static class ExposedGZIPInputStream extends GZIPInputStream {\n      public ExposedGZIPInputStream(final InputStream in) throws IOException {\n        super(in);\n      }\n      public Inflater getInflater() {\n        return inf;\n      }\n  }\n\n    /**\n     * Provides raw bytes-read tracking for a GZIP input stream. Reports the\n        public Inflater getInflater() {\n     * number of uncompressed bytes.\n     */\n    private static class TrackingGZIPInputStream extends TrackingInputStream {\n      private ExposedGZIPInputStream gzin;\n      public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {\n        super(gzin);\n        this.gzin = gzin;\n      }\n      public long getTotalRawBytesRead() {\n        return gzin.getInflater().getBytesRead();\n      }\n  }\n\n    /**\n     * Provides simple total-bytes-read tracking for an existing InputStream\n        public long getTotalRawBytesRead() {\n            return gzin.getInflater().getBytesRead();\n        }\n    }\n\n    /**\n     * Provides simple total-bytes-read tracking for an existing InputStream\n     */\n    private static class SimpleTrackingInputStream extends TrackingInputStream {\n        private long bytesRead = 0;\n        public SimpleTrackingInputStream(InputStream stream) {\n            super(stream);\n        }\n\n        private int updateBytesRead(int newBytesRead) {\n            if (newBytesRead != -1) {\n                bytesRead += newBytesRead;\n            }\n            return newBytesRead;\n        }\n\n        @Override\n        public int read() throws IOException {\n            return updateBytesRead(super.read());\n        }\n\n        // Note: FilterInputStream delegates read(byte[] bytes) to the below method,\n        // so we don\'t override it or else double count (CB-5631).\n        @Override\n        public int read(byte[] bytes, int offset, int count) throws IOException {\n            return updateBytesRead(super.read(bytes, offset, count));\n        }\n\n        public long getTotalRawBytesRead() {\n            return bytesRead;\n        }\n    }\n\n    private boolean download(final String source, final File file, final JSONObject headers, final ProgressEvent progress, final CallbackContext callbackContext) {\n        Log.d(LOG_TAG, "download " + source);\n\n        if (!Patterns.WEB_URL.matcher(source).matches()) {\n            sendErrorMessage("Invalid URL", INVALID_URL_ERROR, callbackContext);\n            return false;\n        }\n\n        final CordovaResourceApi resourceApi = webView.getResourceApi();\n\n        cordova.getThreadPool().execute(new Runnable() {\n            public void run() {\n                if (context.aborted) {\n                    return;\n                }\n                HttpURLConnection connection = null;\n                HostnameVerifier oldHostnameVerifier = null;\n        }\n        HttpURLConnection connection = null;\n        PluginResult result = null;\n        TrackingInputStream inputStream = null;\n        boolean cached = false;\n        boolean retval = true;\n\n        OutputStream outputStream = null;\n        try {\n            OpenForReadResult readResult = null;\n            final Uri targetUri = resourceApi.remapUri(Uri.fromFile(file));\n\n            progress.setTargetFile(file);\n            progress.setStatus(STATUS_DOWNLOADING);\n\n            Log.d(LOG_TAG, "Download file: " + sourceUri);\n            Log.d(LOG_TAG, "Target file: " + file);\n            Log.d(LOG_TAG, "size = " + file.length());\n\n            if (isLocalTransfer) {\n                readResult = resourceApi.openForRead(sourceUri);\n                if (readResult.length != -1) {\n                    progress.setTotal(readResult.length);\n                }\n                inputStream = new SimpleTrackingInputStream(readResult.inputStream);\n            } else {\n                // connect to server\n                // Open a HTTP connection to the URL based on protocol\n                connection = resourceApi.createHttpConnection(sourceUri);\n                connection.setRequestMethod("GET");\n\n                // TODO: Make OkHttp use this CookieManager by default.\n                String cookie = getCookies(sourceUri.toString());\n\n                if(cookie != null)\n                {\n                    connection.setRequestProperty("cookie", cookie);\n                }\n\n                // This must be explicitly set for gzip progress tracking to work.\n                connection.setRequestProperty("Accept-Encoding", "gzip");\n\n                // Handle the other headers\n                if (headers != null) {\n                    addHeadersToRequest(connection, headers);\n                }\n\n                connection.connect();\n                if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {\n                    cached = true;\n                    connection.disconnect();\n                    sendErrorMessage("Resource not modified: " + source, CONNECTION_ERROR, callbackContext, connection.getResponseCode());\n                    retval = false;\n                    return retval;\n                } else {\n                    if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {\n                        // Only trust content-length header if we understand\n                        // the encoding -- identity or gzip\n                        int connectionLength = connection.getContentLength();\n                        if (connectionLength != -1) {\n                            if (connectionLength > getFreeSpace()) {\n                                cached = true;\n                                connection.disconnect();\n                        if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {\n                            cached = true;\n                            connection.disconnect();\n                            Log.d(LOG_TAG, "Resource not modified: " + source);\n                            JSONObject error = new JSONObject();\n                            result = new PluginResult(PluginResult.Status.ERROR, error);\n                        } else {\n                            if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {\n                                // Only trust content-length header if we understand\n                    }\n                    inputStream = getInputStream(connection);\n                }\n                                \tif (connectionLength > getFreeSpace()) {\n                                \t\tcached = true;\n                                \t\tconnection.disconnect();\n                                        JSONObject error = new JSONObject();\n                                        error.put("message", "Not enough free space to download");\n                                        result = new PluginResult(PluginResult.Status.ERROR, error);\n                                \t} else {\n                                        progress.setTotal(connectionLength);\n                                \t}\n                            return retval;\n                        }\n                        //progress.connection = connection;\n                    }\n\n                    // write bytes to file\n                    if (!cached) {\n                        try {\n                            synchronized (context) {\n                                if (context.aborted) {\n                                    return;\n                                }\n                                context.connection = connection;\n                                return retval;\n                            }\n                        }\n                        Log.d(LOG_TAG, "bytes read = " + bytesRead);\n                            int bytesRead = 0;\n                            outputStream = resourceApi.openOutputStream(targetUri);\n                            while ((bytesRead = inputStream.read(buffer)) > 0) {\n                                Log.d(LOG_TAG, "bytes read = " + bytesRead);\n                                outputStream.write(buffer, 0, bytesRead);\n                                // Send a progress event.\n                    safeClose(outputStream);\n                }\n            }\n                            }\n\n                            unzip(context.targetFile, id, type, callbackContext);\n\n                        } finally {\n                            synchronized (context) {\n                                context.connection = null;\n            }\n        }\n\n                        }\n                    }\n\n                } catch (FileNotFoundException e) {\n                    JSONObject error = new JSONObject();\n                    Log.e(LOG_TAG, error.toString(), e);\n                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n                    callbackContext.sendPluginResult(result);\n                } catch (IOException e) {\n                    JSONObject error = new JSONObject();\n                    Log.e(LOG_TAG, error.toString(), e);\n                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n                    callbackContext.sendPluginResult(result);\n                } catch (JSONException e) {\n                    Log.e(LOG_TAG, e.getMessage(), e);\n                    result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);\n                    callbackContext.sendPluginResult(result);\n                } catch (Throwable e) {\n                    JSONObject error = new JSONObject();\n                    Log.e(LOG_TAG, error.toString(), e);\n                    result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);\n                    callbackContext.sendPluginResult(result);\n                } finally {\n//                    synchronized (activeRequests) {\n//                        activeRequests.remove(id);\n//                    }\n\n                    if (connection != null) {\n                        // Revert back to the proper verifier and socket factories\n                        if (trustEveryone && useHttps) {\n        JSONObject error = new JSONObject();\n        try {\n            error.put("type", type);\n            error.put("responseCode", httpResponseCode);\n                    }\n\n                    // Remove incomplete download.\n                    if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {\n                        file.delete();\n                    }\n                }\n        long blockSize = stat.getBlockSize();\n        });\n    }\n\n\tprivate long getFreeSpace() {\n\t\tFile path = Environment.getDataDirectory();\n        StatFs stat = new StatFs(path.getPath());\n        } else {\n            headers = new JSONObject();\n        }\n        final boolean copyCordovaAssets;\n        final boolean copyRootApp = args.getBoolean(5);\n        if (copyRootApp) {\n            copyCordovaAssets = true;\n\t\tLog.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());\n\n\t\t// unzip\n\t\t//String outputDirectory = cordova.getActivity().getFilesDir().getAbsolutePath();\n\t\tString outputDirectory = cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();\n\t\toutputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n\t\toutputDirectory += id;\n\t\tLog.d(LOG_TAG, "output dir = " + outputDirectory);\n         * content\n         */\n        cordova.getActivity().runOnUiThread(new Runnable() {\n            public void run() {\n                webView.clearCache(true);\n            }\n\t\t\t\ttry {\n\t\t\t\t\tcopyFolder(dir, backup);\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\t// TODO Auto-generated catch block\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t} else {\n                        return;\n\t\t\t}\n\t\t}\n\n\t\tif (unzipSync(targetFile, outputDirectory, callbackContext)) {\n\t\t\t// success, remove backup\n\t\t\tremoveFolder(backup);\n\t\t} else {\n\t\t\t// failure, revert backup\n\t\t\tremoveFolder(dir);\n\t\t\tbackup.renameTo(dir);\n\t\t}\n\n\t\t// delete temp file\n\t\ttargetFile.delete();\n\n\t\t// complete\n\t\tJSONObject result = new JSONObject();\n\t\tresult.put(PROP_LOCAL_PATH, getUriForArg(outputDirectory));\n        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n\t}\n\n\tprivate static TrackingInputStream getInputStream(URLConnection conn) throws IOException {\n                if (!type.equals(TYPE_LOCAL)) {\n                    // download file\n                    if (download(src, createDownloadFileLocation(id), headers, progress, callbackContext)) {\n                        // update progress with zip file\n                        File targetFile = progress.getTargetFile();\n                        Log.d(LOG_TAG, "downloaded = " + targetFile.getAbsolutePath());\n\n                        // Backup existing directory\n                        File backup = backupExistingDirectory(outputDirectory, type, dir);\n\n                        // @TODO: Do we do this even when type is local?\n                        if (copyRootApp) {\n                            copyRootApp(outputDirectory, manifestFile);\n                        }\n\n                        boolean win = false;\n                        if (isZipFile(targetFile)) {\n                            win = unzipSync(targetFile, outputDirectory, progress, callbackContext);\n                        } else {\n                            // copy file to ID\n                            win = targetFile.renameTo(new File(outputDirectory));\n                            progress.setLoaded(1);\n                            progress.setTotal(1);\n                            progress.setStatus(STATUS_EXTRACTING);\n                            progress.updatePercentage();\n                        }\n\n                        // delete temp file\n                        targetFile.delete();\n\n                        if (copyCordovaAssets) {\n                            copyCordovaAssets(outputDirectory);\n                        }\n\n                        if (win) {\n                            // success, remove backup\n                            removeFolder(backup);\n                        } else {\n                            // failure, revert backup\n                            removeFolder(dir);\n                            backup.renameTo(dir);\n                        }\n                    } else {\n                        return;\n                    }\n                }\n\n                // complete\n                synchronized (activeRequests) {\n                    activeRequests.remove(id);\n                }\n\n                // Send last progress event\n                progress.setStatus(STATUS_COMPLETE);\n                updateProgress(callbackContext, progress);\n\n                // Send completion message\n                try {\n                    JSONObject result = new JSONObject();\n                    result.put(PROP_LOCAL_PATH, outputDirectory);\n\n                    if (dir.list() != null) {\n                        Log.d(LOG_TAG, "size of output dir = " + dir.list().length);\n                    }\n                    boolean cached = false;\n                    if (type.equals(TYPE_LOCAL) && dir.exists() && dir.isDirectory() && dir.list() != null && dir.list().length > 0) {\n                        Log.d(LOG_TAG, "we have a dir with some files in it.");\n                        cached = true;\n                    }\n\n                    result.put(PROP_CACHED, cached);\n                    callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));\n                } catch (JSONException e) {\n                    // never happens\n                }\n            }\n        });\n    }\n\n    private void savePrefs() {\n        SharedPreferences.Editor editor = cordova.getActivity().getSharedPreferences(cordova.getActivity().getPackageName(), 0).edit();\n        editor.putInt(PREVIOUS_VERSION, getCurrentAppVersion());\n        editor.commit();\n    }\n\n    private boolean hasAppBeenUpdated() {\n        Activity activity = cordova.getActivity();\n\n        int currentAppVersion = -1;\n\n        SharedPreferences settings = activity.getSharedPreferences(activity.getPackageName(), 0);\n        int previousAppVersion = settings.getInt(PREVIOUS_VERSION, -1);\n\n        currentAppVersion = getCurrentAppVersion();\n\n        Log.d(LOG_TAG, "current = " + currentAppVersion);\n        Log.d(LOG_TAG, "previous = " + previousAppVersion);\n\n        return currentAppVersion > previousAppVersion ? true : false;\n    }\n\n    private int getCurrentAppVersion() {\n        Activity activity = cordova.getActivity();\n        int currentAppVersion = -1;\n        try {\n            currentAppVersion = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode;\n        } catch (PackageManager.NameNotFoundException e) {\n            // ignore\n        }\n        return currentAppVersion;\n    }\n\n    private boolean isZipFile(File targetFile) {\n        boolean success = true;\n        try {\n            ZipFile zip = new ZipFile(targetFile);\n            Log.d(LOG_TAG, "seems like a zip file");\n        return a | b << 8 | c << 16 | d << 24;\n    }\n\n    private boolean unzipSync(File targetFile, String outputDirectory, CallbackContext callbackContext) {\n    \tLog.d(LOG_TAG, "unzipSync called");\n    \tLog.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());\n        InputStream inputStream = null;\n        ZipFile zip = null;\n        boolean anyEntries = false;\n        try {\n        \tzip = new ZipFile(targetFile);\n\n            // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://\n            // Accept a path or a URI for the source zip.\n    private File createDownloadFileLocation(final String id) {\n        File file = null;\n        try {\n            String tempId = (id.lastIndexOf("/") > -1) ? id.substring(id.lastIndexOf("/")+1, id.length()) : id;\n\n            File tempFile = resourceApi.mapUriToFile(zipUri);\n            if (tempFile == null || !tempFile.exists()) {\n                String errorMessage = "Zip file does not exist";\n                callbackContext.error(errorMessage);\n                Log.e(LOG_TAG, errorMessage);\n                return false;\n            }\n\n    private File backupExistingDirectory(String outputDirectory, String type, File dir) {\n            outputDirectory = outputDir.getAbsolutePath();\n            outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n            if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){\n                String errorMessage = "Could not create output directory";\n                callbackContext.error(errorMessage);\n                Log.e(LOG_TAG, errorMessage);\n                return false;\n            }\n\n                }\n            } else {\n                dir.renameTo(backup);\n            }\n        }\n        return backup;\n    }\n\n    private void copyRootApp(String outputDirectory, String manifestFile) {\n        boolean wwwExists = (new File(outputDirectory, "www")).exists();\n        boolean copied = false;\n        if (manifestFile != null && !"".equals(manifestFile)) {\n            Log.d(LOG_TAG, "Manifest copy");\n            try {\n                copyRootAppByManifest(outputDirectory, manifestFile, wwwExists);\n                copied = true;\n            } catch (Exception e) {\n                Log.e(LOG_TAG, e.getLocalizedMessage(), e);\n            }\n        }\n        if (!copied) {\n            Log.d(LOG_TAG, "Long copy");\n            try {\n                copyAssetFileOrDir(outputDirectory, "www", wwwExists);\n            } catch (IOException e) {\n                Log.e(LOG_TAG, e.getLocalizedMessage(), e);\n            }\n        }\n    }\n\n    private void copyRootAppByManifest(String outputDirectory, String manifestFile, boolean wwwExists) throws IOException, JSONException {\n        File fp = new File(outputDirectory);\n            ZipEntry ze;\n            byte[] buffer = new byte[32 * 1024];\n\n            while ((ze = zis.getNextEntry()) != null)\n            {\n                anyEntries = true;\n                String compressedName = ze.getName();\n\n        }\n    }\n\n    private void copyCordovaAssets(String outputDirectory) {\n        try {\n            boolean wwwExists = (new File(outputDirectory, "www")).exists();\n\n            // cordova.js\n            this.copyAssetFile(outputDirectory, "www/cordova.js", wwwExists);\n\n            // cordova_plugins.js\n            this.copyAssetFile(outputDirectory, "www/cordova_plugins.js", wwwExists);\n\n            // plugins folder\n            this.copyAssetFileOrDir(outputDirectory, "www/plugins", wwwExists);\n        } catch(IOException e) {\n            Log.e(LOG_TAG, "Failed to copy asset file", e);\n        }\n    }\n\n    private void copyAssetFileOrDir(String outputDirectory, String path, boolean wwwExists) throws IOException {\n        if (path.contains(".")) {\n            try {\n                updateProgress(callbackContext, progress);\n                zis.closeEntry();\n            }\n\n            // final progress = 100%\n//            progress.setLoaded(progress.getTotal());\n//            updateProgress(callbackContext, progress);\n        } catch (Exception e) {\n            String errorMessage = "An error occurred while unzipping.";\n            callbackContext.error(errorMessage);\n            Log.e(LOG_TAG, errorMessage, e);\n        } finally {\n            if (inputStream != null) {\n    private void copyAssetDir(String outputDirectory, String path, boolean wwwExists) throws IOException {\n        String assets[] = cordova.getActivity().getAssets().list(path);\n        if (assets.length != 0) {\n            for (String file : assets) {\n                copyAssetFileOrDir(outputDirectory, path + File.separator + file, wwwExists);\n            }\n        } else {\n            this.copyAssetFile(outputDirectory, path, wwwExists);\n        }\n    }\n\n    private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {\n        String encoding = conn.getContentEncoding();\n        if (encoding != null && encoding.equalsIgnoreCase("gzip")) {\n            return new TrackingGZIPInputStream(new ExposedGZIPInputStream(conn.getInputStream()));\n        }\n        return new SimpleTrackingInputStream(conn.getInputStream());\n    }\n\n    private static void safeClose(Closeable stream) {\n        if (stream != null) {\n            try {\n                stream.close();\n            } catch (IOException e) {\n            }\n        }\n    }\n\n    private String getCookies(final String target) {\n        boolean gotCookie = false;\n        String cookie = null;\n        Class webViewClass = webView.getClass();\n        try {\n            Method gcmMethod = webViewClass.getMethod("getCookieManager");\n            Class iccmClass  = gcmMethod.getReturnType();\n            Method gcMethod  = iccmClass.getMethod("getCookie", String.class);\n\n            cookie = (String)gcMethod.invoke(\n                    iccmClass.cast(\n                            gcmMethod.invoke(webView)\n                    ), target);\n\n            gotCookie = true;\n        } catch (NoSuchMethodException e) {\n        } catch (IllegalAccessException e) {\n        } catch (InvocationTargetException e) {\n        } catch (ClassCastException e) {\n        }\n\n        if (!gotCookie) {\n            cookie = CookieManager.getInstance().getCookie(target);\n        }\n\n        return cookie;\n    }\n\n    private static void addHeadersToRequest(URLConnection connection, JSONObject headers) {\n        try {\n            for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {\n                String headerKey = iter.next().toString();\n                JSONArray headerValues = headers.optJSONArray(headerKey);\n                if (headerValues == null) {\n                    headerValues = new JSONArray();\n                    headerValues.put(headers.getString(headerKey));\n                }\n                connection.setRequestProperty(headerKey, headerValues.getString(0));\n                for (int i = 1; i < headerValues.length(); ++i) {\n                    connection.addRequestProperty(headerKey, headerValues.getString(i));\n                }\n            }\n        } catch (JSONException e1) {\n            // No headers to be manipulated!\n        }\n    }\n\n    /* Unzip code */\n\n    // Can\'t use DataInputStream because it has the wrong endian-ness.\n    private static int readInt(InputStream is) throws IOException {\n        int a = is.read();\n        int b = is.read();\n        int c = is.read();\n        int d = is.read();\n        return a | b << 8 | c << 16 | d << 24;\n    }\n\n    private boolean unzipSync(File targetFile, String outputDirectory, ProgressEvent progress, CallbackContext callbackContext) {\n        Log.d(LOG_TAG, "unzipSync called");\n        Log.d(LOG_TAG, "zip = " + targetFile.getAbsolutePath());\n        InputStream inputStream = null;\n        ZipFile zip = null;\n        boolean anyEntries = false;\n        try {\n            synchronized (progress) {\n                if (progress.isAborted()) {\n                    return false;\n                }\n            }\n\n            zip = new ZipFile(targetFile);\n\n            // Since Cordova 3.3.0 and release of File plugins, files are accessed via cdvfile://\n            // Accept a path or a URI for the source zip.\n            Uri zipUri = getUriForArg(targetFile.getAbsolutePath());\n            Uri outputUri = getUriForArg(outputDirectory);\n\n            CordovaResourceApi resourceApi = webView.getResourceApi();\n\n            File tempFile = resourceApi.mapUriToFile(zipUri);\n            if (tempFile == null || !tempFile.exists()) {\n                sendErrorMessage("Zip file does not exist", UNZIP_ERROR, callbackContext);\n            }\n\n            File outputDir = resourceApi.mapUriToFile(outputUri);\n            outputDirectory = outputDir.getAbsolutePath();\n            outputDirectory += outputDirectory.endsWith(File.separator) ? "" : File.separator;\n            if (outputDir == null || (!outputDir.exists() && !outputDir.mkdirs())){\n                sendErrorMessage("Could not create output directory", UNZIP_ERROR, callbackContext);\n            }\n\n            OpenForReadResult zipFile = resourceApi.openForRead(zipUri);\n            progress.setStatus(STATUS_EXTRACTING);\n            progress.setLoaded(0);\n            progress.setTotal(zip.size());\n            Log.d(LOG_TAG, "zip file len = " + zip.size());\n\n            inputStream = new BufferedInputStream(zipFile.inputStream);\n            inputStream.mark(10);\n            int magic = readInt(inputStream);\n\n            if (magic != 875721283) { // CRX identifier\n                inputStream.reset();\n            } else {\n                // CRX files contain a header. This header consists of:\n                //  * 4 bytes of magic number\n                //  * 4 bytes of CRX format version,\n                //  * 4 bytes of public key length\n                //  * 4 bytes of signature length\n                //  * the public key\n                //  * the signature\n                // and then the ordinary zip data follows. We skip over the header before creating the ZipInputStream.\n                readInt(inputStream); // version == 2.\n                int pubkeyLength = readInt(inputStream);\n                int signatureLength = readInt(inputStream);\n\n                inputStream.skip(pubkeyLength + signatureLength);\n            }\n\n            // The inputstream is now pointing at the start of the actual zip file content.\n            ZipInputStream zis = new ZipInputStream(inputStream);\n            inputStream = zis;\n\n            ZipEntry ze;\n            byte[] buffer = new byte[32 * 1024];\n\n            while ((ze = zis.getNextEntry()) != null) {\n                synchronized (progress) {\n                    if (progress.isAborted()) {\n                        return false;\n                    }\n                }\n\n                anyEntries = true;\n                String compressedName = ze.getName();\n\n                if (ze.getSize() > getFreeSpace()) {\n                    return false;\n                }\n\n                if (ze.isDirectory()) {\n                    File dir = new File(outputDirectory + compressedName);\n                    dir.mkdirs();\n                } else {\n                    File file = new File(outputDirectory + compressedName);\n                    file.getParentFile().mkdirs();\n                    if(file.exists() || file.createNewFile()){\n                        Log.w(LOG_TAG, "extracting: " + file.getPath());\n                        FileOutputStream fout = new FileOutputStream(file);\n                        int count;\n                        while ((count = zis.read(buffer)) != -1)\n                        {\n                            fout.write(buffer, 0, count);\n                        }\n                        fout.close();\n                    }\n\n                }\n                progress.addLoaded(1);\n                updateProgress(callbackContext, progress);\n                zis.closeEntry();\n            }\n        } catch (Exception e) {\n            String errorMessage = "An error occurred while unzipping.";\n            sendErrorMessage(errorMessage, UNZIP_ERROR, callbackContext);\n            Log.e(LOG_TAG, errorMessage, e);\n        } finally {\n            if (inputStream != null) {\n                try {\n                    inputStream.close();\n                } catch (IOException e) {\n                }\n            }\n            if (zip != null) {\n                try {\n                    zip.close();\n                } catch (IOException e) {\n                }\n            }\n        }\n\n        if (anyEntries)\n            return true;\n        else\n            return false;\n    }\n\n    private void updateProgress(CallbackContext callbackContext, ProgressEvent progress) {\n        try {\n            if (progress.getLoaded() != progress.getTotal() || progress.getStatus() == STATUS_COMPLETE) {\n                PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());\n                pluginResult.setKeepCallback(true);\n                callbackContext.sendPluginResult(pluginResult);\n            }\n        } catch (JSONException e) {\n            // never happens\n        }\n    }\n\n    private Uri getUriForArg(String arg) {\n        Uri tmpTarget = Uri.parse(arg);\n        return webView.getResourceApi().remapUri(\n                tmpTarget.getScheme() != null ? tmpTarget : Uri.fromFile(new File(arg)));\n    }\n\n    private static class ProgressEvent {\n        private long loaded;\n        private long total;\n        private double percentage;\n        private int status;\n        private boolean aborted;\n        private File targetFile;\n        public ProgressEvent() {\n            this.status = STATUS_STOPPED;\n        }\n        public long getLoaded() {\n            return loaded;\n        }\n        public void setLoaded(long loaded) {\n            this.loaded = loaded;\n            updatePercentage();\n        }\n        public void addLoaded(long add) {\n            this.loaded += add;\n            updatePercentage();\n        }\n        public long getTotal() {\n            return total;\n        }\n        public void setTotal(long total) {\n            this.total = total;\n            updatePercentage();\n        }\n        public int getStatus() {\n            return status;\n        }\n        public void setStatus(int status) {\n            this.status = status;\n        }\n        public boolean isAborted() {\n            return aborted;\n        }\n        public void setAborted(boolean aborted) {\n            this.aborted = aborted;\n        }\n        public File getTargetFile() {\n            return targetFile;\n        }\n        public void setTargetFile(File targetFile) {\n            this.targetFile = targetFile;\n        }\n        public JSONObject toJSONObject() throws JSONException {\n            JSONObject jsonProgress = new JSONObject();\n            jsonProgress.put(PROP_PROGRESS, this.percentage);\n            jsonProgress.put(PROP_STATUS, this.getStatus());\n            jsonProgress.put(PROP_LOADED, this.getLoaded());\n            jsonProgress.put(PROP_TOTAL, this.getTotal());\n            return jsonProgress;\n\n        }\n        private void updatePercentage() {\n            double loaded = this.getLoaded();\n            double total = this.getTotal();\n            this.percentage = Math.floor((loaded / total * 100) / 2);\n            if (this.getStatus() == STATUS_EXTRACTING) {\n                this.percentage += 50;\n            }\n        }\n    }\n\n    private void copyFolder(File src, File dest) throws IOException{\n        if(src.isDirectory()) {\n            if(!dest.exists()){\n                dest.mkdir();\n            }\n\n            //list all the directory contents\n            String files[] = src.list();\n\n            for (String file : files) {\n                //recursive copy\n                copyFolder(new File(src, file), new File(dest, file));\n            }\n\n        } else {\n            //if file, then copy it\n            copyFile(new FileInputStream(src), new FileOutputStream(dest));\n        }\n    }\n\n    private void copyAssetFile(String outputDirectory, String filename, boolean wwwExists) throws IOException {\n        String targetFile = filename;\n        if (!wwwExists) {\n            if (targetFile.startsWith("www/")) {\n                targetFile = targetFile.substring(4,targetFile.length());\n            }\n        }\n\n        int lastIndex = targetFile.lastIndexOf("/");\n        if (lastIndex > 0) {\n            File targetDir = new File(outputDirectory + "/" + targetFile.substring(0, lastIndex));\n            if (!targetDir.exists()) {\n                targetDir.mkdirs();\n            }\n        } else {\n            File targetDir = new File(outputDirectory);\n            if (!targetDir.exists()) {\n                targetDir.mkdirs();\n            }\n        }\n\n        copyFile(cordova.getActivity().getAssets().open(filename), new FileOutputStream(new File(outputDirectory, targetFile)));\n    }\n\n    private void copyFile(InputStream in, OutputStream out) throws IOException {\n        byte[] buffer = new byte[4096];\n\n        int length;\n        //copy the file content in bytes\n        while ((length = in.read(buffer)) > 0){\n            out.write(buffer, 0, length);\n        }\n\n        in.close();\n        out.close();\n    }\n\n    private void removeFolder(File directory) {\n        if (directory.exists() && directory.isDirectory()) {\n            File[] files = directory.listFiles();\n            if (files != null) {\n                for (File file : files) {\n                    removeFolder(file);\n                }\n            }\n        }\n        directory.delete();\n    }\n}\n', 'message': 'Implement cancel and multiple download\n\nAble to cancel download\nClean up error handling\nCancel during unzip\nformatting\n', 'author_name': 'Simon MacDonald', 'lang': 'java'}