From 399f5567a2f390a4c3fc9dc9e770423e4b40d866 Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Wed, 4 Sep 2019 23:26:02 +0200 Subject: [PATCH] Migrate network stack to OkHttp and Okio --- app/build.gradle | 1 + .../be/digitalia/fosdem/api/FosdemApi.java | 22 ++-- .../fosdem/api/LiveRoomStatusesLiveData.java | 7 +- .../parsers/AbstractJsonPullParser.java | 7 +- .../fosdem/parsers/AbstractPullParser.java | 10 +- .../be/digitalia/fosdem/parsers/Parser.java | 4 +- .../fosdem/utils/ByteCountInputStream.java | 86 --------------- .../fosdem/utils/ByteCountSource.java | 53 +++++++++ .../be/digitalia/fosdem/utils/HttpUtils.java | 91 ---------------- .../fosdem/utils/network/HttpUtils.java | 101 ++++++++++++++++++ 10 files changed, 183 insertions(+), 199 deletions(-) delete mode 100644 app/src/main/java/be/digitalia/fosdem/utils/ByteCountInputStream.java create mode 100644 app/src/main/java/be/digitalia/fosdem/utils/ByteCountSource.java delete mode 100644 app/src/main/java/be/digitalia/fosdem/utils/HttpUtils.java create mode 100644 app/src/main/java/be/digitalia/fosdem/utils/network/HttpUtils.java diff --git a/app/build.gradle b/app/build.gradle index b581d7d..925ac71 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,5 +46,6 @@ dependencies { implementation 'androidx.paging:paging-runtime:2.1.0' implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" + implementation 'com.squareup.okhttp3:okhttp:3.12.3' implementation 'com.github.chrisbanes:PhotoView:2.3.0' } diff --git a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java index beae2a3..a94b265 100644 --- a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java +++ b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java @@ -2,10 +2,15 @@ package be.digitalia.fosdem.api; import android.content.Context; import android.os.AsyncTask; + import androidx.annotation.MainThread; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + import be.digitalia.fosdem.db.AppDatabase; import be.digitalia.fosdem.db.ScheduleDao; import be.digitalia.fosdem.livedata.SingleEvent; @@ -13,11 +18,8 @@ import be.digitalia.fosdem.model.DetailedEvent; import be.digitalia.fosdem.model.DownloadScheduleResult; import be.digitalia.fosdem.model.RoomStatus; import be.digitalia.fosdem.parsers.EventsParser; -import be.digitalia.fosdem.utils.HttpUtils; - -import java.io.InputStream; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; +import be.digitalia.fosdem.utils.network.HttpUtils; +import okio.BufferedSource; /** * Main API entry point. @@ -55,19 +57,19 @@ public class FosdemApi { DownloadScheduleResult res = DownloadScheduleResult.error(); try { ScheduleDao scheduleDao = AppDatabase.getInstance(context).getScheduleDao(); - HttpUtils.HttpResult httpResult = HttpUtils.get( + HttpUtils.Response httpResponse = HttpUtils.get( FosdemUrls.getSchedule(), scheduleDao.getLastModifiedTag(), progress::postValue); - if (httpResult.inputStream == null) { + if (httpResponse.source == null) { // Nothing to parse, the result is up-to-date. res = DownloadScheduleResult.upToDate(); return; } - try (InputStream is = httpResult.inputStream) { - Iterable events = new EventsParser().parse(is); - int count = scheduleDao.storeSchedule(events, httpResult.lastModified); + try (BufferedSource source = httpResponse.source) { + Iterable events = new EventsParser().parse(source); + int count = scheduleDao.storeSchedule(events, httpResponse.lastModified); res = DownloadScheduleResult.success(count); } diff --git a/app/src/main/java/be/digitalia/fosdem/api/LiveRoomStatusesLiveData.java b/app/src/main/java/be/digitalia/fosdem/api/LiveRoomStatusesLiveData.java index 5d43c26..82733d0 100644 --- a/app/src/main/java/be/digitalia/fosdem/api/LiveRoomStatusesLiveData.java +++ b/app/src/main/java/be/digitalia/fosdem/api/LiveRoomStatusesLiveData.java @@ -9,7 +9,8 @@ import android.text.format.DateUtils; import androidx.lifecycle.LiveData; import be.digitalia.fosdem.model.RoomStatus; import be.digitalia.fosdem.parsers.RoomStatusesParser; -import be.digitalia.fosdem.utils.HttpUtils; +import be.digitalia.fosdem.utils.network.HttpUtils; +import okio.BufferedSource; import java.io.InputStream; import java.util.Collections; @@ -77,8 +78,8 @@ class LiveRoomStatusesLiveData extends LiveData> { @Override protected Map doInBackground(Void... voids) { - try (InputStream is = HttpUtils.get(FosdemUrls.getRooms())) { - return new RoomStatusesParser().parse(is); + try (BufferedSource source = HttpUtils.get(FosdemUrls.getRooms())) { + return new RoomStatusesParser().parse(source); } catch (Throwable e) { return null; } diff --git a/app/src/main/java/be/digitalia/fosdem/parsers/AbstractJsonPullParser.java b/app/src/main/java/be/digitalia/fosdem/parsers/AbstractJsonPullParser.java index cda7fed..cdb90dd 100644 --- a/app/src/main/java/be/digitalia/fosdem/parsers/AbstractJsonPullParser.java +++ b/app/src/main/java/be/digitalia/fosdem/parsers/AbstractJsonPullParser.java @@ -2,14 +2,15 @@ package be.digitalia.fosdem.parsers; import android.util.JsonReader; -import java.io.InputStream; import java.io.InputStreamReader; +import okio.BufferedSource; + public abstract class AbstractJsonPullParser implements Parser { @Override - public T parse(InputStream source) throws Exception { - JsonReader reader = new JsonReader(new InputStreamReader(source)); + public T parse(BufferedSource source) throws Exception { + JsonReader reader = new JsonReader(new InputStreamReader(source.inputStream())); return parse(reader); } diff --git a/app/src/main/java/be/digitalia/fosdem/parsers/AbstractPullParser.java b/app/src/main/java/be/digitalia/fosdem/parsers/AbstractPullParser.java index b4a1be5..415f12b 100644 --- a/app/src/main/java/be/digitalia/fosdem/parsers/AbstractPullParser.java +++ b/app/src/main/java/be/digitalia/fosdem/parsers/AbstractPullParser.java @@ -5,11 +5,12 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; -import java.io.InputStream; + +import okio.BufferedSource; /** * Base class with helper methods for XML pull parsing. - * + * * @author Christophe Beyls */ public abstract class AbstractPullParser implements Parser { @@ -64,9 +65,10 @@ public abstract class AbstractPullParser implements Parser { } } - public T parse(InputStream is) throws Exception { + @Override + public T parse(BufferedSource source) throws Exception { m_parser = getFactory().newPullParser(); - m_parser.setInput(is, null); + m_parser.setInput(source.inputStream(), null); return parse(m_parser); } diff --git a/app/src/main/java/be/digitalia/fosdem/parsers/Parser.java b/app/src/main/java/be/digitalia/fosdem/parsers/Parser.java index 92d534d..ad4e84d 100644 --- a/app/src/main/java/be/digitalia/fosdem/parsers/Parser.java +++ b/app/src/main/java/be/digitalia/fosdem/parsers/Parser.java @@ -1,7 +1,7 @@ package be.digitalia.fosdem.parsers; -import java.io.InputStream; +import okio.BufferedSource; public interface Parser { - T parse(InputStream is) throws Exception; + T parse(BufferedSource source) throws Exception; } diff --git a/app/src/main/java/be/digitalia/fosdem/utils/ByteCountInputStream.java b/app/src/main/java/be/digitalia/fosdem/utils/ByteCountInputStream.java deleted file mode 100644 index d61a83d..0000000 --- a/app/src/main/java/be/digitalia/fosdem/utils/ByteCountInputStream.java +++ /dev/null @@ -1,86 +0,0 @@ -package be.digitalia.fosdem.utils; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -import androidx.annotation.NonNull; - -/** - * An InputStream which counts the total number of bytes read and notifies a listener. - * - * @author Christophe Beyls - * - */ -public class ByteCountInputStream extends FilterInputStream { - - public interface ByteCountListener { - void onNewCount(int byteCount); - } - - private final ByteCountListener listener; - private final int interval; - private int currentBytes = 0; - private int nextStepBytes; - - public ByteCountInputStream(InputStream input, ByteCountListener listener, int interval) { - super(input); - if (listener == null) { - throw new IllegalArgumentException("listener must not be null"); - } - if (interval <= 0) { - throw new IllegalArgumentException("interval must be at least 1 byte"); - } - this.listener = listener; - this.interval = interval; - nextStepBytes = interval; - listener.onNewCount(0); - } - - @Override - public int read() throws IOException { - int b = super.read(); - addBytes((b == -1) ? -1 : 1); - return b; - } - - @Override - public int read(@NonNull byte[] buffer, int offset, int max) throws IOException { - int count = super.read(buffer, offset, max); - addBytes(count); - return count; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public synchronized void mark(int readlimit) { - throw new IllegalStateException(); - } - - @Override - public synchronized void reset() throws IOException { - throw new IllegalStateException(); - } - - @Override - public long skip(long byteCount) throws IOException { - long count = super.skip(byteCount); - addBytes((int) count); - return count; - } - - private void addBytes(int count) { - if (count != -1) { - currentBytes += count; - if (currentBytes < nextStepBytes) { - return; - } - nextStepBytes = currentBytes + interval; - } - listener.onNewCount(currentBytes); - } -} diff --git a/app/src/main/java/be/digitalia/fosdem/utils/ByteCountSource.java b/app/src/main/java/be/digitalia/fosdem/utils/ByteCountSource.java new file mode 100644 index 0000000..434ef35 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/utils/ByteCountSource.java @@ -0,0 +1,53 @@ +package be.digitalia.fosdem.utils; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +import okio.Buffer; +import okio.ForwardingSource; +import okio.Source; + +/** + * A Source which counts the total number of bytes read and notifies a listener. + * + * @author Christophe Beyls + */ +public class ByteCountSource extends ForwardingSource { + + public interface ByteCountListener { + void onNewCount(long byteCount); + } + + private final ByteCountListener listener; + private final long interval; + private long currentBytes = 0; + private long nextStepBytes; + + public ByteCountSource(@NonNull Source input, @NonNull ByteCountListener listener, long interval) { + super(input); + if (interval <= 0) { + throw new IllegalArgumentException("interval must be at least 1 byte"); + } + this.listener = listener; + this.interval = interval; + nextStepBytes = interval; + listener.onNewCount(0L); + } + + @Override + public long read(Buffer sink, long byteCount) throws IOException { + final long count = super.read(sink, byteCount); + + if (count != -1L) { + currentBytes += count; + if (currentBytes < nextStepBytes) { + return count; + } + nextStepBytes = currentBytes + interval; + } + listener.onNewCount(currentBytes); + + return count; + } +} diff --git a/app/src/main/java/be/digitalia/fosdem/utils/HttpUtils.java b/app/src/main/java/be/digitalia/fosdem/utils/HttpUtils.java deleted file mode 100644 index 7d393b2..0000000 --- a/app/src/main/java/be/digitalia/fosdem/utils/HttpUtils.java +++ /dev/null @@ -1,91 +0,0 @@ -package be.digitalia.fosdem.utils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.zip.GZIPInputStream; - -/** - * Utility class to perform HTTP requests. - * - * @author Christophe Beyls - */ -public class HttpUtils { - - private static final int DEFAULT_TIMEOUT = 10000; - private static final int BUFFER_SIZE = 8192; - - public static class HttpResult { - // Will be null when the local content is up-to-date - public InputStream inputStream; - public String lastModified; - } - - public interface ProgressUpdateListener { - void onProgressUpdate(int percent); - } - - public static InputStream get(@NonNull String path) throws IOException { - return get(new URL(path), null, null).inputStream; - } - - public static HttpResult get(@NonNull String path, @Nullable String lastModified, @Nullable ProgressUpdateListener listener) - throws IOException { - return get(new URL(path), lastModified, listener); - } - - public static HttpResult get(@NonNull URL url, @Nullable String lastModified, @Nullable final ProgressUpdateListener listener) - throws IOException { - HttpResult result = new HttpResult(); - - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setReadTimeout(DEFAULT_TIMEOUT); - connection.setConnectTimeout(DEFAULT_TIMEOUT); - // We handle gzip manually to avoid EOFException bug in many Android versions when server returns HTTP 304 - connection.addRequestProperty("Accept-Encoding", "gzip"); - if (lastModified != null) { - connection.addRequestProperty("If-Modified-Since", lastModified); - } - connection.connect(); - - String contentEncoding = connection.getHeaderField("Content-Encoding"); - result.lastModified = connection.getHeaderField("Last-Modified"); - - int responseCode = connection.getResponseCode(); - if (responseCode != HttpURLConnection.HTTP_OK) { - connection.disconnect(); - - if ((responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) && (lastModified != null)) { - // Cached result is still valid; return an empty response - return result; - } - - throw new IOException("Server returned response code: " + responseCode); - } - - final int length = connection.getContentLength(); - result.inputStream = connection.getInputStream(); - - if ((listener != null) && (length != -1)) { - // Broadcast the progression in percents, with a precision of 1/10 of the total file size - result.inputStream = new ByteCountInputStream(result.inputStream, - byteCount -> { - // Cap percent to 100 - int percent = (byteCount >= length) ? 100 : byteCount * 100 / length; - listener.onProgressUpdate(percent); - }, length / 10); - } - - if ("gzip".equals(contentEncoding)) { - result.inputStream = new GZIPInputStream(result.inputStream, BUFFER_SIZE); - } else { - result.inputStream = new BufferedInputStream(result.inputStream, BUFFER_SIZE); - } - return result; - } -} diff --git a/app/src/main/java/be/digitalia/fosdem/utils/network/HttpUtils.java b/app/src/main/java/be/digitalia/fosdem/utils/network/HttpUtils.java new file mode 100644 index 0000000..be8c66d --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/utils/network/HttpUtils.java @@ -0,0 +1,101 @@ +package be.digitalia.fosdem.utils.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import be.digitalia.fosdem.utils.ByteCountSource; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import okio.BufferedSource; +import okio.Okio; + +/** + * Utility class to perform HTTP requests. + * + * @author Christophe Beyls + */ +public class HttpUtils { + + private static final long DEFAULT_CONNECT_TIMEOUT = 10L; + private static final long DEFAULT_READ_TIMEOUT = 10L; + + private static OkHttpClient sClient = new OkHttpClient.Builder() + .connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS) + .build(); + + private HttpUtils() { + } + + public static class Response { + // Will be null when the local content is up-to-date + @Nullable + public BufferedSource source; + @Nullable + public String lastModified; + } + + public interface ProgressUpdateListener { + void onProgressUpdate(int percent); + } + + public static BufferedSource get(@NonNull String path) throws IOException { + return get(new URL(path), null, null).source; + } + + public static Response get(@NonNull String path, @Nullable String lastModified, @Nullable ProgressUpdateListener listener) + throws IOException { + return get(new URL(path), lastModified, listener); + } + + public static Response get(@NonNull URL url, @Nullable String lastModified, @Nullable final ProgressUpdateListener listener) + throws IOException { + Request.Builder requestBuilder = new Request.Builder(); + + if (lastModified != null) { + requestBuilder.header("If-Modified-Since", lastModified); + } + + Request request = requestBuilder + .url(url) + .build(); + + final Response response = new Response(); + final okhttp3.Response okhttpResponse = sClient.newCall(request).execute(); + final ResponseBody body = okhttpResponse.body(); + if (!okhttpResponse.isSuccessful() || (body == null)) { + if ((okhttpResponse.code() == HttpURLConnection.HTTP_NOT_MODIFIED) && (lastModified != null)) { + // Cached result is still valid; return an empty response + return response; + } + + if (body != null) { + body.close(); + } + throw new IOException("Server returned response code: " + okhttpResponse.code()); + } + + response.lastModified = okhttpResponse.header("Last-Modified"); + + final long length = body.contentLength(); + if ((listener != null) && (length != -1L)) { + // Broadcast the progression in percents, with a precision of 1/10 of the total file size + response.source = Okio.buffer(new ByteCountSource(body.source(), + byteCount -> { + // Cap percent to 100 + int percent = (byteCount >= length) ? 100 : (int) (byteCount * 100L / length); + listener.onProgressUpdate(percent); + }, length / 10L)); + } else { + response.source = body.source(); + } + + return response; + } +}