Skip to content

Commit 83aec68

Browse files
authored
feat: Add support for uploading a ParseFile from a URI (#1207)
1 parent 697d213 commit 83aec68

10 files changed

+510
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.parse;
10+
11+
import android.net.Uri;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.io.OutputStream;
15+
16+
class ParseCountingUriHttpBody extends ParseUriHttpBody {
17+
18+
private static final int DEFAULT_CHUNK_SIZE = 4096;
19+
private static final int EOF = -1;
20+
21+
private final ProgressCallback progressCallback;
22+
23+
public ParseCountingUriHttpBody(Uri uri, ProgressCallback progressCallback) {
24+
this(uri, null, progressCallback);
25+
}
26+
27+
public ParseCountingUriHttpBody(
28+
Uri uri, String contentType, ProgressCallback progressCallback) {
29+
super(uri, contentType);
30+
this.progressCallback = progressCallback;
31+
}
32+
33+
@Override
34+
public void writeTo(OutputStream output) throws IOException {
35+
if (output == null) {
36+
throw new IllegalArgumentException("Output stream may not be null");
37+
}
38+
39+
final InputStream fileInput =
40+
Parse.getApplicationContext().getContentResolver().openInputStream(uri);
41+
try {
42+
byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
43+
int n;
44+
long totalLength = getContentLength();
45+
long position = 0;
46+
while (EOF != (n = fileInput.read(buffer))) {
47+
output.write(buffer, 0, n);
48+
position += n;
49+
50+
if (progressCallback != null) {
51+
int progress = (int) (100 * position / totalLength);
52+
progressCallback.done(progress);
53+
}
54+
}
55+
} finally {
56+
ParseIOUtils.closeQuietly(fileInput);
57+
}
58+
}
59+
}

parse/src/main/java/com/parse/ParseFile.java

+27
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
package com.parse;
1010

11+
import android.net.Uri;
1112
import android.os.Parcel;
1213
import android.os.Parcelable;
1314
import com.parse.boltsinternal.Continuation;
@@ -64,6 +65,7 @@ public ParseFile[] newArray(int size) {
6465
*/
6566
/* package for tests */ byte[] data;
6667
/* package for tests */ File file;
68+
/* package for tests */ Uri uri;
6769
private State state;
6870

6971
/**
@@ -102,6 +104,21 @@ public ParseFile(String name, byte[] data, String contentType) {
102104
this.data = data;
103105
}
104106

107+
/**
108+
* Creates a new file from a content uri, file name, and content type. Content type will be used
109+
* instead of auto-detection by file extension.
110+
*
111+
* @param name The file's name, ideally with extension. The file name must begin with an
112+
* alphanumeric character, and consist of alphanumeric characters, periods, spaces,
113+
* underscores, or dashes.
114+
* @param uri The file uri.
115+
* @param contentType The file's content type.
116+
*/
117+
public ParseFile(String name, Uri uri, String contentType) {
118+
this(new State.Builder().name(name).mimeType(contentType).build());
119+
this.uri = uri;
120+
}
121+
105122
/**
106123
* Creates a new file from a byte array.
107124
*
@@ -274,6 +291,16 @@ private Task<Void> saveAsync(
274291
progressCallbackOnMainThread(
275292
uploadProgressCallback),
276293
cancellationToken);
294+
} else if (uri != null) {
295+
saveTask =
296+
getFileController()
297+
.saveAsync(
298+
state,
299+
uri,
300+
sessionToken,
301+
progressCallbackOnMainThread(
302+
uploadProgressCallback),
303+
cancellationToken);
277304
} else {
278305
saveTask =
279306
getFileController()

parse/src/main/java/com/parse/ParseFileController.java

+44
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
package com.parse;
1010

11+
import android.net.Uri;
1112
import com.parse.boltsinternal.Task;
1213
import com.parse.http.ParseHttpRequest;
1314
import java.io.File;
@@ -163,6 +164,49 @@ public Task<ParseFile.State> saveAsync(
163164
ParseExecutors.io());
164165
}
165166

167+
public Task<ParseFile.State> saveAsync(
168+
final ParseFile.State state,
169+
final Uri uri,
170+
String sessionToken,
171+
ProgressCallback uploadProgressCallback,
172+
Task<Void> cancellationToken) {
173+
if (state.url() != null) { // !isDirty
174+
return Task.forResult(state);
175+
}
176+
if (cancellationToken != null && cancellationToken.isCancelled()) {
177+
return Task.cancelled();
178+
}
179+
180+
final ParseRESTCommand command =
181+
new ParseRESTFileCommand.Builder()
182+
.fileName(state.name())
183+
.uri(uri)
184+
.contentType(state.mimeType())
185+
.sessionToken(sessionToken)
186+
.build();
187+
188+
return command.executeAsync(restClient, uploadProgressCallback, null, cancellationToken)
189+
.onSuccess(
190+
task -> {
191+
JSONObject result = task.getResult();
192+
ParseFile.State newState =
193+
new ParseFile.State.Builder(state)
194+
.name(result.getString("name"))
195+
.url(result.getString("url"))
196+
.build();
197+
198+
// Write data to cache
199+
try {
200+
ParseFileUtils.writeUriToFile(getCacheFile(newState), uri);
201+
} catch (IOException e) {
202+
// do nothing
203+
}
204+
205+
return newState;
206+
},
207+
ParseExecutors.io());
208+
}
209+
166210
public Task<File> fetchAsync(
167211
final ParseFile.State state,
168212
@SuppressWarnings("UnusedParameters") String sessionToken,

parse/src/main/java/com/parse/ParseFileUtils.java

+25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.parse;
1818

19+
import android.net.Uri;
1920
import androidx.annotation.NonNull;
2021
import java.io.File;
2122
import java.io.FileInputStream;
@@ -115,6 +116,30 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti
115116
}
116117
}
117118

119+
/**
120+
* Writes a content uri to a file creating the file if it does not exist.
121+
*
122+
* <p>NOTE: As from v1.3, the parent directories of the file will be created if they do not
123+
* exist.
124+
*
125+
* @param file the file to write to
126+
* @param uri the content uri with data to write to the file
127+
* @throws IOException in case of an I/O error
128+
* @since Commons IO 1.1
129+
*/
130+
public static void writeUriToFile(File file, Uri uri) throws IOException {
131+
OutputStream out = null;
132+
InputStream in = null;
133+
try {
134+
in = Parse.getApplicationContext().getContentResolver().openInputStream(uri);
135+
out = openOutputStream(file);
136+
ParseIOUtils.copyLarge(in, out);
137+
} finally {
138+
ParseIOUtils.closeQuietly(out);
139+
ParseIOUtils.closeQuietly(in);
140+
}
141+
}
142+
118143
// -----------------------------------------------------------------------
119144

120145
/**

parse/src/main/java/com/parse/ParseRESTFileCommand.java

+29-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
package com.parse;
1010

11+
import android.net.Uri;
1112
import com.parse.http.ParseHttpBody;
1213
import com.parse.http.ParseHttpRequest;
1314
import java.io.File;
@@ -18,15 +19,23 @@ class ParseRESTFileCommand extends ParseRESTCommand {
1819
private final byte[] data;
1920
private final String contentType;
2021
private final File file;
22+
private final Uri uri;
2123

2224
public ParseRESTFileCommand(Builder builder) {
2325
super(builder);
2426
if (builder.file != null && builder.data != null) {
2527
throw new IllegalArgumentException("File and data can not be set at the same time");
2628
}
29+
if (builder.uri != null && builder.data != null) {
30+
throw new IllegalArgumentException("URI and data can not be set at the same time");
31+
}
32+
if (builder.file != null && builder.uri != null) {
33+
throw new IllegalArgumentException("File and URI can not be set at the same time");
34+
}
2735
this.data = builder.data;
2836
this.contentType = builder.contentType;
2937
this.file = builder.file;
38+
this.uri = builder.uri;
3039
}
3140

3241
@Override
@@ -35,20 +44,29 @@ protected ParseHttpBody newBody(final ProgressCallback progressCallback) {
3544
// file
3645
// in ParseFileController
3746
if (progressCallback == null) {
38-
return data != null
39-
? new ParseByteArrayHttpBody(data, contentType)
40-
: new ParseFileHttpBody(file, contentType);
47+
if (data != null) {
48+
return new ParseByteArrayHttpBody(data, contentType);
49+
} else if (uri != null) {
50+
return new ParseUriHttpBody(uri, contentType);
51+
} else {
52+
return new ParseFileHttpBody(file, contentType);
53+
}
54+
}
55+
if (data != null) {
56+
return new ParseCountingByteArrayHttpBody(data, contentType, progressCallback);
57+
} else if (uri != null) {
58+
return new ParseCountingUriHttpBody(uri, contentType, progressCallback);
59+
} else {
60+
return new ParseCountingFileHttpBody(file, contentType, progressCallback);
4161
}
42-
return data != null
43-
? new ParseCountingByteArrayHttpBody(data, contentType, progressCallback)
44-
: new ParseCountingFileHttpBody(file, contentType, progressCallback);
4562
}
4663

4764
public static class Builder extends Init<Builder> {
4865

4966
private byte[] data = null;
5067
private String contentType = null;
5168
private File file;
69+
private Uri uri;
5270

5371
public Builder() {
5472
// We only ever use ParseRESTFileCommand for file uploads, so default to POST.
@@ -74,6 +92,11 @@ public Builder file(File file) {
7492
return this;
7593
}
7694

95+
public Builder uri(Uri uri) {
96+
this.uri = uri;
97+
return this;
98+
}
99+
77100
@Override
78101
/* package */ Builder self() {
79102
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.parse;
10+
11+
import static com.parse.Parse.getApplicationContext;
12+
13+
import android.content.res.AssetFileDescriptor;
14+
import android.database.Cursor;
15+
import android.net.Uri;
16+
import android.os.ParcelFileDescriptor;
17+
import android.provider.OpenableColumns;
18+
import com.parse.http.ParseHttpBody;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
22+
23+
class ParseUriHttpBody extends ParseHttpBody {
24+
25+
/* package */ final Uri uri;
26+
27+
public ParseUriHttpBody(Uri uri) {
28+
this(uri, null);
29+
}
30+
31+
public ParseUriHttpBody(Uri uri, String contentType) {
32+
super(contentType, getUriLength(uri));
33+
this.uri = uri;
34+
}
35+
36+
private static long getUriLength(Uri uri) {
37+
long length = -1;
38+
39+
try (Cursor cursor =
40+
getApplicationContext()
41+
.getContentResolver()
42+
.query(uri, null, null, null, null, null)) {
43+
if (cursor != null && cursor.moveToFirst()) {
44+
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
45+
if (!cursor.isNull(sizeIndex)) {
46+
length = cursor.getLong(sizeIndex);
47+
}
48+
}
49+
}
50+
if (length == -1) {
51+
try {
52+
ParcelFileDescriptor parcelFileDescriptor =
53+
getApplicationContext().getContentResolver().openFileDescriptor(uri, "r");
54+
if (parcelFileDescriptor != null) {
55+
length = parcelFileDescriptor.getStatSize();
56+
parcelFileDescriptor.close();
57+
}
58+
} catch (IOException ignored) {
59+
}
60+
}
61+
if (length == -1) {
62+
try {
63+
AssetFileDescriptor assetFileDescriptor =
64+
getApplicationContext()
65+
.getContentResolver()
66+
.openAssetFileDescriptor(uri, "r");
67+
if (assetFileDescriptor != null) {
68+
length = assetFileDescriptor.getLength();
69+
assetFileDescriptor.close();
70+
}
71+
} catch (IOException ignored) {
72+
}
73+
}
74+
return length;
75+
}
76+
77+
@Override
78+
public InputStream getContent() throws IOException {
79+
return getApplicationContext().getContentResolver().openInputStream(uri);
80+
}
81+
82+
@Override
83+
public void writeTo(OutputStream out) throws IOException {
84+
if (out == null) {
85+
throw new IllegalArgumentException("Output stream can not be null");
86+
}
87+
88+
final InputStream fileInput =
89+
getApplicationContext().getContentResolver().openInputStream(uri);
90+
try {
91+
ParseIOUtils.copy(fileInput, out);
92+
} finally {
93+
ParseIOUtils.closeQuietly(fileInput);
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)