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;
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.ByteArrayOutputStream;
import java.io.Closeable;
@@ -8,6 +16,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
@@ -16,23 +25,40 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
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.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
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 {
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 {
/*
* 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();
threadPoolExecutor().execute(new Runnable() {
@Override
public void run () {
public void run() {
final LoadImageFromFileTask task = new LoadImageFromFileTask(requestId, callback);
final Bitmap result = task.doInBackground(fileName);
mHandler.post(new Runnable() {
@Override
public void run () {
public void run() {
task.onPostExecute(result);
}
});
@@ -125,12 +128,12 @@ public class Async
final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() {
@Override
public void run () {
public void run() {
final LoadImageFromBase64StringTask task = new LoadImageFromBase64StringTask(requestId, callback);
final Bitmap result = task.doInBackground(source);
mHandler.post(new Runnable() {
@Override
public void run () {
public void run() {
task.onPostExecute(result);
}
});
@@ -142,12 +145,12 @@ public class Async
final android.os.Handler mHandler = new android.os.Handler();
threadPoolExecutor().execute(new Runnable() {
@Override
public void run () {
public void run() {
final DownloadImageTask task = new DownloadImageTask(callback, context);
final Bitmap result = task.doInBackground(url);
mHandler.post(new Runnable() {
@Override
public void run () {
public void run() {
task.onPostExecute(result);
}
});
@@ -171,24 +174,28 @@ public class Async
Bitmap bmp = BitmapFactory.decodeStream(stream);
return bmp;
} catch (MalformedURLException e) {
e.printStackTrace();
Log.e(TAG, "Failed to decode stream, MalformedURLException: " + e.getMessage());
return null;
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Failed to decode stream, IOException: " + e.getMessage());
return null;
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage());
}
}
}
}
protected void onPostExecute(final Bitmap result) {
if (result != null) {
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) {
if (result != null) {
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) {
if (result != null) {
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) {
if (result != null) {
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 GET_METHOD = "GET";
private static final String HEAD_METHOD = "HEAD";
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 value;
public KeyValuePair(String key, String value)
{
public KeyValuePair(String key, String value) {
this.key = key;
this.value = value;
}
}
public static class RequestOptions
{
public static class RequestOptions {
public String url;
public String method;
public ArrayList<KeyValuePair> headers;
@@ -291,16 +328,13 @@ public class Async
public int screenHeight = -1;
public boolean dontFollowRedirects = false;
public void addHeaders(HttpURLConnection connection)
{
if (this.headers == null)
{
public void addHeaders(HttpURLConnection connection) {
if (this.headers == null) {
return;
}
boolean hasAcceptHeader = false;
for (KeyValuePair pair : this.headers)
{
for (KeyValuePair pair : this.headers) {
String key = pair.key.toString();
connection.addRequestProperty(key, pair.value.toString());
if (key.toLowerCase().contentEquals("accept-encoding")) {
@@ -314,10 +348,8 @@ public class Async
}
}
public void writeContent(HttpURLConnection connection, Stack<Closeable> openedStreams) throws IOException
{
if (this.content == null || this.content.getClass() != String.class)
{
public void writeContent(HttpURLConnection connection, Stack<Closeable> openedStreams) throws IOException {
if (this.content == null || this.content.getClass() != String.class) {
return;
}
@@ -331,27 +363,7 @@ public class Async
}
}
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 static class RequestResult {
public ByteArrayOutputStream raw;
public ArrayList<KeyValuePair> headers = new ArrayList<KeyValuePair>();
public int statusCode;
@@ -361,31 +373,27 @@ public class Async
public String url;
public String statusText;
public void getHeaders(HttpURLConnection connection)
{
public void getHeaders(HttpURLConnection connection) {
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
return;
}
int size = headers.size();
if (size == 0)
{
if (size == 0) {
return;
}
for (Map.Entry<String, List<String>> entry: headers.entrySet()) {
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String key = entry.getKey();
for (String value: entry.getValue()) {
for (String value : entry.getValue()) {
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();
InputStream inStream =
@@ -393,8 +401,7 @@ public class Async
? connection.getErrorStream()
: connection.getInputStream();
if (inStream == null)
{
if (inStream == null) {
// inStream is null when receiving status code 401 or 407
// see this thread for more information http://stackoverflow.com/a/24986433
return;
@@ -417,8 +424,7 @@ public class Async
byte[] buff = new byte[4096];
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);
}
@@ -429,8 +435,7 @@ public class Async
// world for better performance
// since we do not have some explicit way to determine whether
// the content-type is image
try
{
try {
// TODO: Generally this approach will not work for very
// large files
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
@@ -438,22 +443,19 @@ public class Async
// check the size of the bitmap first
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;
final int height = bitmapOptions.outHeight;
final int width = bitmapOptions.outWidth;
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 halfWidth = width / 2;
// scale down the image since it is larger than the
// screen resolution
while ((halfWidth / scale) > options.screenWidth && (halfHeight / scale) > options.screenHeight)
{
while ((halfWidth / scale) > options.screenWidth && (halfHeight / scale) > options.screenHeight) {
scale *= 2;
}
}
@@ -462,63 +464,48 @@ public class Async
bitmapOptions.inSampleSize = scale;
this.responseAsImage = BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions);
}
}
catch (Exception e)
{
// bitmap decoding failed, the stream is not an image
e.printStackTrace();
} catch (Exception e) {
Log.e(TAG, "Failed to decode byte array, Exception: " + e.getMessage());
}
if (this.responseAsImage == null)
{
if (this.responseAsImage == null) {
// convert to string
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)
{
if (cookieManager == null)
{
cookieManager = new CookieManager();
CookieHandler.setDefault(cookieManager);
public ByteArrayOutputStream2(int size) {
super(size);
}
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);
/**
* Returns the internal buffer of this ByteArrayOutputStream, without copying.
*/
public synchronized byte[] buf() {
return this.buf;
}
});
}
});
}
static class HttpRequestTask
{
static class HttpRequestTask {
private CompleteCallback callback;
private Object context;
public HttpRequestTask(CompleteCallback callback, Object context)
{
public HttpRequestTask(CompleteCallback callback, Object context) {
this.callback = callback;
this.context = context;
}
protected RequestResult doInBackground(RequestOptions... params)
{
protected RequestResult doInBackground(RequestOptions... params) {
RequestResult result = new RequestResult();
Stack<Closeable> openedStreams = new Stack<Closeable>();
try
{
try {
RequestOptions options = params[0];
URL url = new URL(options.url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
@@ -531,8 +518,7 @@ public class Async
options.addHeaders(connection);
// apply timeout
if (options.timeout > 0)
{
if (options.timeout > 0) {
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
if (!requestMethod.equals(DELETE_METHOD))
{
if (!requestMethod.equals(DELETE_METHOD)) {
options.writeContent(connection, openedStreams);
}
@@ -558,8 +543,7 @@ public class Async
result.url = options.url;
result.statusCode = connection.getResponseCode();
result.statusText = connection.getResponseMessage();
if (!requestMethod.equals(HEAD_METHOD))
{
if (!requestMethod.equals(HEAD_METHOD)) {
result.readResponseStream(connection, openedStreams, options);
}
@@ -570,36 +554,30 @@ public class Async
connection.disconnect();
return result;
}
catch (Exception e) // TODO: Catch all exceptions?
} catch (Exception e) // TODO: Catch all exceptions?
{
result.error = e;
return result;
}
finally
{
try
{
} finally {
try {
this.closeOpenedStreams(openedStreams);
}
catch (IOException e)
{
e.printStackTrace();
// TODO: Java rules - what to do here???
} catch (IOException e) {
Log.e(TAG, "Failed to close opened streams, IOException: " + e.getMessage());
}
}
}
protected void onPostExecute(final RequestResult result)
{
protected void onPostExecute(final RequestResult result) {
if (result != null) {
this.callback.onComplete(result, this.context);
} else {
this.callback.onError(this.context);
}
}
private void closeOpenedStreams(Stack<Closeable> streams) throws IOException
{
while (streams.size() > 0)
{
private void closeOpenedStreams(Stack<Closeable> streams) throws IOException {
while (streams.size() > 0) {
Closeable stream = streams.pop();
stream.close();
}