1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

Migrate network stack to OkHttp and Okio

This commit is contained in:
Christophe Beyls 2019-09-04 23:26:02 +02:00
parent 0f4bc111a1
commit 399f5567a2
10 changed files with 183 additions and 199 deletions

View file

@ -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'
}

View file

@ -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<DetailedEvent> events = new EventsParser().parse(is);
int count = scheduleDao.storeSchedule(events, httpResult.lastModified);
try (BufferedSource source = httpResponse.source) {
Iterable<DetailedEvent> events = new EventsParser().parse(source);
int count = scheduleDao.storeSchedule(events, httpResponse.lastModified);
res = DownloadScheduleResult.success(count);
}

View file

@ -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<Map<String, RoomStatus>> {
@Override
protected Map<String, RoomStatus> 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;
}

View file

@ -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<T> implements Parser<T> {
@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);
}

View file

@ -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<T> implements Parser<T> {
@ -64,9 +65,10 @@ public abstract class AbstractPullParser<T> implements Parser<T> {
}
}
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);
}

View file

@ -1,7 +1,7 @@
package be.digitalia.fosdem.parsers;
import java.io.InputStream;
import okio.BufferedSource;
public interface Parser<T> {
T parse(InputStream is) throws Exception;
T parse(BufferedSource source) throws Exception;
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}