diff --git a/docs-android/app/app.iml b/docs-android/app/app.iml
index 2fa09bbf..1a8f311d 100644
--- a/docs-android/app/app.iml
+++ b/docs-android/app/app.iml
@@ -111,6 +111,7 @@
+
diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle
index b886eea0..bddc240f 100644
--- a/docs-android/app/build.gradle
+++ b/docs-android/app/build.gradle
@@ -58,5 +58,6 @@ dependencies {
compile 'com.shamanland:fab:0.0.6'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.0.1'
+ compile "com.squareup.okhttp3:okhttp-urlconnection:3.0.1"
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
}
diff --git a/docs-android/app/src/main/java/com/sismics/docs/fragment/DocListFragment.java b/docs-android/app/src/main/java/com/sismics/docs/fragment/DocListFragment.java
index c647c372..db810b88 100644
--- a/docs-android/app/src/main/java/com/sismics/docs/fragment/DocListFragment.java
+++ b/docs-android/app/src/main/java/com/sismics/docs/fragment/DocListFragment.java
@@ -21,7 +21,7 @@ import com.sismics.docs.event.DocumentAddEvent;
import com.sismics.docs.event.DocumentDeleteEvent;
import com.sismics.docs.event.DocumentEditEvent;
import com.sismics.docs.event.SearchEvent;
-import com.sismics.docs.listener.JsonHttpResponseHandler;
+import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.listener.RecyclerItemClickListener;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.ui.view.DividerItemDecoration;
@@ -29,7 +29,6 @@ import com.sismics.docs.ui.view.EmptyRecyclerView;
import org.json.JSONObject;
-import cz.msebera.android.httpclient.Header;
import de.greenrobot.event.EventBus;
/**
@@ -218,16 +217,16 @@ public class DocListFragment extends Fragment {
recyclerView.setEmptyView(progressBar);
- DocumentResource.list(getActivity(), reset ? 0 : adapter.getItemCount(), query, new JsonHttpResponseHandler() {
+ DocumentResource.list(getActivity(), reset ? 0 : adapter.getItemCount(), query, new HttpCallback() {
@Override
- public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
+ public void onSuccess(JSONObject response) {
adapter.addDocuments(response.optJSONArray("documents"));
documentsEmptyView.setText(R.string.no_documents);
recyclerView.setEmptyView(documentsEmptyView);
}
@Override
- public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
+ public void onFailure(JSONObject response, Exception e) {
documentsEmptyView.setText(R.string.error_loading_documents);
recyclerView.setEmptyView(documentsEmptyView);
diff --git a/docs-android/app/src/main/java/com/sismics/docs/listener/HttpCallback.java b/docs-android/app/src/main/java/com/sismics/docs/listener/HttpCallback.java
new file mode 100644
index 00000000..17802174
--- /dev/null
+++ b/docs-android/app/src/main/java/com/sismics/docs/listener/HttpCallback.java
@@ -0,0 +1,78 @@
+package com.sismics.docs.listener;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.Response;
+
+/**
+ * An HTTP callback.
+ *
+ * @author bgamard.
+ */
+public class HttpCallback {
+ public void onSuccess(JSONObject json) {
+ // Implement me
+ }
+
+ public void onFailure(JSONObject json, Exception e) {
+ // Implement me
+ }
+
+ public void onFinish() {
+ // Implement me
+ }
+
+ /**
+ * Build an OkHttp Callback from a HttpCallback.
+ *
+ * @param httpCallback HttpCallback
+ * @return OkHttp Callback
+ */
+ public static Callback buildOkHttpCallback(final HttpCallback httpCallback) {
+ return new Callback() {
+ @Override
+ public void onResponse(final Call call, final Response response) throws IOException {
+ final String body = response.body().string();
+
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ if (response.isSuccessful()) {
+ try {
+ httpCallback.onSuccess(new JSONObject(body));
+ } catch (Exception e) {
+ httpCallback.onFailure(null, e);
+ }
+ } else {
+ try {
+ httpCallback.onFailure(new JSONObject(body), null);
+ } catch (Exception e) {
+ httpCallback.onFailure(null, e);
+ }
+ }
+
+ httpCallback.onFinish();
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(final Call call, final IOException e) {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ httpCallback.onFailure(null, e);
+ httpCallback.onFinish();
+ }
+ });
+ }
+ };
+ }
+}
diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java b/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java
index ac3b3fde..985c69b3 100644
--- a/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java
+++ b/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java
@@ -9,7 +9,11 @@ import com.sismics.docs.util.ApplicationUtil;
import com.sismics.docs.util.PreferenceUtil;
import java.io.IOException;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.HttpCookie;
import java.net.Socket;
+import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@@ -18,12 +22,18 @@ import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import cz.msebera.android.httpclient.conn.ssl.SSLSocketFactory;
+import okhttp3.Interceptor;
+import okhttp3.JavaNetCookieJar;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
/**
* Base class for API access.
@@ -43,10 +53,15 @@ public class BaseResource {
protected static String ACCEPT_LANGUAGE = null;
/**
- * HTTP client.
+ * Async HTTP client.
*/
protected static AsyncHttpClient client = new AsyncHttpClient();
-
+
+ /**
+ * OkHttp client.
+ */
+ protected static OkHttpClient okHttpClient = new OkHttpClient();
+
static {
// 20sec default timeout
client.setTimeout(60000);
@@ -61,6 +76,43 @@ public class BaseResource {
} catch (Exception e) {
// NOP
}
+
+ // OkHttp configuration
+ try {
+ // Create a trust manager that does not validate certificate chains
+ final TrustManager[] trustAllCerts = new TrustManager[] {
+ new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+ };
+
+ // Install the all-trusting trust manager
+ final SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
+ final javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+ // Configure OkHttpClient
+ okHttpClient = okHttpClient.newBuilder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .hostnameVerifier(MySSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
+ .sslSocketFactory(sslSocketFactory)
+ .build();
+ } catch (Exception e) {
+ // NOP
+ }
}
/**
@@ -82,7 +134,47 @@ public class BaseResource {
client.addHeader("Accept-Language", ACCEPT_LANGUAGE);
}
}
-
+
+ /**
+ * Build an OkHttpClient.
+ *
+ * @param context Context
+ * @return OkHttpClient
+ */
+ protected static OkHttpClient buildOkHttpClient(final Context context) {
+ // Header computation
+ if (USER_AGENT == null) {
+ USER_AGENT = "Sismics Docs Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL;
+ }
+
+ if (ACCEPT_LANGUAGE == null) {
+ Locale locale = Locale.getDefault();
+ ACCEPT_LANGUAGE = locale.getLanguage() + "_" + locale.getCountry();
+ }
+
+ // Cookie handling
+ com.sismics.docs.resource.cookie.PersistentCookieStore cookieStore = new com.sismics.docs.resource.cookie.PersistentCookieStore(context);
+ CookieManager cookieManager = new CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL);
+ cookieStore.add(URI.create(PreferenceUtil.getServerUrl(context)),
+ new HttpCookie("auth_token", PreferenceUtil.getAuthToken(context))); // TODO Remove me when async http is ditched
+
+ // Runtime configuration
+ return okHttpClient.newBuilder()
+ .cookieJar(new JavaNetCookieJar(cookieManager))
+ .addNetworkInterceptor(new Interceptor() {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ return chain.proceed(originalRequest.newBuilder()
+ .header("User-Agent", USER_AGENT)
+ .header("Accept-Language", ACCEPT_LANGUAGE)
+ // TODO necessary?? .method(originalRequest.method(), originalRequest.body())
+ .build());
+ }
+ })
+ .build();
+ }
+
/**
* Socket factory to allow self-signed certificates for Async HTTP Client.
*
@@ -106,7 +198,7 @@ public class BaseResource {
}
};
- sslContext.init(null, new TrustManager[] { tm }, null);
+ sslContext.init(null, new TrustManager[]{tm}, null);
}
@Override
@@ -119,7 +211,7 @@ public class BaseResource {
return sslContext.getSocketFactory().createSocket();
}
}
-
+
/**
* Returns cleaned API URL.
*
diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/DocumentResource.java b/docs-android/app/src/main/java/com/sismics/docs/resource/DocumentResource.java
index dc7a89f2..65c8a0ed 100644
--- a/docs-android/app/src/main/java/com/sismics/docs/resource/DocumentResource.java
+++ b/docs-android/app/src/main/java/com/sismics/docs/resource/DocumentResource.java
@@ -3,10 +3,14 @@ package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.RequestParams;
+import com.sismics.docs.listener.HttpCallback;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import java.util.Set;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+
/**
* Access to /document API.
*
@@ -19,18 +23,22 @@ public class DocumentResource extends BaseResource {
* @param context Context
* @param offset Offset
* @param query Search query
- * @param responseHandler Callback
+ * @param callback Callback
*/
- public static void list(Context context, int offset, String query, JsonHttpResponseHandler responseHandler) {
- init(context);
-
- RequestParams params = new RequestParams();
- params.put("limit", 20);
- params.put("offset", offset);
- params.put("sort_column", 3);
- params.put("asc", false);
- params.put("search", query);
- client.get(getApiUrl(context) + "/document/list", params, responseHandler);
+ public static void list(Context context, int offset, String query, HttpCallback callback) {
+ Request request = new Request.Builder()
+ .url(HttpUrl.parse(getApiUrl(context) + "/document/list")
+ .newBuilder()
+ .addQueryParameter("limit", "20")
+ .addQueryParameter("offset", Integer.toString(offset))
+ .addQueryParameter("sort_column", "3")
+ .addQueryParameter("asc", "false")
+ .addQueryParameter("search", query)
+ .build())
+ .build();
+ buildOkHttpClient(context)
+ .newCall(request)
+ .enqueue(HttpCallback.buildOkHttpCallback(callback));
}
/**
diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/PersistentCookieStore.java b/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/PersistentCookieStore.java
new file mode 100644
index 00000000..990d37ed
--- /dev/null
+++ b/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/PersistentCookieStore.java
@@ -0,0 +1,229 @@
+package com.sismics.docs.resource.cookie;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A persistent cookie store which implements the Apache HttpClient CookieStore interface.
+ * Cookies are stored and will persist on the user's device between application sessions since they
+ * are serialized and stored in SharedPreferences.
+ */
+public class PersistentCookieStore implements CookieStore {
+
+ private static final String LOG_TAG = "PersistentCookieStore";
+ private static final String COOKIE_PREFS = "CookiePrefsFileOkHttp";
+ private static final String COOKIE_NAME_PREFIX = "cookie_okhttp_";
+
+ private final HashMap> cookies;
+ private final SharedPreferences cookiePrefs;
+
+ /**
+ * Construct a persistent cookie store.
+ *
+ * @param context Context to attach cookie store to
+ */
+ public PersistentCookieStore(Context context) {
+ cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
+ cookies = new HashMap<>();
+
+ // Load any previously stored cookies into the store
+ Map prefsMap = cookiePrefs.getAll();
+ for (Map.Entry entry : prefsMap.entrySet()) {
+ if (entry.getValue() != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
+ String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
+ for (String name : cookieNames) {
+ String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
+ if (encodedCookie != null) {
+ HttpCookie decodedCookie = decodeCookie(encodedCookie);
+ if (decodedCookie != null) {
+ if (!cookies.containsKey(entry.getKey()))
+ cookies.put(entry.getKey(), new ConcurrentHashMap());
+ cookies.get(entry.getKey()).put(name, decodedCookie);
+ }
+ }
+ }
+
+ }
+ }
+ }
+
+ @Override
+ public void add(URI uri, HttpCookie cookie) {
+ String name = getCookieToken(uri, cookie);
+
+ // Save cookie into local store, or remove if expired
+ if (!cookie.hasExpired()) {
+ if (!cookies.containsKey(uri.getHost()))
+ cookies.put(uri.getHost(), new ConcurrentHashMap());
+ cookies.get(uri.getHost()).put(name, cookie);
+ } else {
+ if (cookies.containsKey(uri.toString()))
+ cookies.get(uri.getHost()).remove(name);
+ }
+
+ // Save cookie into persistent store
+ SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
+ prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
+ prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));
+ prefsWriter.apply();
+ }
+
+ protected String getCookieToken(URI uri, HttpCookie cookie) {
+ return cookie.getName() + cookie.getDomain();
+ }
+
+ @Override
+ public List get(URI uri) {
+ ArrayList ret = new ArrayList<>();
+ if (cookies.containsKey(uri.getHost()))
+ ret.addAll(cookies.get(uri.getHost()).values());
+ return ret;
+ }
+
+ @Override
+ public boolean removeAll() {
+ SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
+ prefsWriter.clear();
+ prefsWriter.apply();
+ cookies.clear();
+ return true;
+ }
+
+
+ @Override
+ public boolean remove(URI uri, HttpCookie cookie) {
+ String name = getCookieToken(uri, cookie);
+
+ if (cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
+ cookies.get(uri.getHost()).remove(name);
+
+ SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
+ if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
+ prefsWriter.remove(COOKIE_NAME_PREFIX + name);
+ }
+ prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
+ prefsWriter.apply();
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public List getCookies() {
+ ArrayList ret = new ArrayList<>();
+ for (String key : cookies.keySet())
+ ret.addAll(cookies.get(key).values());
+
+ return ret;
+ }
+
+ @Override
+ public List getURIs() {
+ ArrayList ret = new ArrayList<>();
+ for (String key : cookies.keySet())
+ try {
+ ret.add(new URI(key));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+
+ return ret;
+ }
+
+ /**
+ * Serializes Cookie object into String
+ *
+ * @param cookie cookie to be encoded, can be null
+ * @return cookie encoded as String
+ */
+ protected String encodeCookie(SerializableHttpCookie cookie) {
+ if (cookie == null)
+ return null;
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ ObjectOutputStream outputStream = new ObjectOutputStream(os);
+ outputStream.writeObject(cookie);
+ } catch (IOException e) {
+ Log.d(LOG_TAG, "IOException in encodeCookie", e);
+ return null;
+ }
+
+ return byteArrayToHexString(os.toByteArray());
+ }
+
+ /**
+ * Returns cookie decoded from cookie string
+ *
+ * @param cookieString string of cookie as returned from http request
+ * @return decoded cookie or null if exception occured
+ */
+ protected HttpCookie decodeCookie(String cookieString) {
+ byte[] bytes = hexStringToByteArray(cookieString);
+ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
+ HttpCookie cookie = null;
+ try {
+ ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
+ cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();
+ } catch (IOException e) {
+ Log.d(LOG_TAG, "IOException in decodeCookie", e);
+ } catch (ClassNotFoundException e) {
+ Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
+ }
+
+ return cookie;
+ }
+
+ /**
+ * Using some super basic byte array <-> hex conversions so we don't have to rely on any
+ * large Base64 libraries. Can be overridden if you like!
+ *
+ * @param bytes byte array to be converted
+ * @return string containing hex values
+ */
+ protected String byteArrayToHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (byte element : bytes) {
+ int v = element & 0xff;
+ if (v < 16) {
+ sb.append('0');
+ }
+ sb.append(Integer.toHexString(v));
+ }
+ return sb.toString().toUpperCase(Locale.US);
+ }
+
+ /**
+ * Converts hex values from strings to byte arra
+ *
+ * @param hexString string of hex-encoded values
+ * @return decoded byte array
+ */
+ protected byte[] hexStringToByteArray(String hexString) {
+ int len = hexString.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
+ }
+ return data;
+ }
+}
\ No newline at end of file
diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/SerializableHttpCookie.java b/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/SerializableHttpCookie.java
new file mode 100644
index 00000000..5991096e
--- /dev/null
+++ b/docs-android/app/src/main/java/com/sismics/docs/resource/cookie/SerializableHttpCookie.java
@@ -0,0 +1,55 @@
+package com.sismics.docs.resource.cookie;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.net.HttpCookie;
+
+public class SerializableHttpCookie implements Serializable {
+ private static final long serialVersionUID = 6374381323722046732L;
+
+ private transient final HttpCookie cookie;
+ private transient HttpCookie clientCookie;
+
+ public SerializableHttpCookie(HttpCookie cookie) {
+ this.cookie = cookie;
+ }
+
+ public HttpCookie getCookie() {
+ HttpCookie bestCookie = cookie;
+ if (clientCookie != null) {
+ bestCookie = clientCookie;
+ }
+ return bestCookie;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeObject(cookie.getName());
+ out.writeObject(cookie.getValue());
+ out.writeObject(cookie.getComment());
+ out.writeObject(cookie.getCommentURL());
+ out.writeObject(cookie.getDomain());
+ out.writeLong(cookie.getMaxAge());
+ out.writeObject(cookie.getPath());
+ out.writeObject(cookie.getPortlist());
+ out.writeInt(cookie.getVersion());
+ out.writeBoolean(cookie.getSecure());
+ out.writeBoolean(cookie.getDiscard());
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ String name = (String) in.readObject();
+ String value = (String) in.readObject();
+ clientCookie = new HttpCookie(name, value);
+ clientCookie.setComment((String) in.readObject());
+ clientCookie.setCommentURL((String) in.readObject());
+ clientCookie.setDomain((String) in.readObject());
+ clientCookie.setMaxAge(in.readLong());
+ clientCookie.setPath((String) in.readObject());
+ clientCookie.setPortlist((String) in.readObject());
+ clientCookie.setVersion(in.readInt());
+ clientCookie.setSecure(in.readBoolean());
+ clientCookie.setDiscard(in.readBoolean());
+ }
+}
\ No newline at end of file