/*************************************************************************************** * 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 com.ichi2.Anki; import com.ichi2.anki.model.SharedDeck; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; 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 { public static Logger log = LoggerFactory.getLogger(AnkiDroidProxy.class); // 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; /** * Shared deck's fields */ private static final int SD_ID = 0; private static final int SD_USERNAME = 1; private static final int SD_TITLE = 2; private static final int SD_DESCRIPTION = 3; private static final int SD_TAGS = 4; private static final int SD_VERSION = 5; private static final int SD_FACTS = 6; private static final int SD_SIZE = 7; private static final int SD_COUNT = 8; private static final int SD_MODIFIED = 9; private static final int SD_FNAME = 10; /** * 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 static class Payload { public int taskType; public Object[] data; public Object result; public boolean success; public int returnType; public Exception exception; public Payload() { data = null; 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 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.info("Server decks = " + mDecks.toString()); mTimestamp = jsonDecks.getDouble("timestamp"); mTimediff = Math.abs(mTimestamp - Utils.now()); log.info("Server timestamp = " + mTimestamp); if (checkClocks && (mTimediff > 300)) { log.error("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.error("connect - unexpected status: " + status); return LOGIN_ERROR; } } catch (JSONException e) { log.error("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.error("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.error("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.error("AnkiDroidProxy.finish error: " + respCode + " " + response.getStatusLine().getReasonPhrase()); return false; } InputStream content = entityResponse.getContent(); String contentString = Utils.convertStreamToString(new InflaterInputStream(content)); log.info("finish: " + contentString); return true; } catch (UnsupportedEncodingException e) { log.error("UnsupportedEncodingException = " + e.getMessage(), e); return false; } catch (ClientProtocolException e) { log.error("ClientProtocolException = " + e.getMessage(), e); return false; } catch (IOException e) { log.error("IOException = " + e.getMessage(), e); return false; } } public String getDecks() { String decksServer = "{}"; try { // FIXME : Change client & String data = "p=" + URLEncoder.encode(mPassword, "UTF-8") + "&client=" + URLEncoder.encode(Anki.CLIENT_NAME, "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(Anki.LIBANKI_VERSION, "UTF-8") + "&pversion=5"; // log.info("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.error("getDecks error: " + respCode + " " + response.getStatusLine().getReasonPhrase()); return decksServer; } HttpEntity entityResponse = response.getEntity(); InputStream content = entityResponse.getContent(); decksServer = Utils.convertStreamToString(new InflaterInputStream(content)); log.info("getDecks response = " + decksServer); } catch (UnsupportedEncodingException e) { log.error("getDecks - UnsupportedEncodingException = " + e.getMessage()); log.error("getDecks - " + e); } catch (ClientProtocolException e) { log.error("getDecks - ClientProtocolException = " + e.getMessage()); log.error("getDecks - " + e); } catch (IOException e) { log.error("getDecks - IOException = " + e.getMessage()); log.error("getDecks - " + 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.info("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.info("Failed to create Deck: " + respCode + " " + reason); result.success = false; result.returnType = respCode; result.result = reason; return result; } else { log.info("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.error("createDeck - UnsupportedEncodingException = " + e.getMessage(), e); result.result = e.getMessage(); } catch (ClientProtocolException e) { log.error("createDeck - ClientProtocolException = " + e.getMessage(), e); result.result = e.getMessage(); } catch (IOException e) { log.error("createDeck - IOException = " + e.getMessage(), e); result.result = e.getMessage(); } catch (JSONException e) { log.error("createDeck - JSONException = " + e.getMessage(), 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.info("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.info("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.error("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.info("Summary server = "); Utils.printJSONObject(summaryServer); return summaryServer; } catch (UnsupportedEncodingException e) { log.error("UnsupportedEncodingException", e); } catch (ClientProtocolException e) { log.error("ClientProtocolException = " + e.getMessage(), e); } catch (IOException e) { log.error("IOException = " + e.getMessage(), e); } catch (JSONException e) { log.error("JSONException = " + e.getMessage(), e); } catch (OutOfMemoryError e) { log.error("OutOfMemoryError = " + e.getMessage(), e); } return null; } /** * Anki Desktop -> libanki/anki/sync.py, HttpSyncServerProxy - applyPayload * * @param payload * @throws JSONException */ public JSONObject applyPayload(JSONObject payload) throws JSONException { log.info("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.info("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.error("applyPayload error: " + respCode + " " + response.getStatusLine().getReasonPhrase()); return null; } HttpEntity entityResponse = response.getEntity(); InputStream content = entityResponse.getContent(); String contentString = Utils.convertStreamToString(new InflaterInputStream(content)); log.info("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.error("UnsupportedEncodingException = " + e.getMessage(), e); return null; } catch (ClientProtocolException e) { log.error("ClientProtocolException = " + e.getMessage(), e); return null; } catch (IOException e) { log.error("IOException = " + e.getMessage(), e); return null; } return payloadReply; } /** * 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); String response = Utils.convertStreamToString(httpResponse.getEntity().getContent()); // log.info("Content = " + response); sSharedDecks.addAll(getSharedDecksListFromJSONArray(new JSONArray(response))); } } catch (Exception e) { sSharedDecks = null; throw new Exception(); } return sSharedDecks; } public static void resetSharedDecks() { sSharedDecks = null; } private static List<SharedDeck> getSharedDecksListFromJSONArray(JSONArray jsonSharedDecks) throws JSONException { List<SharedDeck> sharedDecks = new ArrayList<SharedDeck>(); if (jsonSharedDecks != null) { // log.info("Number of shared decks = " + jsonSharedDecks.length()); int nbDecks = jsonSharedDecks.length(); for (int i = 0; i < nbDecks; i++) { JSONArray jsonSharedDeck = jsonSharedDecks.getJSONArray(i); SharedDeck sharedDeck = new SharedDeck(); sharedDeck.setId(jsonSharedDeck.getInt(SD_ID)); sharedDeck.setUsername(jsonSharedDeck.getString(SD_USERNAME)); sharedDeck.setTitle(jsonSharedDeck.getString(SD_TITLE)); sharedDeck.setDescription(jsonSharedDeck.getString(SD_DESCRIPTION)); sharedDeck.setTags(jsonSharedDeck.getString(SD_TAGS)); sharedDeck.setVersion(jsonSharedDeck.getInt(SD_VERSION)); sharedDeck.setFacts(jsonSharedDeck.getInt(SD_FACTS)); sharedDeck.setSize(jsonSharedDeck.getInt(SD_SIZE)); sharedDeck.setCount(jsonSharedDeck.getInt(SD_COUNT)); sharedDeck.setModified(jsonSharedDeck.getDouble(SD_MODIFIED)); sharedDeck.setFileName(jsonSharedDeck.getString(SD_FNAME)); // sharedDeck.prettyLog(); sharedDecks.add(sharedDeck); } } return sharedDecks; } }