feat(Async): add onError of CompleteCallback interface (#147)

* fix(Android): better Async error handling

* fix(Async): use Log instead of printStackTrace

* refactor(Async): replace Log.v() with Log.e()

* style(Async): reformat file
This commit is contained in:
Nathan Walker
2018-11-28 03:21:25 -08:00
committed by Vasil Chimev
parent 6055a6a278
commit ce45e75d2b

View File

@@ -1,5 +1,13 @@
package org.nativescript.widgets; package org.nativescript.widgets;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.Closeable; import java.io.Closeable;
@@ -8,6 +16,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@@ -16,23 +25,40 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Stack; import java.util.Stack;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.util.Base64;
import java.net.CookieManager;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
public class Async public class Async {
{ static final String TAG = "Async";
static ThreadPoolExecutor executor = null;
static ThreadPoolExecutor threadPoolExecutor() {
if (executor == null) {
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
ThreadFactory backgroundPriorityThreadFactory = new PriorityThreadFactory(android.os.Process.THREAD_PRIORITY_BACKGROUND);
executor = new ThreadPoolExecutor(
NUMBER_OF_CORES * 2,
NUMBER_OF_CORES * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
backgroundPriorityThreadFactory
);
}
return executor;
}
public interface CompleteCallback {
void onComplete(Object result, Object tag);
void onError(Object tag);
}
static class PriorityThreadFactory implements ThreadFactory { static class PriorityThreadFactory implements ThreadFactory {
private final int mThreadPriority; private final int mThreadPriority;
@@ -57,29 +83,6 @@ public class Async
} }
} }
static ThreadPoolExecutor executor = null;
static ThreadPoolExecutor threadPoolExecutor(){
if(executor == null) {
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
ThreadFactory backgroundPriorityThreadFactory = new PriorityThreadFactory(android.os.Process.THREAD_PRIORITY_BACKGROUND);
executor = new ThreadPoolExecutor(
NUMBER_OF_CORES * 2,
NUMBER_OF_CORES * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
backgroundPriorityThreadFactory
);
}
return executor;
}
public interface CompleteCallback {
void onComplete(Object result, Object tag);
}
public static class Image { public static class Image {
/* /*
* The request id parameter is needed for the sake of the JavaScript implementation. * The request id parameter is needed for the sake of the JavaScript implementation.
@@ -108,12 +111,12 @@ public class Async
final android.os.Handler mHandler = new android.os.Handler(); final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() { threadPoolExecutor().execute(new Runnable() {
@Override @Override
public void run () { public void run() {
final LoadImageFromFileTask task = new LoadImageFromFileTask(requestId, callback); final LoadImageFromFileTask task = new LoadImageFromFileTask(requestId, callback);
final Bitmap result = task.doInBackground(fileName); final Bitmap result = task.doInBackground(fileName);
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run () { public void run() {
task.onPostExecute(result); task.onPostExecute(result);
} }
}); });
@@ -125,12 +128,12 @@ public class Async
final android.os.Handler mHandler = new android.os.Handler(); final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() { threadPoolExecutor().execute(new Runnable() {
@Override @Override
public void run () { public void run() {
final LoadImageFromBase64StringTask task = new LoadImageFromBase64StringTask(requestId, callback); final LoadImageFromBase64StringTask task = new LoadImageFromBase64StringTask(requestId, callback);
final Bitmap result = task.doInBackground(source); final Bitmap result = task.doInBackground(source);
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run () { public void run() {
task.onPostExecute(result); task.onPostExecute(result);
} }
}); });
@@ -142,12 +145,12 @@ public class Async
final android.os.Handler mHandler = new android.os.Handler(); final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() { threadPoolExecutor().execute(new Runnable() {
@Override @Override
public void run () { public void run() {
final DownloadImageTask task = new DownloadImageTask(callback, context); final DownloadImageTask task = new DownloadImageTask(callback, context);
final Bitmap result = task.doInBackground(url); final Bitmap result = task.doInBackground(url);
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run () { public void run() {
task.onPostExecute(result); task.onPostExecute(result);
} }
}); });
@@ -171,24 +174,28 @@ public class Async
Bitmap bmp = BitmapFactory.decodeStream(stream); Bitmap bmp = BitmapFactory.decodeStream(stream);
return bmp; return bmp;
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
e.printStackTrace(); Log.e(TAG, "Failed to decode stream, MalformedURLException: " + e.getMessage());
return null; return null;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); Log.e(TAG, "Failed to decode stream, IOException: " + e.getMessage());
return null; return null;
} finally { } finally {
if (stream != null) { if (stream != null) {
try { try {
stream.close(); stream.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage());
} }
} }
} }
} }
protected void onPostExecute(final Bitmap result) { protected void onPostExecute(final Bitmap result) {
if (result != null) {
this.callback.onComplete(result, this.context); this.callback.onComplete(result, this.context);
} else {
this.callback.onError(this.context);
}
} }
} }
@@ -217,7 +224,11 @@ public class Async
} }
protected void onPostExecute(final Bitmap result) { protected void onPostExecute(final Bitmap result) {
if (result != null) {
this.callback.onComplete(result, this.requestId); this.callback.onComplete(result, this.requestId);
} else {
this.callback.onError(this.requestId);
}
} }
} }
@@ -236,7 +247,11 @@ public class Async
} }
protected void onPostExecute(final Bitmap result) { protected void onPostExecute(final Bitmap result) {
if (result != null) {
this.callback.onComplete(result, this.requestId); this.callback.onComplete(result, this.requestId);
} else {
this.callback.onError(this.requestId);
}
} }
} }
@@ -256,32 +271,54 @@ public class Async
} }
protected void onPostExecute(final Bitmap result) { protected void onPostExecute(final Bitmap result) {
if (result != null) {
this.callback.onComplete(result, this.requestId); this.callback.onComplete(result, this.requestId);
} else {
this.callback.onError(this.requestId);
}
} }
} }
} }
public static class Http public static class Http {
{
private static final String DELETE_METHOD = "DELETE"; private static final String DELETE_METHOD = "DELETE";
private static final String GET_METHOD = "GET"; private static final String GET_METHOD = "GET";
private static final String HEAD_METHOD = "HEAD"; private static final String HEAD_METHOD = "HEAD";
private static CookieManager cookieManager; private static CookieManager cookieManager;
public static class KeyValuePair public static void MakeRequest(final RequestOptions options, final CompleteCallback callback, final Object context) {
{ if (cookieManager == null) {
cookieManager = new CookieManager();
CookieHandler.setDefault(cookieManager);
}
final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
final HttpRequestTask task = new HttpRequestTask(callback, context);
final RequestResult result = task.doInBackground(options);
mHandler.post(new Runnable() {
@Override
public void run() {
task.onPostExecute(result);
}
});
}
});
}
public static class KeyValuePair {
public String key; public String key;
public String value; public String value;
public KeyValuePair(String key, String value) public KeyValuePair(String key, String value) {
{
this.key = key; this.key = key;
this.value = value; this.value = value;
} }
} }
public static class RequestOptions public static class RequestOptions {
{
public String url; public String url;
public String method; public String method;
public ArrayList<KeyValuePair> headers; public ArrayList<KeyValuePair> headers;
@@ -291,16 +328,13 @@ public class Async
public int screenHeight = -1; public int screenHeight = -1;
public boolean dontFollowRedirects = false; public boolean dontFollowRedirects = false;
public void addHeaders(HttpURLConnection connection) public void addHeaders(HttpURLConnection connection) {
{ if (this.headers == null) {
if (this.headers == null)
{
return; return;
} }
boolean hasAcceptHeader = false; boolean hasAcceptHeader = false;
for (KeyValuePair pair : this.headers) for (KeyValuePair pair : this.headers) {
{
String key = pair.key.toString(); String key = pair.key.toString();
connection.addRequestProperty(key, pair.value.toString()); connection.addRequestProperty(key, pair.value.toString());
if (key.toLowerCase().contentEquals("accept-encoding")) { if (key.toLowerCase().contentEquals("accept-encoding")) {
@@ -314,10 +348,8 @@ public class Async
} }
} }
public void writeContent(HttpURLConnection connection, Stack<Closeable> openedStreams) throws IOException public void writeContent(HttpURLConnection connection, Stack<Closeable> openedStreams) throws IOException {
{ if (this.content == null || this.content.getClass() != String.class) {
if (this.content == null || this.content.getClass() != String.class)
{
return; return;
} }
@@ -331,27 +363,7 @@ public class Async
} }
} }
public static class RequestResult public static class RequestResult {
{
public static final class ByteArrayOutputStream2 extends ByteArrayOutputStream
{
public ByteArrayOutputStream2()
{
super();
}
public ByteArrayOutputStream2(int size)
{
super(size);
}
/** Returns the internal buffer of this ByteArrayOutputStream, without copying. */
public synchronized byte[] buf()
{
return this.buf;
}
}
public ByteArrayOutputStream raw; public ByteArrayOutputStream raw;
public ArrayList<KeyValuePair> headers = new ArrayList<KeyValuePair>(); public ArrayList<KeyValuePair> headers = new ArrayList<KeyValuePair>();
public int statusCode; public int statusCode;
@@ -361,31 +373,27 @@ public class Async
public String url; public String url;
public String statusText; public String statusText;
public void getHeaders(HttpURLConnection connection) public void getHeaders(HttpURLConnection connection) {
{
Map<String, List<String>> headers = connection.getHeaderFields(); Map<String, List<String>> headers = connection.getHeaderFields();
if (headers == null) if (headers == null) {
{
// no headers, this may happen if there is no internet connection currently available // no headers, this may happen if there is no internet connection currently available
return; return;
} }
int size = headers.size(); int size = headers.size();
if (size == 0) if (size == 0) {
{
return; return;
} }
for (Map.Entry<String, List<String>> entry: headers.entrySet()) { for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
for (String value: entry.getValue()) { for (String value : entry.getValue()) {
this.headers.add(new KeyValuePair(key, value)); this.headers.add(new KeyValuePair(key, value));
} }
} }
} }
public void readResponseStream(HttpURLConnection connection, Stack<Closeable> openedStreams, RequestOptions options) throws IOException public void readResponseStream(HttpURLConnection connection, Stack<Closeable> openedStreams, RequestOptions options) throws IOException {
{
int contentLength = connection.getContentLength(); int contentLength = connection.getContentLength();
InputStream inStream = InputStream inStream =
@@ -393,8 +401,7 @@ public class Async
? connection.getErrorStream() ? connection.getErrorStream()
: connection.getInputStream(); : connection.getInputStream();
if (inStream == null) if (inStream == null) {
{
// inStream is null when receiving status code 401 or 407 // inStream is null when receiving status code 401 or 407
// see this thread for more information http://stackoverflow.com/a/24986433 // see this thread for more information http://stackoverflow.com/a/24986433
return; return;
@@ -417,8 +424,7 @@ public class Async
byte[] buff = new byte[4096]; byte[] buff = new byte[4096];
int read = -1; int read = -1;
while ((read = buffer.read(buff, 0, buff.length)) != -1) while ((read = buffer.read(buff, 0, buff.length)) != -1) {
{
responseStream.write(buff, 0, read); responseStream.write(buff, 0, read);
} }
@@ -429,8 +435,7 @@ public class Async
// world for better performance // world for better performance
// since we do not have some explicit way to determine whether // since we do not have some explicit way to determine whether
// the content-type is image // the content-type is image
try try {
{
// TODO: Generally this approach will not work for very // TODO: Generally this approach will not work for very
// large files // large files
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
@@ -438,22 +443,19 @@ public class Async
// check the size of the bitmap first // check the size of the bitmap first
BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions); BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions);
if (bitmapOptions.outWidth > 0 && bitmapOptions.outHeight > 0) if (bitmapOptions.outWidth > 0 && bitmapOptions.outHeight > 0) {
{
int scale = 1; int scale = 1;
final int height = bitmapOptions.outHeight; final int height = bitmapOptions.outHeight;
final int width = bitmapOptions.outWidth; final int width = bitmapOptions.outWidth;
if ((options.screenWidth > 0 && bitmapOptions.outWidth > options.screenWidth) || if ((options.screenWidth > 0 && bitmapOptions.outWidth > options.screenWidth) ||
(options.screenHeight > 0 && bitmapOptions.outHeight > options.screenHeight)) (options.screenHeight > 0 && bitmapOptions.outHeight > options.screenHeight)) {
{
final int halfHeight = height / 2; final int halfHeight = height / 2;
final int halfWidth = width / 2; final int halfWidth = width / 2;
// scale down the image since it is larger than the // scale down the image since it is larger than the
// screen resolution // screen resolution
while ((halfWidth / scale) > options.screenWidth && (halfHeight / scale) > options.screenHeight) while ((halfWidth / scale) > options.screenWidth && (halfHeight / scale) > options.screenHeight) {
{
scale *= 2; scale *= 2;
} }
} }
@@ -462,63 +464,48 @@ public class Async
bitmapOptions.inSampleSize = scale; bitmapOptions.inSampleSize = scale;
this.responseAsImage = BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions); this.responseAsImage = BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions);
} }
} } catch (Exception e) {
catch (Exception e) Log.e(TAG, "Failed to decode byte array, Exception: " + e.getMessage());
{
// bitmap decoding failed, the stream is not an image
e.printStackTrace();
} }
if (this.responseAsImage == null) if (this.responseAsImage == null) {
{
// convert to string // convert to string
this.responseAsString = responseStream.toString(); this.responseAsString = responseStream.toString();
} }
} }
public static final class ByteArrayOutputStream2 extends ByteArrayOutputStream {
public ByteArrayOutputStream2() {
super();
} }
public static void MakeRequest(final RequestOptions options, final CompleteCallback callback, final Object context) public ByteArrayOutputStream2(int size) {
{ super(size);
if (cookieManager == null)
{
cookieManager = new CookieManager();
CookieHandler.setDefault(cookieManager);
} }
final android.os.Handler mHandler = new android.os.Handler(); /**
threadPoolExecutor().execute(new Runnable() { * Returns the internal buffer of this ByteArrayOutputStream, without copying.
@Override */
public void run () { public synchronized byte[] buf() {
final HttpRequestTask task = new HttpRequestTask(callback, context); return this.buf;
final RequestResult result = task.doInBackground(options);
mHandler.post(new Runnable() {
@Override
public void run () {
task.onPostExecute(result);
} }
});
} }
});
} }
static class HttpRequestTask static class HttpRequestTask {
{
private CompleteCallback callback; private CompleteCallback callback;
private Object context; private Object context;
public HttpRequestTask(CompleteCallback callback, Object context) public HttpRequestTask(CompleteCallback callback, Object context) {
{
this.callback = callback; this.callback = callback;
this.context = context; this.context = context;
} }
protected RequestResult doInBackground(RequestOptions... params) protected RequestResult doInBackground(RequestOptions... params) {
{
RequestResult result = new RequestResult(); RequestResult result = new RequestResult();
Stack<Closeable> openedStreams = new Stack<Closeable>(); Stack<Closeable> openedStreams = new Stack<Closeable>();
try try {
{
RequestOptions options = params[0]; RequestOptions options = params[0];
URL url = new URL(options.url); URL url = new URL(options.url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
@@ -531,8 +518,7 @@ public class Async
options.addHeaders(connection); options.addHeaders(connection);
// apply timeout // apply timeout
if (options.timeout > 0) if (options.timeout > 0) {
{
connection.setConnectTimeout(options.timeout); connection.setConnectTimeout(options.timeout);
} }
@@ -542,8 +528,7 @@ public class Async
} }
// Do not attempt to write the content (body) for DELETE method, Java will throw directly // Do not attempt to write the content (body) for DELETE method, Java will throw directly
if (!requestMethod.equals(DELETE_METHOD)) if (!requestMethod.equals(DELETE_METHOD)) {
{
options.writeContent(connection, openedStreams); options.writeContent(connection, openedStreams);
} }
@@ -558,8 +543,7 @@ public class Async
result.url = options.url; result.url = options.url;
result.statusCode = connection.getResponseCode(); result.statusCode = connection.getResponseCode();
result.statusText = connection.getResponseMessage(); result.statusText = connection.getResponseMessage();
if (!requestMethod.equals(HEAD_METHOD)) if (!requestMethod.equals(HEAD_METHOD)) {
{
result.readResponseStream(connection, openedStreams, options); result.readResponseStream(connection, openedStreams, options);
} }
@@ -570,36 +554,30 @@ public class Async
connection.disconnect(); connection.disconnect();
return result; return result;
} } catch (Exception e) // TODO: Catch all exceptions?
catch (Exception e) // TODO: Catch all exceptions?
{ {
result.error = e; result.error = e;
return result; return result;
} } finally {
finally try {
{
try
{
this.closeOpenedStreams(openedStreams); this.closeOpenedStreams(openedStreams);
} } catch (IOException e) {
catch (IOException e) Log.e(TAG, "Failed to close opened streams, IOException: " + e.getMessage());
{
e.printStackTrace();
// TODO: Java rules - what to do here???
} }
} }
} }
protected void onPostExecute(final RequestResult result) protected void onPostExecute(final RequestResult result) {
{ if (result != null) {
this.callback.onComplete(result, this.context); this.callback.onComplete(result, this.context);
} else {
this.callback.onError(this.context);
}
} }
private void closeOpenedStreams(Stack<Closeable> streams) throws IOException private void closeOpenedStreams(Stack<Closeable> streams) throws IOException {
{ while (streams.size() > 0) {
while (streams.size() > 0)
{
Closeable stream = streams.pop(); Closeable stream = streams.pop();
stream.close(); stream.close();
} }