Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(android): run downloadFile asynchronously #2287

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import org.json.JSONException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.os.Handler;
import android.os.Looper;

public class Filesystem {

Expand Down Expand Up @@ -101,7 +105,7 @@ public File[] readdir(String path, String directory) throws DirectoryNotFoundExc
}

public File copy(String from, String directory, String to, String toDirectory, boolean doRename)
throws IOException, CopyFailedException {
throws IOException, CopyFailedException {
if (toDirectory == null) {
toDirectory = directory;
}
Expand Down Expand Up @@ -303,81 +307,123 @@ public void copyRecursively(File src, File dst) throws IOException {
}
}

public JSObject downloadFile(PluginCall call, Bridge bridge, HttpRequestHandler.ProgressEmitter emitter)
throws IOException, URISyntaxException, JSONException {
public void downloadFile(PluginCall call, Bridge bridge, HttpRequestHandler.ProgressEmitter emitter,
FilesystemDownloadCallback callback) throws IOException, URISyntaxException, JSONException {
Comment on lines +310 to +311
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this method no longer throws the mentioned exceptions, but rather returns them in FilesystemDownloadCallback, therefore we should update the signature

Suggested change
public void downloadFile(PluginCall call, Bridge bridge, HttpRequestHandler.ProgressEmitter emitter,
FilesystemDownloadCallback callback) throws IOException, URISyntaxException, JSONException {
public void downloadFile(PluginCall call, Bridge bridge, HttpRequestHandler.ProgressEmitter emitter,
FilesystemDownloadCallback callback) {

String urlString = call.getString("url", "");
JSObject headers = call.getObject("headers", new JSObject());
JSObject params = call.getObject("params", new JSObject());
Integer connectTimeout = call.getInt("connectTimeout");
Integer readTimeout = call.getInt("readTimeout");
Boolean disableRedirects = call.getBoolean("disableRedirects");
Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
Boolean progress = call.getBoolean("progress", false);

String method = call.getString("method", "GET").toUpperCase(Locale.ROOT);
String path = call.getString("path");
String directory = call.getString("directory", Environment.DIRECTORY_DOWNLOADS);

final URL url = new URL(urlString);
final File file = getFileObject(path, directory);

HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
.setUrl(url)
.setMethod(method)
.setHeaders(headers)
.setUrlParams(params, shouldEncode)
.setConnectTimeout(connectTimeout)
.setReadTimeout(readTimeout)
.setDisableRedirects(disableRedirects)
.openConnection();

CapacitorHttpUrlConnection connection = connectionBuilder.build();

connection.setSSLSocketFactory(bridge);

InputStream connectionInputStream = connection.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream(file, false);

String contentLength = connection.getHeaderField("content-length");
int bytes = 0;
int maxBytes = 0;
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());

executor.execute(() -> {
AsyncTaskResult result = doDownloadInBackground(urlString, call, bridge, emitter);
handler.post(() -> {
if (result.error != null) {
callback.onError(result.error);
} else {
callback.onSuccess(result.result);
}
});
Comment on lines +317 to +324
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that it is unnecessary to have this AsyncTaskResult, since it either just wraps the result or error if it exists.

I feel like we could lift the try-catch out of doDownloadInBackground, and have it return the JSObject directly

Suggested change
AsyncTaskResult result = doDownloadInBackground(urlString, call, bridge, emitter);
handler.post(() -> {
if (result.error != null) {
callback.onError(result.error);
} else {
callback.onSuccess(result.result);
}
});
try {
JSObject result = doDownloadInBackground(urlString, call, bridge, emitter);
handler.post(() -> callback.onSuccess(result));
} catch (Exception error) {
handler.post(() -> callback.onError(error));
}

Also, if you do this change, add the throws IOException, URISyntaxException, JSONException to the doDownloadInBackground signature.

What do you think?

executor.shutdown();
});
}

private AsyncTaskResult doDownloadInBackground(String urlString, PluginCall call, Bridge bridge,
HttpRequestHandler.ProgressEmitter emitter) {
try {
maxBytes = contentLength != null ? Integer.parseInt(contentLength) : 0;
} catch (NumberFormatException ignored) {}
JSObject headers = call.getObject("headers", new JSObject());
JSObject params = call.getObject("params", new JSObject());
Integer connectTimeout = call.getInt("connectTimeout");
Integer readTimeout = call.getInt("readTimeout");
Boolean disableRedirects = call.getBoolean("disableRedirects");
Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
Boolean progress = call.getBoolean("progress", false);

String method = call.getString("method", "GET").toUpperCase(Locale.ROOT);
String path = call.getString("path");
String directory = call.getString("directory", Environment.DIRECTORY_DOWNLOADS);

final URL url = new URL(urlString);
final File file = getFileObject(path, directory);

HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
.setUrl(url)
.setMethod(method)
.setHeaders(headers)
.setUrlParams(params, shouldEncode)
.setConnectTimeout(connectTimeout)
.setReadTimeout(readTimeout)
.setDisableRedirects(disableRedirects)
.openConnection();

CapacitorHttpUrlConnection connection = connectionBuilder.build();

connection.setSSLSocketFactory(bridge);

InputStream connectionInputStream = connection.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream(file, false);

String contentLength = connection.getHeaderField("content-length");
int bytes = 0;
int maxBytes = 0;

try {
maxBytes = contentLength != null ? Integer.parseInt(contentLength) : 0;
} catch (NumberFormatException ignored) {
}

byte[] buffer = new byte[1024];
int len;
byte[] buffer = new byte[1024];
int len;

// Throttle emitter to 100ms so it doesn't slow down app
long lastEmitTime = System.currentTimeMillis();
long minEmitIntervalMillis = 100;
// Throttle emitter to 100ms so it doesn't slow down app
long lastEmitTime = System.currentTimeMillis();
long minEmitIntervalMillis = 100;

while ((len = connectionInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, len);
while ((len = connectionInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, len);

bytes += len;
bytes += len;

if (progress && null != emitter) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastEmitTime > minEmitIntervalMillis) {
emitter.emit(bytes, maxBytes);
lastEmitTime = currentTime;
if (progress && null != emitter) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastEmitTime > minEmitIntervalMillis) {
emitter.emit(bytes, maxBytes);
lastEmitTime = currentTime;
}
}
}

if (progress && null != emitter) {
emitter.emit(bytes, maxBytes);
}

connectionInputStream.close();
fileOutputStream.close();

JSObject ret = new JSObject();
ret.put("path", file.getAbsolutePath());
return new AsyncTaskResult(ret);
} catch (Exception e) {
return new AsyncTaskResult(e);
}
}

private static class AsyncTaskResult {
final JSObject result;
final Exception error;

AsyncTaskResult(JSObject result) {
this.result = result;
this.error = null;
}

if (progress && null != emitter) {
emitter.emit(bytes, maxBytes);
AsyncTaskResult(Exception error) {
this.result = null;
this.error = error;
}
}

connectionInputStream.close();
fileOutputStream.close();
public interface FilesystemDownloadCallback {
void onSuccess(JSObject result);

return new JSObject() {
{
put("path", file.getAbsolutePath());
}
};
void onError(Exception error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void writeFile(PluginCall call) {
} else {
if (
fileObject.getParentFile() == null ||
fileObject.getParentFile().exists() ||
fileObject.getParentFile().exists() ||
(recursive && fileObject.getParentFile().mkdirs())
) {
saveFile(call, fileObject, data);
Expand Down Expand Up @@ -169,7 +169,7 @@ private void saveFile(PluginCall call, File file, String data) {
call.resolve(result);
} catch (IOException ex) {
Logger.error(
getLogTag(),
getLogTag(),
"Creating file '" + file.getPath() + "' with charset '" + charset + "' failed. Error: " + ex.getMessage(),
ex
);
Expand Down Expand Up @@ -388,23 +388,37 @@ public void downloadFile(PluginCall call) {

if (isPublicDirectory(directory) && !isStoragePermissionGranted()) {
requestAllPermissions(call, "permissionCallback");
} else {
HttpRequestHandler.ProgressEmitter emitter = (bytes, contentLength) -> {
JSObject ret = new JSObject();
ret.put("url", call.getString("url"));
ret.put("bytes", bytes);
ret.put("contentLength", contentLength);
return;
}

notifyListeners("progress", ret);
};
HttpRequestHandler.ProgressEmitter emitter = (bytes, contentLength) -> {
JSObject ret = new JSObject();
ret.put("url", call.getString("url"));
ret.put("bytes", bytes);
ret.put("contentLength", contentLength);
notifyListeners("progress", ret);
};

implementation.downloadFile(
call,
bridge,
emitter,
new Filesystem.FilesystemDownloadCallback() {
@Override
public void onSuccess(JSObject response) {
// update mediaStore index only if file was written to external storage
if (isPublicDirectory(directory)) {
MediaScannerConnection.scanFile(getContext(), new String[] { response.getString("path") }, null, null);
}
call.resolve(response);
}

JSObject response = implementation.downloadFile(call, bridge, emitter);
// update mediaStore index only if file was written to external storage
if (isPublicDirectory(directory)) {
MediaScannerConnection.scanFile(getContext(), new String[] { response.getString("path") }, null, null);
@Override
public void onError(Exception error) {
call.reject("Error downloading file: " + error.getLocalizedMessage(), error);
}
}
call.resolve(response);
}
);
} catch (Exception ex) {
call.reject("Error downloading file: " + ex.getLocalizedMessage(), ex);
}
Expand Down
Loading