com.ichi2.anki.AnkiDroidProxy.java Source code

Java tutorial

Introduction

Here is the source code for com.ichi2.anki.AnkiDroidProxy.java

Source

/***************************************************************************************
 * Copyright (c) 2009 Edu Zamora <edu.zasu@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 com.ichi2.anki;

import android.util.Log;

import com.google.gson.stream.JsonReader;
import com.ichi2.async.Connection.Payload;
import com.ichi2.utils.Base64;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.InflaterInputStream;

public class AnkiDroidProxy {

    // Sync protocol version
    public static final String SYNC_VERSION = "2";

    // The possible values for the status response from the AnkiWeb server.
    private static final String ANKIWEB_STATUS_OK = "OK";
    private static final String ANKIWEB_STATUS_INVALID_USER_PASS = "invalidUserPass";
    private static final String ANKIWEB_STATUS_OLD_VERSION = "oldVersion";
    private static final String ANKIWEB_STATUS_TOO_BUSY = "AnkiWeb is too busy right now. Please try again later.";
    /**
     * Connection settings
     */
    // ankiweb.net hosted at 78.46.104.19
    public static final String SYNC_HOST = "ankiweb.net";
    public static final String SYNC_URL = "http://" + SYNC_HOST + "/sync/";
    public static final String SYNC_SEARCH = "http://" + SYNC_HOST + "/file/search";

    /**
     * Synchronization.
     */
    public static final int LOGIN_ERROR = -1;
    public static final int LOGIN_OK = 0;
    public static final int LOGIN_INVALID_USER_PASS = 1;
    public static final int LOGIN_CLOCKS_UNSYNCED = 2;
    public static final int SYNC_CONFLICT_RESOLUTION = 3;
    public static final int LOGIN_OLD_VERSION = 4;
    /** The server is too busy to serve the request. */
    public static final int LOGIN_TOO_BUSY = 5;
    public static final int DB_ERROR = 6;

    /**
     * List to hold the shared decks
     */
    private static List<SharedDeck> sSharedDecks;

    private String mUsername;
    private String mPassword;
    private String mDeckName;

    private JSONObject mDecks;
    private double mTimestamp;
    private double mTimediff;

    public AnkiDroidProxy(String user, String password) {
        mUsername = user;
        mPassword = password;
        mDeckName = "";
        mDecks = null;
        mTimediff = 0.0;
    }

    public void setDeckName(String deckName) {
        mDeckName = deckName;
    }

    public double getTimestamp() {
        return mTimestamp;
    }

    public double getTimediff() {
        return mTimediff;
    }

    public int connect(boolean checkClocks) {
        if (mDecks == null) {
            String decksString = getDecks();
            try {
                JSONObject jsonDecks = new JSONObject(decksString);
                String status = jsonDecks.getString("status");
                if (ANKIWEB_STATUS_OK.equalsIgnoreCase(status)) {
                    mDecks = jsonDecks.getJSONObject("decks");
                    Log.i(AnkiDroidApp.TAG, "Server decks = " + mDecks.toString());
                    mTimestamp = jsonDecks.getDouble("timestamp");
                    mTimediff = Math.abs(mTimestamp - Utils.now());
                    Log.i(AnkiDroidApp.TAG, "Server timestamp = " + mTimestamp);
                    if (checkClocks && (mTimediff > 300)) {
                        Log.e(AnkiDroidApp.TAG,
                                "connect - The clock of the device and that of the server are unsynchronized!");
                        return LOGIN_CLOCKS_UNSYNCED;
                    }
                    return LOGIN_OK;
                } else if (ANKIWEB_STATUS_INVALID_USER_PASS.equalsIgnoreCase(status)) {
                    return LOGIN_INVALID_USER_PASS;
                } else if (ANKIWEB_STATUS_OLD_VERSION.equalsIgnoreCase(status)) {
                    return LOGIN_OLD_VERSION;
                } else if (ANKIWEB_STATUS_TOO_BUSY.equalsIgnoreCase(status)) {
                    return LOGIN_TOO_BUSY;
                } else {
                    Log.e(AnkiDroidApp.TAG, "connect - unexpected status: " + status);
                    return LOGIN_ERROR;
                }
            } catch (JSONException e) {
                Log.e(AnkiDroidApp.TAG, "connect - JSONException = " + e.getMessage());
                return LOGIN_ERROR;
            }
        }

        return LOGIN_OK;
    }

    /**
     * Returns true if the server has the given deck.
     * <p>
     * It assumes connect() has already been called and will fail if it was not or the connection
     * was unsuccessful.
     *
     * @param name the name of the deck to look for
     * @return true if the server has the given deck, false otherwise
     */
    public boolean hasDeck(String name) {
        // We assume that gets have already been loading by doing a connect.
        if (mDecks == null)
            throw new IllegalStateException("Should have called connect first");
        @SuppressWarnings("unchecked")
        Iterator<String> decksIterator = (Iterator<String>) mDecks.keys();
        while (decksIterator.hasNext()) {
            String serverDeckName = decksIterator.next();
            if (name.equalsIgnoreCase(serverDeckName)) {
                return true;
            }
        }

        return false;
    }

    public double modified() {
        double lastModified = 0;

        // TODO: Why do we need to run connect?
        if (connect(false) != LOGIN_OK) {
            return -1.0;
        }
        try {
            JSONArray deckInfo = mDecks.getJSONArray(mDeckName);
            lastModified = deckInfo.getDouble(0);
        } catch (JSONException e) {
            Log.e(AnkiDroidApp.TAG, "modified - JSONException = " + e.getMessage());
            return -1.0;
        }

        return lastModified;
    }

    public double lastSync() {
        double lastSync = 0;

        // TODO: Why do we need to run connect?
        if (connect(false) != LOGIN_OK) {
            return -1.0;
        }
        try {
            JSONArray deckInfo = mDecks.getJSONArray(mDeckName);
            lastSync = deckInfo.getDouble(1);
        } catch (JSONException e) {
            Log.e(AnkiDroidApp.TAG, "lastSync - JSONException = " + e.getMessage());
            return -1.0;
        }
        return lastSync;
    }

    public boolean finish() {
        try {
            String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&u="
                    + URLEncoder.encode(mUsername, "UTF-8") + "&v=" + URLEncoder.encode(SYNC_VERSION, "UTF-8")
                    + "&d=" + URLEncoder.encode(mDeckName, "UTF-8");
            HttpPost httpPost = new HttpPost(SYNC_URL + "finish");
            StringEntity entity = new StringEntity(data);
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept-Encoding", "identity");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpResponse response = httpClient.execute(httpPost);
            HttpEntity entityResponse = response.getEntity();
            int respCode = response.getStatusLine().getStatusCode();
            if (respCode != 200) {
                Log.e(AnkiDroidApp.TAG, "AnkiDroidProxy.finish error: " + respCode + " "
                        + response.getStatusLine().getReasonPhrase());
                return false;
            }
            InputStream content = entityResponse.getContent();
            String contentString = Utils.convertStreamToString(new InflaterInputStream(content));
            Log.i(AnkiDroidApp.TAG, "finish: " + contentString);
            return true;
        } catch (UnsupportedEncodingException e) {
            Log.e(AnkiDroidApp.TAG, "UnsupportedEncodingException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return false;
        } catch (ClientProtocolException e) {
            Log.e(AnkiDroidApp.TAG, "ClientProtocolException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return false;
        } catch (IOException e) {
            Log.e(AnkiDroidApp.TAG, "IOException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return false;
        }
    }

    public String getDecks() {
        String decksServer = "{}";

        try {
            String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&client="
                    + URLEncoder.encode("ankidroid-" + AnkiDroidApp.getPkgVersion(), "UTF-8") + "&u="
                    + URLEncoder.encode(mUsername, "UTF-8") + "&v=" + URLEncoder.encode(SYNC_VERSION, "UTF-8")
                    + "&d=None&sources=" + URLEncoder.encode("[]", "UTF-8") + "&libanki="
                    + URLEncoder.encode(AnkiDroidApp.LIBANKI_VERSION, "UTF-8") + "&pversion=5";

            // Log.i(AnkiDroidApp.TAG, "Data json = " + data);
            HttpPost httpPost = new HttpPost(SYNC_URL + "getDecks");
            StringEntity entity = new StringEntity(data);
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept-Encoding", "identity");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpResponse response = httpClient.execute(httpPost);
            int respCode = response.getStatusLine().getStatusCode();
            if (respCode != 200) {
                Log.e(AnkiDroidApp.TAG,
                        "getDecks error: " + respCode + " " + response.getStatusLine().getReasonPhrase());
                return decksServer;
            }
            HttpEntity entityResponse = response.getEntity();
            InputStream content = entityResponse.getContent();
            decksServer = Utils.convertStreamToString(new InflaterInputStream(content));
            Log.i(AnkiDroidApp.TAG, "getDecks response = " + decksServer);

        } catch (UnsupportedEncodingException e) {
            Log.e(AnkiDroidApp.TAG, "getDecks - UnsupportedEncodingException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, "getDecks - " + Log.getStackTraceString(e));
        } catch (ClientProtocolException e) {
            Log.e(AnkiDroidApp.TAG, "getDecks - ClientProtocolException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, "getDecks - " + Log.getStackTraceString(e));
        } catch (IOException e) {
            Log.e(AnkiDroidApp.TAG, "getDecks - IOException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, "getDecks - " + Log.getStackTraceString(e));
        }

        return decksServer;
    }

    public List<String> getPersonalDecks() {
        ArrayList<String> personalDecks = new ArrayList<String>();
        @SuppressWarnings("unchecked")
        Iterator<String> decksIterator = (Iterator<String>) mDecks.keys();
        while (decksIterator.hasNext()) {
            personalDecks.add((String) decksIterator.next());
        }

        return personalDecks;
    }

    public Payload createDeck(String name) {
        Log.i(AnkiDroidApp.TAG, "createDeck");

        Payload result = new Payload();

        try {
            String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&u="
                    + URLEncoder.encode(mUsername, "UTF-8") + "&v=" + URLEncoder.encode(SYNC_VERSION, "UTF-8")
                    + "&d=None&name=" + URLEncoder.encode(name, "UTF-8");

            HttpPost httpPost = new HttpPost(SYNC_URL + "createDeck");
            StringEntity entity = new StringEntity(data);
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept-Encoding", "identity");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpResponse response = httpClient.execute(httpPost);
            int respCode = response.getStatusLine().getStatusCode();
            HttpEntity entityResponse = response.getEntity();
            InputStream content = entityResponse.getContent();
            if (respCode != 200) {
                String reason = response.getStatusLine().getReasonPhrase();
                Log.i(AnkiDroidApp.TAG, "Failed to create Deck: " + respCode + " " + reason);
                result.success = false;
                result.returnType = respCode;
                result.result = reason;
                return result;
            } else {
                Log.i(AnkiDroidApp.TAG,
                        "createDeck - response = " + Utils.convertStreamToString(new InflaterInputStream(content)));
                result.success = true;
                result.returnType = 200;
                // Add created deck to the list of decks on server
                mDecks.put(name, new JSONArray("[0,0]"));
                return result;
            }
        } catch (UnsupportedEncodingException e) {
            Log.e(AnkiDroidApp.TAG, "createDeck - UnsupportedEncodingException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            result.result = e.getMessage();
        } catch (ClientProtocolException e) {
            Log.e(AnkiDroidApp.TAG, "createDeck - ClientProtocolException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            result.result = e.getMessage();
        } catch (IOException e) {
            Log.e(AnkiDroidApp.TAG, "createDeck - IOException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            result.result = e.getMessage();
        } catch (JSONException e) {
            Log.e(AnkiDroidApp.TAG, "createDeck - JSONException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            result.result = e.getMessage();
        }
        result.success = false;
        result.returnType = -1;
        return result;
    }

    /**
     * Anki Desktop -> libanki/anki/sync.py, HttpSyncServerProxy - summary
     * 
     * @param lastSync
     */
    public JSONObject summary(double lastSync) {

        Log.i(AnkiDroidApp.TAG, "Summary Server");

        JSONObject summaryServer = new JSONObject();

        try {
            String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&u="
                    + URLEncoder.encode(mUsername, "UTF-8") + "&d=" + URLEncoder.encode(mDeckName, "UTF-8") + "&v="
                    + URLEncoder.encode(SYNC_VERSION, "UTF-8") + "&lastSync="
                    + URLEncoder.encode(
                            Base64.encodeBytes(
                                    Utils.compress(String.format(Utils.ENGLISH_LOCALE, "%f", lastSync).getBytes())),
                            "UTF-8")
                    + "&base64=" + URLEncoder.encode("true", "UTF-8");

            // Log.i(AnkiDroidApp.TAG, "Data json = " + data);
            HttpPost httpPost = new HttpPost(SYNC_URL + "summary");
            StringEntity entity = new StringEntity(data);
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept-Encoding", "identity");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpResponse response = httpClient.execute(httpPost);
            int respCode = response.getStatusLine().getStatusCode();
            if (respCode != 200) {
                Log.e(AnkiDroidApp.TAG, "Error getting server summary: " + respCode + " "
                        + response.getStatusLine().getReasonPhrase());
                return null;
            }
            HttpEntity entityResponse = response.getEntity();
            InputStream content = entityResponse.getContent();
            summaryServer = new JSONObject(Utils.convertStreamToString(new InflaterInputStream(content)));
            Log.i(AnkiDroidApp.TAG, "Summary server = ");
            Utils.printJSONObject(summaryServer);
            return summaryServer;
        } catch (UnsupportedEncodingException e) {
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
        } catch (ClientProtocolException e) {
            Log.e(AnkiDroidApp.TAG, "ClientProtocolException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
        } catch (IOException e) {
            Log.e(AnkiDroidApp.TAG, "IOException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
        } catch (JSONException e) {
            Log.e(AnkiDroidApp.TAG, "JSONException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
        } catch (OutOfMemoryError e) {
            Log.e(AnkiDroidApp.TAG, "OutOfMemoryError = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
        }
        return null;
    }

    /**
     * Anki Desktop -> libanki/anki/sync.py, HttpSyncServerProxy - applyPayload
     * 
     * @param payload
     * @throws JSONException 
     */
    public JSONObject applyPayload(JSONObject payload) throws JSONException {
        Log.i(AnkiDroidApp.TAG, "applyPayload");
        JSONObject payloadReply = new JSONObject();

        try {
            // FIXME: Try to do the connection without encoding the payload in Base 64
            String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&u="
                    + URLEncoder.encode(mUsername, "UTF-8") + "&v=" + URLEncoder.encode(SYNC_VERSION, "UTF-8")
                    + "&d=" + URLEncoder.encode(mDeckName, "UTF-8") + "&payload="
                    + URLEncoder.encode(Base64.encodeBytes(Utils.compress(payload.toString().getBytes())), "UTF-8")
                    + "&base64=" + URLEncoder.encode("true", "UTF-8");

            // Log.i(AnkiDroidApp.TAG, "Data json = " + data);
            HttpPost httpPost = new HttpPost(SYNC_URL + "applyPayload");
            StringEntity entity = new StringEntity(data);
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept-Encoding", "identity");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpResponse response = httpClient.execute(httpPost);
            int respCode = response.getStatusLine().getStatusCode();
            if (respCode != 200) {
                Log.e(AnkiDroidApp.TAG,
                        "applyPayload error: " + respCode + " " + response.getStatusLine().getReasonPhrase());
                return null;
            }
            HttpEntity entityResponse = response.getEntity();
            InputStream content = entityResponse.getContent();
            String contentString = Utils.convertStreamToString(new InflaterInputStream(content));
            Log.i(AnkiDroidApp.TAG, "Payload response = ");
            payloadReply = new JSONObject(contentString);
            Utils.printJSONObject(payloadReply, false);
            //Utils.saveJSONObject(payloadReply); //XXX: do we really want to append all JSON objects forever? I don't think so.
        } catch (UnsupportedEncodingException e) {
            Log.e(AnkiDroidApp.TAG, "UnsupportedEncodingException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return null;
        } catch (ClientProtocolException e) {
            Log.e(AnkiDroidApp.TAG, "ClientProtocolException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return null;
        } catch (IOException e) {
            Log.e(AnkiDroidApp.TAG, "IOException = " + e.getMessage());
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            return null;
        }

        return payloadReply;
    }

    /**
     * Parses a JSON InputStream to a list of SharedDecks efficiently 
     * @param is InputStream to parse and convert. It only works with the reply from
     *        getSharedDecks request
     * @return List of SharedDecks that were parsed
     * @throws Exception 
     */
    public static List<SharedDeck> parseGetSharedDecksResponce(InputStream is) throws Exception {
        BufferedReader rd = new BufferedReader(new InputStreamReader(is), 409600);
        JsonReader reader = new JsonReader(rd);
        List<SharedDeck> sharedDecks = new ArrayList<SharedDeck>();
        try {
            reader.beginArray();
            int count = 0;
            while (reader.hasNext()) {
                SharedDeck sd = new SharedDeck();
                reader.beginArray();
                sd.setId(reader.nextInt()); // SD_ID
                reader.skipValue(); // SD_USERNAME
                sd.setTitle(reader.nextString()); // SD_TITLE
                reader.skipValue(); // SD_DESCRIPTION
                reader.skipValue(); // SD_TAGS
                reader.skipValue(); // SD_VERSION
                sd.setFacts(reader.nextInt()); // SD_FACTS
                sd.setSize(reader.nextInt()); // SD_SIZE
                reader.skipValue(); // SD_COUNT
                reader.skipValue(); // SD_MODIFIED
                reader.skipValue(); // SD_FNAME
                reader.endArray();
                sharedDecks.add(sd);
                count++;
            }
            reader.endArray();
            reader.close();
            Log.d(AnkiDroidApp.TAG, "parseGetSharedDecksResponce: found " + count + " shared decks");
        } catch (Exception e) {
            Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e));
            sharedDecks.clear();
            throw e;
        }

        return sharedDecks;
    }

    /**
     * Get shared decks.
     */
    public static List<SharedDeck> getSharedDecks() throws Exception {

        try {
            if (sSharedDecks == null) {
                sSharedDecks = new ArrayList<SharedDeck>();

                HttpGet httpGet = new HttpGet(SYNC_SEARCH);
                httpGet.setHeader("Accept-Encoding", "identity");
                httpGet.setHeader("Host", SYNC_HOST);
                DefaultHttpClient defaultHttpClient = new DefaultHttpClient();

                HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
                HttpEntity ent = httpResponse.getEntity();
                InputStream inpStream = ent.getContent();
                sSharedDecks.addAll(parseGetSharedDecksResponce(inpStream));
            }
        } catch (Exception e) {
            sSharedDecks = null;
            throw e;
        }

        return sSharedDecks;
    }

    public static void resetSharedDecks() {
        sSharedDecks = null;
    }

}