From 1fc9eb33019ad269aab0f3b4553ce6b52c997947 Mon Sep 17 00:00:00 2001 From: Synced Synapse Date: Sat, 18 Apr 2015 10:52:31 +0100 Subject: [PATCH] Switched HTTP library. Using OkHttp explicitly now, instead of the default Android one, which varies acroos OS versions. OkHttp is also used as the backend for loading images (Picasso). Added disk cache to image loading. --- app/build.gradle | 3 +- app/proguard-rules.pro | 3 + .../java/org/xbmc/kore/host/HostManager.java | 49 ++- .../org/xbmc/kore/jsonrpc/HostConnection.java | 394 +++++++++++------- ... => BasicAuthUrlConnectionDownloader.java} | 6 +- .../java/org/xbmc/kore/utils/NetUtils.java | 37 ++ 6 files changed, 333 insertions(+), 159 deletions(-) rename app/src/main/java/org/xbmc/kore/utils/{BasicAuthPicassoDownloader.java => BasicAuthUrlConnectionDownloader.java} (85%) diff --git a/app/build.gradle b/app/build.gradle index f7a15e5..305a21d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,7 +72,8 @@ dependencies { compile 'com.fasterxml.jackson.core:jackson-databind:2.4.2' compile 'com.jakewharton:butterknife:5.1.2' - compile 'com.squareup.picasso:picasso:2.4.0' + compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'com.squareup.picasso:picasso:2.5.2' compile 'de.greenrobot:eventbus:2.2.1' compile 'javax.jmdns:jmdns:3.4.1' compile 'com.astuetz:pagerslidingtabstrip:1.0.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0c8dbe1..ee0c693 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,6 +15,9 @@ # Picasso -dontwarn com.squareup.okhttp.** +# okio via OkHttp +-dontwarn okio.** + # Butterknife -dontwarn butterknife.internal.** -keep class **$$ViewInjector { *; } diff --git a/app/src/main/java/org/xbmc/kore/host/HostManager.java b/app/src/main/java/org/xbmc/kore/host/HostManager.java index 0a78c31..7de37b2 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostManager.java +++ b/app/src/main/java/org/xbmc/kore/host/HostManager.java @@ -21,14 +21,26 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Base64; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.picasso.OkHttpDownloader; import com.squareup.picasso.Picasso; + +import org.xbmc.kore.BuildConfig; import org.xbmc.kore.Settings; import org.xbmc.kore.jsonrpc.HostConnection; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.utils.BasicAuthPicassoDownloader; +import org.xbmc.kore.utils.BasicAuthUrlConnectionDownloader; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.NetUtils; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; /** @@ -195,9 +207,40 @@ public class HostManager { if (currentPicasso == null) { currentHostInfo = getHostInfo(); if (currentHostInfo != null) { +// currentPicasso = new Picasso.Builder(context) +// .downloader(new BasicAuthUrlConnectionDownloader(context, +// currentHostInfo.getUsername(), currentHostInfo.getPassword())) +// .indicatorsEnabled(BuildConfig.DEBUG) +// .build(); + + // Http client should already handle authentication + OkHttpClient picassoClient = getConnection().getOkHttpClient().clone(); + +// OkHttpClient picassoClient = new OkHttpClient(); +// // Set authentication on the client +// if (!TextUtils.isEmpty(currentHostInfo.getUsername())) { +// picassoClient.interceptors().add(new Interceptor() { +// @Override +// public Response intercept(Chain chain) throws IOException { +// +// String creds = currentHostInfo.getUsername() + ":" + currentHostInfo.getPassword(); +// Request newRequest = chain.request().newBuilder() +// .addHeader("Authorization", +// "Basic " + Base64.encodeToString(creds.getBytes(), Base64.NO_WRAP)) +// .build(); +// return chain.proceed(newRequest); +// } +// }); +// } + + // Set cache + File cacheDir = NetUtils.createDefaultCacheDir(context); + long cacheSize = NetUtils.calculateDiskCacheSize(cacheDir); + picassoClient.setCache(new com.squareup.okhttp.Cache(cacheDir,cacheSize)); + currentPicasso = new Picasso.Builder(context) - .downloader(new BasicAuthPicassoDownloader(context, - currentHostInfo.getUsername(), currentHostInfo.getPassword())) + .downloader(new OkHttpDownloader(picassoClient)) +// .indicatorsEnabled(BuildConfig.DEBUG) .build(); } } diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java index 9db2ffc..6d12de6 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java @@ -17,31 +17,36 @@ package org.xbmc.kore.jsonrpc; import android.os.Handler; import android.os.Process; -import android.util.Base64; +import android.text.TextUtils; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.squareup.okhttp.Authenticator; +import com.squareup.okhttp.Credentials; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.jsonrpc.notification.Input; import org.xbmc.kore.jsonrpc.notification.Player; import org.xbmc.kore.jsonrpc.notification.System; import org.xbmc.kore.utils.LogUtils; -import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; import java.net.InetSocketAddress; -import java.net.ProtocolException; +import java.net.Proxy; import java.net.Socket; -import java.net.URL; import java.util.HashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * Class responsible for communicating with the host. @@ -140,6 +145,18 @@ public class HostConnection { private static final int TCP_READ_TIMEOUT = 30000; // ms + /** + * OkHttpClient. Make sure it is initialized, by calling {@link #getOkHttpClient()} + */ + private OkHttpClient httpClient = null; + private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); + + /** + * Workaround for connection issues with Kodi. If we get a protocol exception, + * disable conn pooling (disable keep-alive) and try again + */ + private boolean disableConnectionPooling = false; + /** * Creates a new host connection * @param hostInfo Host info object @@ -258,7 +275,8 @@ public class HostConnection { public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); if (protocol == PROTOCOL_HTTP) { - executeThroughHTTP(method, callback, handler); +// executeThroughHttp(method, callback, handler); + executeThroughOkHttp(method, callback, handler); } else { executeThroughTcp(method, callback, handler); } @@ -269,60 +287,154 @@ public class HostConnection { //new Thread(command).start(); } - /** - * Sends the JSON RPC request through HTTP - */ - private void executeThroughHTTP(final ApiMethod method, final ApiCallback callback, - final Handler handler) { - String jsonRequest = method.toJsonString(); - try { - HttpURLConnection connection = openHttpConnection(hostInfo); - sendHttpRequest(connection, jsonRequest); - // Read response and convert it - final T result = method.resultFromJson(parseJsonResponse(readHttpResponse(connection))); - - if ((handler != null) && (callback != null)) { - handler.post(new Runnable() { - @Override - public void run() { - callback.onSuccess(result); - } - }); - } - } catch (final ApiException e) { - // Got an error, call error handler - - if ((handler != null) && (callback != null)) { - handler.post(new Runnable() { - @Override - public void run() { - callback.onError(e.getCode(), e.getMessage()); - } - }); - } - } - } +// /** +// * Sends the JSON RPC request through HTTP +// */ +// private void executeThroughHttp(final ApiMethod method, final ApiCallback callback, +// final Handler handler) { +// String jsonRequest = method.toJsonString(); +// try { +// HttpURLConnection connection = openHttpConnection(hostInfo); +// sendHttpRequest(connection, jsonRequest); +// // Read response and convert it +// final T result = method.resultFromJson(parseJsonResponse(readHttpResponse(connection))); +// +// if ((handler != null) && (callback != null)) { +// handler.post(new Runnable() { +// @Override +// public void run() { +// callback.onSuccess(result); +// } +// }); +// } +// } catch (final ApiException e) { +// // Got an error, call error handler +// +// if ((handler != null) && (callback != null)) { +// handler.post(new Runnable() { +// @Override +// public void run() { +// callback.onError(e.getCode(), e.getMessage()); +// } +// }); +// } +// } +// } +// +// /** +// * Auxiliary method to open a HTTP connection. +// * This method calls connect() so that any errors are cathced +// * @param hostInfo Host info +// * @return Connection set up +// * @throws ApiException +// */ +// private HttpURLConnection openHttpConnection(HostInfo hostInfo) throws ApiException { +// try { +//// LogUtils.LOGD(TAG, "Opening HTTP connection."); +// HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint()).openConnection(); +// connection.setRequestMethod("POST"); +// connection.setConnectTimeout(connectTimeout); +// //connection.setReadTimeout(connectTimeout); +// connection.setRequestProperty("Content-Type", "application/json"); +// connection.setDoOutput(true); +// +// // http basic authorization +// if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && +// (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { +// final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + +// hostInfo.getPassword()).getBytes(), Base64.DEFAULT); +// connection.setRequestProperty("Authorization", "Basic " + token); +// } +// +// // Check the connection +// connection.connect(); +// return connection; +// } catch (ProtocolException e) { +// // Won't try to catch this +// LogUtils.LOGE(TAG, "Got protocol exception while opening HTTP connection.", e); +// throw new RuntimeException(e); +// } catch (IOException e) { +// LogUtils.LOGW(TAG, "Failed to open HTTP connection.", e); +// throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); +// } +// } +// +// /** +// * Send an HTTP POST request +// * @param connection Open connection +// * @param request Request to send +// * @throws ApiException +// */ +// private void sendHttpRequest(HttpURLConnection connection, String request) throws ApiException { +// try { +// LogUtils.LOGD(TAG, "Sending request via HTTP: " + request); +// OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); +// out.write(request); +// out.flush(); +// out.close(); +// } catch (IOException e) { +// LogUtils.LOGW(TAG, "Failed to send HTTP request.", e); +// throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); +// } +// } +// +// /** +// * Reads the response from the server +// * @param connection Connection +// * @return Response read +// * @throws ApiException +// */ +// private String readHttpResponse(HttpURLConnection connection) throws ApiException { +// try { +//// LogUtils.LOGD(TAG, "Reading HTTP response."); +// int responseCode = connection.getResponseCode(); +// +// switch (responseCode) { +// case 200: +// // All ok, read response +// BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); +// StringBuilder response = new StringBuilder(); +// String inputLine; +// while ((inputLine = in.readLine()) != null) +// response.append(inputLine); +// in.close(); +// LogUtils.LOGD(TAG, "HTTP response: " + response.toString()); +// return response.toString(); +// case 401: +// LogUtils.LOGD(TAG, "HTTP response read error. Got a 401."); +// throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED, +// "Server returned response code: " + responseCode); +// case 404: +// LogUtils.LOGD(TAG, "HTTP response read error. Got a 404."); +// throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND, +// "Server returned response code: " + responseCode); +// default: +// LogUtils.LOGD(TAG, "HTTP response read error. Got: " + responseCode); +// throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN, +// "Server returned response code: " + responseCode); +// } +// } catch (IOException e) { +// LogUtils.LOGW(TAG, "Failed to read HTTP response.", e); +// throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e); +// } +// } /** - * Sends the JSON RPC request through HTTP, and calls the callback with the raw response, - * not parsed into the internal representation. - * Useful for sync methods that don't want to incur the overhead of constructing the - * internal objects. - * - * @param method Method object that represents the method too call - * @param callback {@link ApiCallback} to post the response to. This will be the raw - * {@link ObjectNode} received - * @param handler {@link Handler} to invoke callbacks on - * @param Method return type + * Sends the JSON RPC request through HTTP (using OkHttp library) */ - public void executeRaw(final ApiMethod method, final ApiCallback callback, - final Handler handler) { + private void executeThroughOkHttp(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + OkHttpClient client = getOkHttpClient(); String jsonRequest = method.toJsonString(); + try { - HttpURLConnection connection = openHttpConnection(hostInfo); - sendHttpRequest(connection, jsonRequest); - // Read response and convert it - final ObjectNode result = parseJsonResponse(readHttpResponse(connection)); + Request request = new Request.Builder() + .url(hostInfo.getJsonRpcHttpEndpoint()) + .post(RequestBody.create(MEDIA_TYPE_JSON, jsonRequest)) + .build(); + + Response response = sendOkHttpRequest(request); + final T result = method.resultFromJson(parseJsonResponse(handleOkHttpResponse(response))); if ((handler != null) && (callback != null)) { handler.post(new Runnable() { @@ -345,107 +457,85 @@ public class HostConnection { } } + /** + * Initializes this class OkHttpClient + */ + public OkHttpClient getOkHttpClient() { + if (httpClient == null) { + httpClient = new OkHttpClient(); + httpClient.setConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS); + + httpClient.setAuthenticator(new Authenticator() { + @Override + public Request authenticate(Proxy proxy, Response response) throws IOException { + if (TextUtils.isEmpty(hostInfo.getUsername())) + return null; + + String credential = Credentials.basic(hostInfo.getUsername(), hostInfo.getPassword()); + return response.request().newBuilder().header("Authorization", credential).build(); + } + + @Override + public Request authenticateProxy(Proxy proxy, Response response) throws IOException { + return null; + } + }); + } + return httpClient; + } /** - * Auxiliary method to open a HTTP connection. - * This method calls connect() so that any errors are cathced - * @param hostInfo Host info - * @return Connection set up - * @throws ApiException - */ - private HttpURLConnection openHttpConnection(HostInfo hostInfo) throws ApiException { - try { -// LogUtils.LOGD(TAG, "Opening HTTP connection."); - HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint()).openConnection(); - connection.setRequestMethod("POST"); - connection.setConnectTimeout(connectTimeout); - //connection.setReadTimeout(connectTimeout); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); + * Send an OkHttp POST request + * @param request Request to send + * @throws ApiException + */ + private Response sendOkHttpRequest(Request request) throws ApiException { + try { + LogUtils.LOGD(TAG, "Sending request via OkHttp: " + request.body()); + return httpClient.newCall(request).execute(); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to send OkHttp request.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); + } + } - // http basic authorization - if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && - (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { - final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + - hostInfo.getPassword()).getBytes(), Base64.DEFAULT); - connection.setRequestProperty("Authorization", "Basic " + token); - } - - // Check the connection - connection.connect(); - return connection; - } catch (ProtocolException e) { - // Won't try to catch this - LogUtils.LOGE(TAG, "Got protocol exception while opening HTTP connection.", e); - throw new RuntimeException(e); - } catch (IOException e) { - LogUtils.LOGW(TAG, "Failed to open HTTP connection.", e); - throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); - } - } - - /** - * Send an HTTP POST request - * @param connection Open connection - * @param request Request to send - * @throws ApiException - */ - private void sendHttpRequest(HttpURLConnection connection, String request) throws ApiException { - try { - LogUtils.LOGD(TAG, "Sending request via HTTP: " + request); - OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); - out.write(request); - out.flush(); - out.close(); - } catch (IOException e) { - LogUtils.LOGW(TAG, "Failed to send HTTP request.", e); - throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); - } - - } - - /** - * Reads the response from the server - * @param connection Connection - * @return Response read - * @throws ApiException - */ - private String readHttpResponse(HttpURLConnection connection) throws ApiException { - try { + /** + * Reads the response from the server + * @param response Response from OkHttp + * @return Response body string + * @throws ApiException + */ + private String handleOkHttpResponse(Response response) throws ApiException { + try { // LogUtils.LOGD(TAG, "Reading HTTP response."); - int responseCode = connection.getResponseCode(); + int responseCode = response.code(); - switch (responseCode) { - case 200: - // All ok, read response - BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); - StringBuilder response = new StringBuilder(); - String inputLine; - while ((inputLine = in.readLine()) != null) - response.append(inputLine); - in.close(); - LogUtils.LOGD(TAG, "HTTP response: " + response.toString()); - return response.toString(); - case 401: - LogUtils.LOGD(TAG, "HTTP response read error. Got a 401."); - throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED, - "Server returned response code: " + responseCode); - case 404: - LogUtils.LOGD(TAG, "HTTP response read error. Got a 404."); - throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND, - "Server returned response code: " + responseCode); - default: - LogUtils.LOGD(TAG, "HTTP response read error. Got: " + responseCode); - throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN, - "Server returned response code: " + responseCode); - } - } catch (IOException e) { - LogUtils.LOGW(TAG, "Failed to read HTTP response.", e); - throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e); - } - } + switch (responseCode) { + case 200: + // All ok, read response + String res = response.body().string(); + response.body().close(); + return res; + case 401: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 401: " + response); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED, + "Server returned response code: " + response); + case 404: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 404: " + response); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND, + "Server returned response code: " + response); + default: + LogUtils.LOGD(TAG, "HTTP response read error. Got: " + response); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN, + "Server returned response code: " + response); + } + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to read HTTP response.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e); + } + } - /** + /** * Parses the JSON response from the server. * If it is a valid result returns the JSON {@link com.fasterxml.jackson.databind.node.ObjectNode} that represents it. * If it is an error (contains the error tag), returns an {@link ApiException} with the info. @@ -793,7 +883,7 @@ public class HostConnection { /** * Cleans up used resources. - * This method should always be called if the protocoll used is TCP, so we can shutdown gracefully + * This method should always be called if the protocol used is TCP, so we can shutdown gracefully */ public void disconnect() { if (protocol == PROTOCOL_HTTP) diff --git a/app/src/main/java/org/xbmc/kore/utils/BasicAuthPicassoDownloader.java b/app/src/main/java/org/xbmc/kore/utils/BasicAuthUrlConnectionDownloader.java similarity index 85% rename from app/src/main/java/org/xbmc/kore/utils/BasicAuthPicassoDownloader.java rename to app/src/main/java/org/xbmc/kore/utils/BasicAuthUrlConnectionDownloader.java index e68161c..1b92f86 100644 --- a/app/src/main/java/org/xbmc/kore/utils/BasicAuthPicassoDownloader.java +++ b/app/src/main/java/org/xbmc/kore/utils/BasicAuthUrlConnectionDownloader.java @@ -25,18 +25,18 @@ import java.net.HttpURLConnection; /** * Picasso Downloader that sets basic authentication in the headers */ -public class BasicAuthPicassoDownloader extends UrlConnectionDownloader { +public class BasicAuthUrlConnectionDownloader extends UrlConnectionDownloader { protected final String username; protected final String password; - public BasicAuthPicassoDownloader(android.content.Context context) { + public BasicAuthUrlConnectionDownloader(android.content.Context context) { super(context); this.username = null; this.password = null; } - public BasicAuthPicassoDownloader(android.content.Context context, String username, String password) { + public BasicAuthUrlConnectionDownloader(android.content.Context context, String username, String password) { super(context); this.username = username; this.password = password; diff --git a/app/src/main/java/org/xbmc/kore/utils/NetUtils.java b/app/src/main/java/org/xbmc/kore/utils/NetUtils.java index 1d03a13..b4d077b 100644 --- a/app/src/main/java/org/xbmc/kore/utils/NetUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/NetUtils.java @@ -15,7 +15,11 @@ */ package org.xbmc.kore.utils; +import android.content.Context; +import android.os.StatFs; + import java.io.BufferedReader; +import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.DatagramPacket; @@ -190,4 +194,37 @@ public class NetUtils { } return bytes; } + + /** + * Utility functions to create a cache for images, used with the picasso library + * Lifted from com.squareup.picasso.Utils + */ + private static final String APP_CACHE = "app-cache"; + private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB + private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB + + public static File createDefaultCacheDir(Context context) { + File cache = new File(context.getApplicationContext().getCacheDir(), APP_CACHE); + if (!cache.exists()) { + //noinspection ResultOfMethodCallIgnored + cache.mkdirs(); + } + return cache; + } + + public static long calculateDiskCacheSize(File dir) { + long size = MIN_DISK_CACHE_SIZE; + + try { + StatFs statFs = new StatFs(dir.getAbsolutePath()); + long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize(); + // Target 2% of the total space. + size = available / 50; + } catch (IllegalArgumentException ignored) { + } + + // Bound inside min/max size for disk cache. + return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE); + } + }