Skip to content

Commit 3dfdca5

Browse files
nitzanjAmir Tocker
authored andcommitted
Add upload progress callback for Android
1 parent 7501af7 commit 3dfdca5

File tree

8 files changed

+162
-54
lines changed

8 files changed

+162
-54
lines changed

cloudinary-android/src/main/java/com/cloudinary/android/MultipartUtility.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
package com.cloudinary.android;
22

3-
import java.io.File;
4-
import java.io.FileInputStream;
5-
import java.io.IOException;
6-
import java.io.InputStream;
7-
import java.io.OutputStream;
8-
import java.io.OutputStreamWriter;
9-
import java.io.PrintWriter;
3+
import com.cloudinary.Cloudinary;
4+
5+
import java.io.*;
106
import java.net.HttpURLConnection;
117
import java.net.URL;
12-
import java.net.URLConnection;
138
import java.util.Map;
149

15-
import com.cloudinary.Cloudinary;
16-
1710
/**
1811
* This utility class provides an abstraction layer for sending multipart HTTP
1912
* POST requests to a web server.
@@ -25,14 +18,13 @@ public class MultipartUtility {
2518
private final String boundary;
2619
private static final String LINE_FEED = "\r\n";
2720
private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
21+
private final MultipartCallback multipartCallback;
2822
private HttpURLConnection httpConn;
2923
private String charset;
3024
private OutputStream outputStream;
3125
private PrintWriter writer;
32-
3326
public final static String USER_AGENT = "CloudinaryAndroid/" + Cloudinary.VERSION;
3427

35-
3628
/**
3729
* This constructor initializes a new HTTP POST request with content type is
3830
* set to multipart/form-data
@@ -42,8 +34,13 @@ public class MultipartUtility {
4234
* @throws IOException
4335
*/
4436
public MultipartUtility(String requestURL, String charset, String boundary, Map<String, String> headers) throws IOException {
37+
this(requestURL, charset, boundary, headers, null);
38+
}
39+
40+
public MultipartUtility(String requestURL, String charset, String boundary, Map<String, String> headers, MultipartCallback multipartCallback) throws IOException {
4541
this.charset = charset;
4642
this.boundary = boundary;
43+
this.multipartCallback = multipartCallback;
4744

4845
URL url = new URL(requestURL);
4946
httpConn = (HttpURLConnection) url.openConnection();
@@ -62,7 +59,7 @@ public MultipartUtility(String requestURL, String charset, String boundary, Map<
6259
}
6360

6461
public MultipartUtility(String requestURL, String charset, String boundary) throws IOException {
65-
this(requestURL, charset, boundary, null);
62+
this(requestURL, charset, boundary, null, null);
6663
}
6764

6865
/**
@@ -107,9 +104,11 @@ public void addFilePart(String fieldName, InputStream inputStream, String fileNa
107104
writer.flush();
108105

109106
byte[] buffer = new byte[4096];
110-
int bytesRead = -1;
107+
int bytesRead;
108+
long totalRead = 0;
111109
while ((bytesRead = inputStream.read(buffer)) != -1) {
112110
outputStream.write(buffer, 0, bytesRead);
111+
notifyCallback(totalRead += bytesRead);
113112
}
114113
outputStream.flush();
115114
inputStream.close();
@@ -118,6 +117,12 @@ public void addFilePart(String fieldName, InputStream inputStream, String fileNa
118117
writer.flush();
119118
}
120119

120+
private void notifyCallback(long bytes) {
121+
if (multipartCallback != null) {
122+
multipartCallback.totalBytesLoaded(bytes);
123+
}
124+
}
125+
121126
public void addFilePart(String fieldName, InputStream inputStream) throws IOException {
122127
addFilePart(fieldName, inputStream, "file");
123128
}
@@ -136,4 +141,7 @@ public HttpURLConnection execute() throws IOException {
136141
return httpConn;
137142
}
138143

144+
interface MultipartCallback {
145+
void totalBytesLoaded(long bytes);
146+
}
139147
}

cloudinary-android/src/main/java/com/cloudinary/android/UploaderStrategy.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
package com.cloudinary.android;
22

3-
import java.io.ByteArrayInputStream;
4-
import java.io.ByteArrayOutputStream;
5-
import java.io.File;
6-
import java.io.IOException;
7-
import java.io.InputStream;
3+
import com.cloudinary.strategies.AbstractUploaderStrategy;
4+
import com.cloudinary.strategies.ProgressCallback;
5+
import com.cloudinary.utils.ObjectUtils;
6+
import com.cloudinary.utils.StringUtils;
7+
import org.cloudinary.json.JSONException;
8+
import org.cloudinary.json.JSONObject;
9+
10+
import java.io.*;
811
import java.net.HttpURLConnection;
912
import java.util.Collection;
1013
import java.util.Map;
1114

12-
import org.cloudinary.json.JSONException;
13-
import org.cloudinary.json.JSONObject;
14-
15-
import com.cloudinary.strategies.AbstractUploaderStrategy;
16-
import com.cloudinary.utils.ObjectUtils;
17-
import com.cloudinary.utils.StringUtils;
15+
import static com.cloudinary.android.MultipartUtility.*;
1816

1917
public class UploaderStrategy extends AbstractUploaderStrategy {
2018

2119
@SuppressWarnings("rawtypes")
2220
@Override
23-
public Map callApi(String action, Map<String, Object> params, Map options, Object file) throws IOException {
21+
public Map callApi(String action, Map<String, Object> params, Map options, Object file, final ProgressCallback progressCallback) throws IOException {
2422
// initialize options if passed as null
2523
if (options == null) {
2624
options = ObjectUtils.emptyMap();
@@ -46,8 +44,22 @@ public Map callApi(String action, Map<String, Object> params, Map options, Objec
4644
params.put("api_key", apiKey);
4745
}
4846
}
47+
4948
String apiUrl = this.cloudinary().cloudinaryApiUrl(action, options);
50-
MultipartUtility multipart = new MultipartUtility(apiUrl, "UTF-8", this.cloudinary().randomPublicId(), (Map<String, String>) options.get("extra_headers"));
49+
MultipartCallback multipartCallback;
50+
if (progressCallback == null) {
51+
multipartCallback = null;
52+
} else {
53+
final long totalBytes = determineLength(file);
54+
multipartCallback = new MultipartCallback() {
55+
@Override
56+
public void totalBytesLoaded(long bytes) {
57+
progressCallback.onProgress(bytes, totalBytes);
58+
}
59+
};
60+
}
61+
62+
MultipartUtility multipart = new MultipartUtility(apiUrl, "UTF-8", this.cloudinary().randomPublicId(), (Map<String, String>) options.get("extra_headers"), multipartCallback);
5163

5264
// Remove blank parameters
5365
for (Map.Entry<String, Object> param : params.entrySet()) {
@@ -112,6 +124,23 @@ public Map callApi(String action, Map<String, Object> params, Map options, Objec
112124
}
113125
}
114126

127+
private long determineLength(Object file) {
128+
long actualLength = -1;
129+
130+
if (file != null) {
131+
if (file instanceof File) {
132+
actualLength = ((File) file).length();
133+
} else if (file instanceof byte[]) {
134+
actualLength = ((byte[]) file).length;
135+
} else if (!(file instanceof InputStream)) {
136+
File f = new File(file.toString());
137+
actualLength = f.length();
138+
}
139+
}
140+
141+
return actualLength;
142+
}
143+
115144
protected static String readFully(InputStream in) throws IOException {
116145
ByteArrayOutputStream baos = new ByteArrayOutputStream();
117146
byte[] buffer = new byte[1024];

cloudinary-core/src/main/java/com/cloudinary/Uploader.java

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.List;
1111
import java.util.Map;
1212

13+
import com.cloudinary.strategies.ProgressCallback;
1314
import org.cloudinary.json.JSONObject;
1415

1516
import com.cloudinary.strategies.AbstractUploaderStrategy;
@@ -30,7 +31,11 @@ private Command() {
3031
}
3132

3233
public Map callApi(String action, Map<String, Object> params, Map options, Object file) throws IOException {
33-
return strategy.callApi(action, params, options, file);
34+
return strategy.callApi(action, params, options, file, null);
35+
}
36+
37+
public Map callApi(String action, Map<String, Object> params, Map options, Object file, ProgressCallback progressCallback) throws IOException {
38+
return strategy.callApi(action, params, options, file, progressCallback);
3439
}
3540

3641
private Cloudinary cloudinary;
@@ -51,39 +56,64 @@ public Map<String, Object> buildUploadParams(Map options) {
5156
}
5257

5358
public Map unsignedUpload(Object file, String uploadPreset, Map options) throws IOException {
59+
return unsignedUpload(file, uploadPreset, options, null);
60+
}
61+
62+
public Map unsignedUpload(Object file, String uploadPreset, Map options, ProgressCallback progressCallback) throws IOException {
5463
if (options == null)
5564
options = ObjectUtils.emptyMap();
5665
HashMap nextOptions = new HashMap(options);
5766
nextOptions.put("unsigned", true);
5867
nextOptions.put("upload_preset", uploadPreset);
59-
return upload(file, nextOptions);
68+
return upload(file, nextOptions, progressCallback);
6069
}
6170

6271
public Map upload(Object file, Map options) throws IOException {
72+
return upload(file, options, null);
73+
}
74+
75+
public Map upload(Object file, Map options, final ProgressCallback progressCallback) throws IOException {
6376
if (options == null)
6477
options = ObjectUtils.emptyMap();
6578
Map<String, Object> params = buildUploadParams(options);
66-
return callApi("upload", params, options, file);
79+
80+
return callApi("upload", params, options, file, progressCallback);
6781
}
6882

6983
public Map uploadLargeRaw(Object file, Map options) throws IOException {
70-
return uploadLargeRaw(file, options, 20000000);
84+
return uploadLargeRaw(file, options, 20000000, null);
85+
}
86+
87+
public Map uploadLargeRaw(Object file, Map options, ProgressCallback progressCallback) throws IOException {
88+
return uploadLargeRaw(file, options, 20000000, progressCallback);
7189
}
7290

7391
public Map uploadLargeRaw(Object file, Map options, int bufferSize) throws IOException {
92+
return uploadLargeRaw(file, options, bufferSize, null);
93+
}
94+
95+
public Map uploadLargeRaw(Object file, Map options, int bufferSize, ProgressCallback callback) throws IOException {
7496
Map sentOptions = new HashMap();
7597
sentOptions.putAll(options);
7698
sentOptions.put("resource_type", "raw");
77-
return uploadLarge(file, sentOptions, bufferSize);
99+
return uploadLarge(file, sentOptions, bufferSize, callback);
78100
}
79101

80102
public Map uploadLarge(Object file, Map options) throws IOException {
103+
return uploadLarge(file, options, null);
104+
}
105+
106+
public Map uploadLarge(Object file, Map options, ProgressCallback progressCallback) throws IOException {
81107
int bufferSize = ObjectUtils.asInteger(options.get("chunk_size"), 20000000);
82-
return uploadLarge(file, options, bufferSize);
108+
return uploadLarge(file, options, bufferSize, progressCallback);
83109
}
84110

85111
@SuppressWarnings("resource")
86112
public Map uploadLarge(Object file, Map options, int bufferSize) throws IOException {
113+
return uploadLarge(file, options, bufferSize, null);
114+
}
115+
116+
public Map uploadLarge(Object file, Map options, int bufferSize, ProgressCallback progressCallback) throws IOException {
87117
InputStream input;
88118
long length = -1;
89119
if (file instanceof InputStream) {
@@ -100,14 +130,14 @@ public Map uploadLarge(Object file, Map options, int bufferSize) throws IOExcept
100130
input = new FileInputStream(f);
101131
}
102132
try {
103-
Map result = uploadLargeParts(input, options, bufferSize, length);
133+
Map result = uploadLargeParts(input, options, bufferSize, length, progressCallback);
104134
return result;
105135
} finally {
106136
input.close();
107137
}
108138
}
109139

110-
private Map uploadLargeParts(InputStream input, Map options, int bufferSize, long length) throws IOException {
140+
private Map uploadLargeParts(InputStream input, Map options, int bufferSize, long length, final ProgressCallback progressCallback) throws IOException {
111141
Map params = buildUploadParams(options);
112142

113143
Map sentOptions = new HashMap();
@@ -123,6 +153,8 @@ private Map uploadLargeParts(InputStream input, Map options, int bufferSize, lon
123153
int partNumber = 0;
124154
long totalBytes = 0;
125155
Map response = null;
156+
final long knownLengthBeforeUpload = length;
157+
long totalBytesUploaded = 0;
126158
while (true) {
127159
bytesRead = input.read(buffer, currentBufferSize, bufferSize - currentBufferSize);
128160
boolean atEnd = bytesRead == -1;
@@ -147,9 +179,27 @@ private Map uploadLargeParts(InputStream input, Map options, int bufferSize, lon
147179
extraHeaders.put("Content-Range", range);
148180
Map sentParams = new HashMap();
149181
sentParams.putAll(params);
150-
response = callApi("upload", sentParams, sentOptions, buffer);
182+
183+
// wrap the callback with another callback to account for multiple parts
184+
final long bytesUploadedSoFar = totalBytesUploaded;
185+
final ProgressCallback singlePartProgressCallback;
186+
if (progressCallback == null) {
187+
singlePartProgressCallback = null;
188+
} else {
189+
singlePartProgressCallback = new ProgressCallback() {
190+
191+
@Override
192+
public void onProgress(long bytesUploaded, long totalBytes) {
193+
progressCallback.onProgress(bytesUploadedSoFar + bytesUploaded, knownLengthBeforeUpload);
194+
}
195+
};
196+
}
197+
198+
response = callApi("upload", sentParams, sentOptions, buffer, singlePartProgressCallback);
199+
151200
if (atEnd) break;
152201
buffer[0] = nibbleBuffer[0];
202+
totalBytesUploaded += currentBufferSize;
153203
currentBufferSize = 1;
154204
partNumber++;
155205
}

cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractUploaderStrategy.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@ public Cloudinary cloudinary() {
1818
}
1919

2020
@SuppressWarnings("rawtypes")
21-
public abstract Map callApi(String action, Map<String, Object> params, Map options, Object file) throws IOException;
21+
public Map callApi(String action, Map<String, Object> params, Map options, Object file) throws IOException{
22+
return callApi(action, params, options, file, null);
23+
}
24+
25+
public abstract Map callApi(String action, Map<String, Object> params, Map options, Object file, ProgressCallback progressCallback) throws IOException;
2226
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.cloudinary.strategies;
2+
3+
public interface ProgressCallback {
4+
void onProgress(long bytesUploaded, long totalBytes);
5+
}

cloudinary-http42/src/main/java/com/cloudinary/http42/UploaderStrategy.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package com.cloudinary.http42;
22

3-
import java.io.File;
4-
import java.io.IOException;
5-
import java.io.InputStream;
6-
import java.nio.charset.Charset;
7-
import java.util.Collection;
8-
import java.util.Map;
9-
3+
import com.cloudinary.Cloudinary;
4+
import com.cloudinary.Util;
5+
import com.cloudinary.strategies.AbstractUploaderStrategy;
6+
import com.cloudinary.strategies.ProgressCallback;
7+
import com.cloudinary.utils.ObjectUtils;
8+
import com.cloudinary.utils.StringUtils;
109
import org.apache.http.HttpHost;
1110
import org.apache.http.HttpResponse;
1211
import org.apache.http.client.HttpClient;
@@ -22,17 +21,22 @@
2221
import org.cloudinary.json.JSONException;
2322
import org.cloudinary.json.JSONObject;
2423

25-
import com.cloudinary.Cloudinary;
26-
import com.cloudinary.Util;
27-
import com.cloudinary.strategies.AbstractUploaderStrategy;
28-
import com.cloudinary.utils.ObjectUtils;
29-
import com.cloudinary.utils.StringUtils;
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.nio.charset.Charset;
28+
import java.util.Collection;
29+
import java.util.Map;
3030

3131
public class UploaderStrategy extends AbstractUploaderStrategy {
3232

3333
@SuppressWarnings({"rawtypes", "unchecked"})
3434
@Override
35-
public Map callApi(String action, Map<String, Object> params, Map options, Object file) throws IOException {
35+
public Map callApi(String action, Map<String, Object> params, Map options, Object file, ProgressCallback progressCallback) throws IOException {
36+
if (progressCallback != null){
37+
throw new IllegalArgumentException("Progress callback is not supported");
38+
}
39+
3640
// initialize options if passed as null
3741
if (options == null) {
3842
options = ObjectUtils.emptyMap();

0 commit comments

Comments
 (0)