website.openeng.async.Connection.java Source code

Java tutorial

Introduction

Here is the source code for website.openeng.async.Connection.java

Source

/****************************************************************************************
 * Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com>                                   *
 * Copyright (c) 2011 Kostas Spyropoulos <inigo.aldana@gmail.com>                       *
 * Copyright (c) 2012 Norbert Nagold <norbert.nagold@gmail.com>                         *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 3 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

package website.openeng.async;

import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;

import website.openeng.anki.KanjiDroidApp;
import website.openeng.anki.CollectionHelper;
import website.openeng.anki.R;
import website.openeng.anki.exception.UnknownHttpResponseException;
import website.openeng.anki.exception.UnsupportedSyncException;
import website.openeng.libanki.Collection;
import website.openeng.libanki.Decks;
import website.openeng.libanki.sync.FullSyncer;
import website.openeng.libanki.sync.HttpSyncer;
import website.openeng.libanki.sync.MediaSyncer;
import website.openeng.libanki.sync.RemoteMediaServer;
import website.openeng.libanki.sync.RemoteServer;
import website.openeng.libanki.sync.Syncer;

import org.apache.http.HttpResponse;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;

import timber.log.Timber;

public class Connection extends BaseAsyncTask<Connection.Payload, Object, Connection.Payload> {

    public static final int TASK_TYPE_LOGIN = 0;
    public static final int TASK_TYPE_SYNC = 1;
    public static final int TASK_TYPE_DOWNLOAD_MEDIA = 5;
    public static final int TASK_TYPE_REGISTER = 6;
    public static final int TASK_TYPE_UPGRADE_DECKS = 7;
    public static final int CONN_TIMEOUT = 30000;

    private static Connection sInstance;
    private TaskListener mListener;
    private CancelCallback mCancelCallback;

    private static Connection launchConnectionTask(TaskListener listener, Payload data) {

        if (!isOnline()) {
            data.success = false;
            listener.onDisconnected();
            return null;
        }

        try {
            if ((sInstance != null) && (sInstance.getStatus() != AsyncTask.Status.FINISHED)) {
                sInstance.get();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        sInstance = new Connection();
        sInstance.mListener = listener;

        sInstance.execute(data);
        return sInstance;
    }

    /*
     * Runs on GUI thread
     */
    @Override
    protected void onCancelled() {
        super.onCancelled();
        if (mCancelCallback != null) {
            mCancelCallback.cancelAllConnections();
        }
        if (mListener instanceof CancellableTaskListener) {
            ((CancellableTaskListener) mListener).onCancelled();
        }
    }

    /*
     * Runs on GUI thread
     */
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (mListener != null) {
            mListener.onPreExecute();
        }
    }

    /*
     * Runs on GUI thread
     */
    @Override
    protected void onPostExecute(Payload data) {
        super.onPostExecute(data);
        if (mListener != null) {
            mListener.onPostExecute(data);
        }
    }

    /*
     * Runs on GUI thread
     */
    @Override
    protected void onProgressUpdate(Object... values) {
        super.onProgressUpdate(values);
        if (mListener != null) {
            mListener.onProgressUpdate(values);
        }
    }

    public static Connection login(TaskListener listener, Payload data) {
        data.taskType = TASK_TYPE_LOGIN;
        return launchConnectionTask(listener, data);
    }

    public static Connection register(TaskListener listener, Payload data) {
        data.taskType = TASK_TYPE_REGISTER;
        return launchConnectionTask(listener, data);
    }

    public static Connection sync(TaskListener listener, Payload data) {
        data.taskType = TASK_TYPE_SYNC;
        return launchConnectionTask(listener, data);
    }

    @Override
    protected Payload doInBackground(Payload... params) {
        super.doInBackground(params);
        if (params.length != 1) {
            throw new IllegalArgumentException();
        }
        return doOneInBackground(params[0]);
    }

    private Payload doOneInBackground(Payload data) {
        switch (data.taskType) {
        case TASK_TYPE_LOGIN:
            return doInBackgroundLogin(data);

        case TASK_TYPE_REGISTER:
            return doInBackgroundRegister(data);

        case TASK_TYPE_SYNC:
            return doInBackgroundSync(data);

        case TASK_TYPE_DOWNLOAD_MEDIA:
            return doInBackgroundDownloadMissingMedia(data);

        case TASK_TYPE_UPGRADE_DECKS:
            throw new RuntimeException("Upgrade decks no longer supported");

        default:
            return null;
        }
    }

    private Payload doInBackgroundLogin(Payload data) {
        String username = (String) data.data[0];
        String password = (String) data.data[1];
        HttpSyncer server = new RemoteServer(this, null);
        HttpResponse ret;
        try {
            ret = server.hostKey(username, password);
        } catch (UnknownHttpResponseException e) {
            data.success = false;
            data.result = new Object[] { "error", e.getResponseCode(), e.getMessage() };
            return data;
        } catch (Exception e2) {
            // Ask user to report all bugs which aren't timeout errors
            if (!timeoutOccured(e2)) {
                KanjiDroidApp.sendExceptionReport(e2, "doInBackgroundLogin");
            }
            data.success = false;
            data.result = new Object[] { "connectionError" };
            return data;
        }
        String hostkey = null;
        boolean valid = false;
        if (ret != null) {
            data.returnType = ret.getStatusLine().getStatusCode();
            Timber.d("doInBackgroundLogin - response from server: %d, (%s)", data.returnType,
                    ret.getStatusLine().getReasonPhrase());
            if (data.returnType == 200) {
                try {
                    JSONObject jo = (new JSONObject(server.stream2String(ret.getEntity().getContent())));
                    hostkey = jo.getString("key");
                    valid = (hostkey != null) && (hostkey.length() > 0);
                } catch (JSONException e) {
                    valid = false;
                } catch (IllegalStateException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            Timber.e("doInBackgroundLogin - empty response from server");
        }
        if (valid) {
            data.success = true;
            data.data = new String[] { username, hostkey };
        } else {
            data.success = false;
        }
        return data;
    }

    private Payload doInBackgroundRegister(Payload data) {
        String username = (String) data.data[0];
        String password = (String) data.data[1];
        HttpSyncer server = new RemoteServer(this, null);
        HttpResponse ret;
        try {
            ret = server.register(username, password);
        } catch (UnknownHttpResponseException e) {
            data.success = false;
            data.result = new Object[] { "error", e.getResponseCode(), e.getMessage() };
            return data;
        }
        String hostkey = null;
        boolean valid = false;
        String status = null;
        if (ret != null) {
            data.returnType = ret.getStatusLine().getStatusCode();
            if (data.returnType == 200) {
                try {
                    JSONObject jo = (new JSONObject(server.stream2String(ret.getEntity().getContent())));
                    status = jo.getString("status");
                    if (status.equals("ok")) {
                        hostkey = jo.getString("hkey");
                        valid = (hostkey != null) && (hostkey.length() > 0);
                    }
                } catch (JSONException e) {
                } catch (IllegalStateException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        if (valid) {
            data.success = true;
            data.data = new String[] { username, hostkey };
        } else {
            data.success = false;
            data.data = new String[] { status != null ? status
                    : KanjiDroidApp.getAppResources().getString(R.string.connection_error_message) };
        }
        return data;
    }

    private boolean timeoutOccured(Exception e) {
        String msg = e.getMessage();
        return msg.contains("UnknownHostException") || msg.contains("HttpHostConnectException")
                || msg.contains("SSLException while building HttpClient") || msg.contains("SocketTimeoutException")
                || msg.contains("ClientProtocolException") || msg.contains("TimeoutException");
    }

    private Payload doInBackgroundSync(Payload data) {
        // for for doInBackgroundLoadDeckCounts if any
        Timber.d("doInBackgroundSync()");
        // Block execution until any previous background task finishes, or timeout after 5s
        boolean ok = DeckTask.waitToFinish(5);

        String hkey = (String) data.data[0];
        boolean media = (Boolean) data.data[1];
        String conflictResolution = (String) data.data[2];
        Collection col = data.col;

        boolean colCorruptFullSync = false;
        if (!CollectionHelper.getInstance().colIsOpen() || !ok) {
            if (conflictResolution != null && conflictResolution.equals("download")) {
                colCorruptFullSync = true;
            } else {
                data.success = false;
                data.result = new Object[] { "genericError" };
                return data;
            }
        }
        try {
            CollectionHelper.getInstance().lockCollection();
            HttpSyncer server = new RemoteServer(this, hkey);
            Syncer client = new Syncer(col, server);

            // run sync and check state
            boolean noChanges = false;
            if (conflictResolution == null) {
                Timber.i("Sync - starting sync");
                publishProgress(R.string.sync_prepare_syncing);
                Object[] ret = client.sync(this);
                data.message = client.getSyncMsg();
                if (ret == null) {
                    data.success = false;
                    data.result = new Object[] { "genericError" };
                    return data;
                }
                String retCode = (String) ret[0];
                if (!retCode.equals("noChanges") && !retCode.equals("success")) {
                    data.success = false;
                    data.result = ret;
                    // Check if there was a sanity check error
                    if (retCode.equals("sanityCheckError")) {
                        // Force full sync next time
                        col.modSchemaNoCheck();
                        col.save();
                    }
                    return data;
                }
                // save and note success state
                if (retCode.equals("noChanges")) {
                    // publishProgress(R.string.sync_no_changes_message);
                    noChanges = true;
                } else {
                    // publishProgress(R.string.sync_database_acknowledge);
                }
            } else {
                try {
                    server = new FullSyncer(col, hkey, this);
                    if (conflictResolution.equals("upload")) {
                        Timber.i("Sync - fullsync - upload collection");
                        publishProgress(R.string.sync_preparing_full_sync_message);
                        Object[] ret = server.upload();
                        if (ret == null) {
                            data.success = false;
                            data.result = new Object[] { "genericError" };
                            CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed?
                            return data;
                        }
                        if (!ret[0].equals(HttpSyncer.ANKIWEB_STATUS_OK)) {
                            data.success = false;
                            data.result = ret;
                            CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed?
                            return data;
                        }
                    } else if (conflictResolution.equals("download")) {
                        Timber.i("Sync - fullsync - download collection");
                        publishProgress(R.string.sync_downloading_message);
                        Object[] ret = server.download();
                        if (ret == null) {
                            data.success = false;
                            data.result = new Object[] { "genericError" };
                            CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed?
                            return data;
                        }
                        if (!ret[0].equals("success")) {
                            data.success = false;
                            data.result = ret;
                            if (!colCorruptFullSync) {
                                CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed?
                            }
                            return data;
                        }
                    }
                    col = CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed?
                } catch (OutOfMemoryError e) {
                    KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-fullSync");
                    data.success = false;
                    data.result = new Object[] { "OutOfMemoryError" };
                    return data;
                } catch (RuntimeException e) {
                    if (timeoutOccured(e)) {
                        data.result = new Object[] { "connectionError" };
                    } else {
                        KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-fullSync");
                        data.result = new Object[] { "IOException" };
                    }
                    data.success = false;
                    return data;
                }
            }

            // clear undo to avoid non syncing orphans (because undo resets usn too
            if (!noChanges) {
                col.clearUndo();
            }
            // then move on to media sync
            boolean noMediaChanges = false;
            String mediaError = null;
            if (media) {
                server = new RemoteMediaServer(col, hkey, this);
                MediaSyncer mediaClient = new MediaSyncer(col, (RemoteMediaServer) server, this);
                String ret;
                try {
                    ret = mediaClient.sync();
                    if (ret == null) {
                        mediaError = KanjiDroidApp.getAppResources().getString(R.string.sync_media_error);
                    } else {
                        if (ret.equals("noChanges")) {
                            publishProgress(R.string.sync_media_no_changes);
                            noMediaChanges = true;
                        }
                        if (ret.equals("sanityFailed")) {
                            mediaError = KanjiDroidApp.getAppResources()
                                    .getString(R.string.sync_media_sanity_failed);
                        } else {
                            publishProgress(R.string.sync_media_success);
                        }
                    }
                } catch (UnsupportedSyncException e) {
                    mediaError = KanjiDroidApp.getAppResources().getString(R.string.sync_media_unsupported);
                    KanjiDroidApp.getSharedPrefs(KanjiDroidApp.getInstance().getApplicationContext()).edit()
                            .putBoolean("syncFetchesMedia", false).commit();
                    KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-mediaSync");
                } catch (RuntimeException e) {
                    if (timeoutOccured(e)) {
                        data.result = new Object[] { "connectionError" };
                    } else {
                        KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-mediaSync");
                    }
                    mediaError = e.getLocalizedMessage();
                }
            }
            if (noChanges && noMediaChanges) {
                data.success = false;
                data.result = new Object[] { "noChanges" };
                return data;
            } else {
                data.success = true;
                data.data = new Object[] { conflictResolution, col, mediaError };
                return data;
            }
        } catch (UnknownHttpResponseException e) {
            Timber.e("doInBackgroundSync -- unknown response code error");
            e.printStackTrace();
            data.success = false;
            Integer code = e.getResponseCode();
            String msg = e.getLocalizedMessage();
            data.result = new Object[] { "error", code, msg };
            return data;
        } catch (Exception e) {
            // Global error catcher.
            // Try to give a human readable error, otherwise print the raw error message
            Timber.e("doInBackgroundSync error");
            e.printStackTrace();
            data.success = false;
            if (timeoutOccured(e)) {
                data.result = new Object[] { "connectionError" };
            } else {
                KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync");
                data.result = new Object[] { e.getLocalizedMessage() };
            }
            return data;
        } finally {
            // Close collection to roll back any sync failures and
            Timber.d("doInBackgroundSync -- closing collection on outer finally statement");
            col.close(false);
            CollectionHelper.getInstance().unlockCollection();
            Timber.d("doInBackgroundSync -- reopening collection on outer finally statement");
            CollectionHelper.getInstance().reopenCollection();
        }

    }

    public void publishProgress(int id) {
        super.publishProgress(id);
    }

    public void publishProgress(String message) {
        super.publishProgress(message);
    }

    public void publishProgress(int id, long up, long down) {
        super.publishProgress(id, up, down);
    }

    /**
     * Downloads any missing media files according to the mediaURL deckvar.
     *
     * @param data
     * @return The return type contains data.resultType and an array of Integer in data.data. data.data[0] is the number
     *         of total missing media, data.data[1] is the number of downloaded ones.
     */
    private Payload doInBackgroundDownloadMissingMedia(Payload data) {
        Timber.i("DownloadMissingMedia");
        HashMap<String, String> missingPaths = new HashMap<String, String>();
        HashMap<String, String> missingSums = new HashMap<String, String>();

        data.result = (Decks) data.data[0]; // pass it to the return object so we close the deck in the deck picker
        String syncName = "";// deck.getDeckName();

        data.success = false;
        data.data = new Object[] { 0, 0, 0 };
        // if (!deck.hasKey("mediaURL")) {
        // data.success = true;
        // return data;
        // }
        String urlbase = "";// deck.getVar("mediaURL");
        if (urlbase.equals("")) {
            data.success = true;
            return data;
        }

        String mdir = "";// deck.mediaDir(true);
        int totalMissing = 0;
        int missing = 0;
        int grabbed = 0;

        Cursor cursor = null;
        try {
            cursor = null;// deck.getDB().getDatabase().rawQuery("SELECT filename, originalPath FROM media", null);
            String path = null;
            String f = null;
            while (cursor.moveToNext()) {
                f = cursor.getString(0);
                path = mdir + "/" + f;
                File file = new File(path);
                if (!file.exists()) {
                    missingPaths.put(f, path);
                    missingSums.put(f, cursor.getString(1));
                    Timber.d("Missing file: %s", f);
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        totalMissing = missingPaths.size();
        data.data[0] = totalMissing;
        if (totalMissing == 0) {
            data.success = true;
            return data;
        }
        publishProgress(Boolean.FALSE, totalMissing, 0, syncName);

        URL url = null;
        HttpURLConnection connection = null;
        String path = null;
        String sum = null;
        int readbytes = 0;
        byte[] buf = new byte[4096];
        for (String file : missingPaths.keySet()) {

            try {
                android.net.Uri uri = android.net.Uri.parse(Uri.encode(urlbase, ":/@%") + Uri.encode(file));
                url = new URI(uri.toString()).toURL();
                connection = (HttpURLConnection) url.openConnection();
                connection.connect();
                if (connection.getResponseCode() == 200) {
                    path = missingPaths.get(file);
                    InputStream is = connection.getInputStream();
                    BufferedInputStream bis = new BufferedInputStream(is, 4096);
                    FileOutputStream fos = new FileOutputStream(path);
                    while ((readbytes = bis.read(buf, 0, 4096)) != -1) {
                        fos.write(buf, 0, readbytes);
                        Timber.d("Downloaded %d file: %s", readbytes, path);
                    }
                    fos.close();

                    // Verify with checksum
                    sum = missingSums.get(file);
                    if (true) {// sum.equals("") || sum.equals(Utils.fileChecksum(path))) {
                        grabbed++;
                    } else {
                        // Download corrupted, delete file
                        Timber.i("Downloaded media file %s failed checksum.", path);
                        File f = new File(path);
                        f.delete();
                        missing++;
                    }
                } else {
                    Timber.e("Connection error (" + connection.getResponseCode() + ") while retrieving media file "
                            + urlbase + file);
                    Timber.e("Connection message: " + connection.getResponseMessage());
                    if (missingSums.get(file).equals("")) {
                        // Ignore and keep going
                        missing++;
                    } else {
                        data.success = false;
                        data.data = new Object[] { file };
                        return data;
                    }
                }
                connection.disconnect();
            } catch (URISyntaxException e) {
                Timber.e(e, "doInBackgroundDownloadMissingMedia URISyntaxException");
            } catch (MalformedURLException e) {
                Timber.e(e, "MalformedURLException while download media file " + path);
                if (missingSums.get(file).equals("")) {
                    // Ignore and keep going
                    missing++;
                } else {
                    data.success = false;
                    data.data = new Object[] { file };
                    return data;
                }
            } catch (IOException e) {
                Timber.e(e, "IOException while download media file " + path);
                if (missingSums.get(file).equals("")) {
                    // Ignore and keep going
                    missing++;
                } else {
                    data.success = false;
                    data.data = new Object[] { file };
                    return data;
                }
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
            publishProgress(Boolean.TRUE, totalMissing, grabbed + missing, syncName);
        }

        data.data[1] = grabbed;
        data.data[2] = missing;
        data.success = true;
        return data;
    }

    public static boolean isOnline() {
        ConnectivityManager cm = (ConnectivityManager) KanjiDroidApp.getInstance().getApplicationContext()
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = cm.getActiveNetworkInfo();
        if (netInfo == null || !netInfo.isConnected() || !netInfo.isAvailable()) {
            return false;
        }
        return true;
    }

    public static interface TaskListener {
        public void onPreExecute();

        public void onProgressUpdate(Object... values);

        public void onPostExecute(Payload data);

        public void onDisconnected();
    }

    public static interface CancellableTaskListener extends TaskListener {
        public void onCancelled();
    }

    public static class Payload {
        public int taskType;
        public Object[] data;
        public Object result;
        public boolean success;
        public int returnType;
        public Exception exception;
        public String message;
        public Collection col;

        public Payload() {
            data = null;
            success = true;
        }

        public Payload(Object[] data, Collection col) {
            this.data = data;
            this.col = col;
            success = true;
        }

        public Payload(Object[] data) {
            this.data = data;
            success = true;
        }

        public Payload(int taskType, Object[] data) {
            this.taskType = taskType;
            this.data = data;
            success = true;
        }

        public Payload(int taskType, Object[] data, String path) {
            this.taskType = taskType;
            this.data = data;
            success = true;
        }
    }

    public class CancelCallback {
        private WeakReference<ThreadSafeClientConnManager> mConnectionManager = null;

        public void setConnectionManager(ThreadSafeClientConnManager connectionManager) {
            mConnectionManager = new WeakReference<ThreadSafeClientConnManager>(connectionManager);
        }

        public void cancelAllConnections() {
            if (mConnectionManager != null) {
                ThreadSafeClientConnManager connectionManager = mConnectionManager.get();
                if (connectionManager != null) {
                    connectionManager.shutdown();
                }
            }
        }
    }
}