From 2c79d428734bf53b528faf48e7cfa6e1e9c5b55c Mon Sep 17 00:00:00 2001 From: Rikard Teodorsson <9367038+hej2010@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:00:02 +0100 Subject: [PATCH 1/3] Upload files using a content uri --- .../com/parse/ParseCountingUriHttpBody.java | 59 ++++++++++++ parse/src/main/java/com/parse/ParseFile.java | 71 +++++++++----- .../java/com/parse/ParseFileController.java | 45 +++++++++ .../main/java/com/parse/ParseFileUtils.java | 32 ++++++- .../java/com/parse/ParseRESTFileCommand.java | 36 ++++++-- .../main/java/com/parse/ParseUriHttpBody.java | 77 ++++++++++++++++ .../parse/ParseCountingUriHttpBodyTest.java | 92 +++++++++++++++++++ .../com/parse/ParseFileControllerTest.java | 39 ++++++++ .../test/java/com/parse/ParseFileTest.java | 46 ++++++++++ .../java/com/parse/ParseUriHttpBodyTest.java | 59 ++++++++++++ 10 files changed, 526 insertions(+), 30 deletions(-) create mode 100644 parse/src/main/java/com/parse/ParseCountingUriHttpBody.java create mode 100644 parse/src/main/java/com/parse/ParseUriHttpBody.java create mode 100644 parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java create mode 100644 parse/src/test/java/com/parse/ParseUriHttpBodyTest.java diff --git a/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java b/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java new file mode 100644 index 000000000..87bf355aa --- /dev/null +++ b/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class ParseCountingUriHttpBody extends ParseUriHttpBody { + + private static final int DEFAULT_CHUNK_SIZE = 4096; + private static final int EOF = -1; + + private final ProgressCallback progressCallback; + + public ParseCountingUriHttpBody(Uri uri, ProgressCallback progressCallback) { + this(uri, null, progressCallback); + } + + public ParseCountingUriHttpBody( + Uri uri, String contentType, ProgressCallback progressCallback) { + super(uri, contentType); + this.progressCallback = progressCallback; + } + + @Override + public void writeTo(OutputStream output) throws IOException { + if (output == null) { + throw new IllegalArgumentException("Output stream may not be null"); + } + + final InputStream fileInput = Parse.getApplicationContext().getContentResolver().openInputStream(uri); + try { + byte[] buffer = new byte[DEFAULT_CHUNK_SIZE]; + int n; + long totalLength = getContentLength(); + long position = 0; + while (EOF != (n = fileInput.read(buffer))) { + output.write(buffer, 0, n); + position += n; + + if (progressCallback != null) { + int progress = (int) (100 * position / totalLength); + progressCallback.done(progress); + } + } + } finally { + ParseIOUtils.closeQuietly(fileInput); + } + } +} diff --git a/parse/src/main/java/com/parse/ParseFile.java b/parse/src/main/java/com/parse/ParseFile.java index f9d167e0d..f79291ce1 100644 --- a/parse/src/main/java/com/parse/ParseFile.java +++ b/parse/src/main/java/com/parse/ParseFile.java @@ -8,6 +8,7 @@ */ package com.parse; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import com.parse.boltsinternal.Continuation; @@ -64,6 +65,7 @@ public ParseFile[] newArray(int size) { */ /* package for tests */ byte[] data; /* package for tests */ File file; + /* package for tests */ Uri uri; private State state; /** @@ -102,6 +104,21 @@ public ParseFile(String name, byte[] data, String contentType) { this.data = data; } + /** + * Creates a new file from a content uri, file name, and content type. Content type will be used + * instead of auto-detection by file extension. + * + * @param name The file's name, ideally with extension. The file name must begin with an + * alphanumeric character, and consist of alphanumeric characters, periods, spaces, + * underscores, or dashes. + * @param uri The file uri. + * @param contentType The file's content type. + */ + public ParseFile(String name, Uri uri, String contentType) { + this(new State.Builder().name(name).mimeType(contentType).build()); + this.uri = uri; + } + /** * Creates a new file from a byte array. * @@ -263,28 +280,38 @@ private Task saveAsync( return Task.cancelled(); } - Task saveTask; - if (data != null) { - saveTask = - getFileController() - .saveAsync( - state, - data, - sessionToken, - progressCallbackOnMainThread( - uploadProgressCallback), - cancellationToken); - } else { - saveTask = - getFileController() - .saveAsync( - state, - file, - sessionToken, - progressCallbackOnMainThread( - uploadProgressCallback), - cancellationToken); - } + Task saveTask; + if (data != null) { + saveTask = + getFileController() + .saveAsync( + state, + data, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } else if (uri != null) { + saveTask = + getFileController() + .saveAsync( + state, + uri, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } else { + saveTask = + getFileController() + .saveAsync( + state, + file, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } return saveTask.onSuccessTask( task1 -> { diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index 084ac7e40..44118bcd6 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.net.Uri; + import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import java.io.File; @@ -163,6 +165,49 @@ public Task saveAsync( ParseExecutors.io()); } + public Task saveAsync( + final ParseFile.State state, + final Uri uri, + String sessionToken, + ProgressCallback uploadProgressCallback, + Task cancellationToken) { + if (state.url() != null) { // !isDirty + return Task.forResult(state); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + final ParseRESTCommand command = + new ParseRESTFileCommand.Builder() + .fileName(state.name()) + .uri(uri) + .contentType(state.mimeType()) + .sessionToken(sessionToken) + .build(); + + return command.executeAsync(restClient, uploadProgressCallback, null, cancellationToken) + .onSuccess( + task -> { + JSONObject result = task.getResult(); + ParseFile.State newState = + new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.writeUriToFile(getCacheFile(newState), uri); + } catch (IOException e) { + // do nothing + } + + return newState; + }, + ParseExecutors.io()); + } + public Task fetchAsync( final ParseFile.State state, @SuppressWarnings("UnusedParameters") String sessionToken, diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java index 50af66982..b7198feee 100644 --- a/parse/src/main/java/com/parse/ParseFileUtils.java +++ b/parse/src/main/java/com/parse/ParseFileUtils.java @@ -16,7 +16,13 @@ */ package com.parse; +import android.net.Uri; + import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -27,8 +33,6 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.List; -import org.json.JSONException; -import org.json.JSONObject; /** General file manipulation utilities. */ public class ParseFileUtils { @@ -115,6 +119,30 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti } } + /** + * Writes a content uri to a file creating the file if it does not exist. + * + *

NOTE: As from v1.3, the parent directories of the file will be created if they do not + * exist. + * + * @param file the file to write to + * @param uri the content uri with data to write to the file + * @throws IOException in case of an I/O error + * @since Commons IO 1.1 + */ + public static void writeUriToFile(File file, Uri uri) throws IOException { + OutputStream out = null; + InputStream in = null; + try { + in = Parse.getApplicationContext().getContentResolver().openInputStream(uri); + out = openOutputStream(file); + ParseIOUtils.copyLarge(in, out); + } finally { + ParseIOUtils.closeQuietly(out); + ParseIOUtils.closeQuietly(in); + } + } + // ----------------------------------------------------------------------- /** diff --git a/parse/src/main/java/com/parse/ParseRESTFileCommand.java b/parse/src/main/java/com/parse/ParseRESTFileCommand.java index e933509ba..1826747ef 100644 --- a/parse/src/main/java/com/parse/ParseRESTFileCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTFileCommand.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.net.Uri; + import com.parse.http.ParseHttpBody; import com.parse.http.ParseHttpRequest; import java.io.File; @@ -18,15 +20,23 @@ class ParseRESTFileCommand extends ParseRESTCommand { private final byte[] data; private final String contentType; private final File file; + private final Uri uri; public ParseRESTFileCommand(Builder builder) { super(builder); if (builder.file != null && builder.data != null) { throw new IllegalArgumentException("File and data can not be set at the same time"); } + if (builder.uri != null && builder.data != null) { + throw new IllegalArgumentException("URI and data can not be set at the same time"); + } + if (builder.file != null && builder.uri != null) { + throw new IllegalArgumentException("File and URI can not be set at the same time"); + } this.data = builder.data; this.contentType = builder.contentType; this.file = builder.file; + this.uri = builder.uri; } @Override @@ -35,13 +45,21 @@ protected ParseHttpBody newBody(final ProgressCallback progressCallback) { // file // in ParseFileController if (progressCallback == null) { - return data != null - ? new ParseByteArrayHttpBody(data, contentType) - : new ParseFileHttpBody(file, contentType); + if (data != null) { + return new ParseByteArrayHttpBody(data, contentType); + } else if (uri != null) { + return new ParseUriHttpBody(uri, contentType); + } else { + return new ParseFileHttpBody(file, contentType); + } + } + if (data != null) { + return new ParseCountingByteArrayHttpBody(data, contentType, progressCallback); + } else if (uri != null) { + return new ParseCountingUriHttpBody(uri, contentType, progressCallback); + } else { + return new ParseCountingFileHttpBody(file, contentType, progressCallback); } - return data != null - ? new ParseCountingByteArrayHttpBody(data, contentType, progressCallback) - : new ParseCountingFileHttpBody(file, contentType, progressCallback); } public static class Builder extends Init { @@ -49,6 +67,7 @@ public static class Builder extends Init { private byte[] data = null; private String contentType = null; private File file; + private Uri uri; public Builder() { // We only ever use ParseRESTFileCommand for file uploads, so default to POST. @@ -74,6 +93,11 @@ public Builder file(File file) { return this; } + public Builder uri(Uri uri) { + this.uri = uri; + return this; + } + @Override /* package */ Builder self() { return this; diff --git a/parse/src/main/java/com/parse/ParseUriHttpBody.java b/parse/src/main/java/com/parse/ParseUriHttpBody.java new file mode 100644 index 000000000..cd169399b --- /dev/null +++ b/parse/src/main/java/com/parse/ParseUriHttpBody.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import static com.parse.Parse.getApplicationContext; + +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.parse.http.ParseHttpBody; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class ParseUriHttpBody extends ParseHttpBody { + + /* package */ final Uri uri; + + public ParseUriHttpBody(Uri uri) { + this(uri, null); + } + + public ParseUriHttpBody(Uri uri, String contentType) { + super(contentType, getUriLength(uri)); + this.uri = uri; + } + + private static long getUriLength(Uri uri) { + long length = -1; + try { + ParcelFileDescriptor parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(uri, "r"); + if (parcelFileDescriptor != null) { + length = parcelFileDescriptor.getStatSize(); + parcelFileDescriptor.close(); + } + } catch (IOException ignored) { + } + if (length == -1) { + try { + AssetFileDescriptor assetFileDescriptor = getApplicationContext().getContentResolver().openAssetFileDescriptor(uri, "r"); + if (assetFileDescriptor != null) { + length = assetFileDescriptor.getLength(); + assetFileDescriptor.close(); + } + } catch (IOException ignored) { + } + } + return length; + } + + @Override + public InputStream getContent() throws IOException { + return getApplicationContext().getContentResolver().openInputStream(uri); + } + + @Override + public void writeTo(OutputStream out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("Output stream can not be null"); + } + + final InputStream fileInput = getApplicationContext().getContentResolver().openInputStream(uri); + try { + ParseIOUtils.copy(fileInput, out); + } finally { + ParseIOUtils.closeQuietly(fileInput); + } + } +} diff --git a/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java b/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java new file mode 100644 index 000000000..8175a6448 --- /dev/null +++ b/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.Uri; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class ParseCountingUriHttpBodyTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static String getData() { + char[] chars = new char[64 << 14]; // 1MB + Arrays.fill(chars, '1'); + return new String(chars); + } + + private static Uri makeTestUri(File root) throws IOException { + File file = new File(root, "test"); + FileWriter writer = new FileWriter(file); + writer.write(getData()); + writer.close(); + return Uri.fromFile(file); + } + + @Test + public void testWriteTo() throws Exception { + final Semaphore didReportIntermediateProgress = new Semaphore(0); + final Semaphore finish = new Semaphore(0); + + ParseCountingUriHttpBody body = + new ParseCountingUriHttpBody( + makeTestUri(temporaryFolder.getRoot()), + new ProgressCallback() { + Integer maxProgressSoFar = 0; + + @Override + public void done(Integer percentDone) { + if (percentDone > maxProgressSoFar) { + maxProgressSoFar = percentDone; + assertTrue(percentDone >= 0 && percentDone <= 100); + + if (percentDone < 100 && percentDone > 0) { + didReportIntermediateProgress.release(); + } else if (percentDone == 100) { + finish.release(); + } else if (percentDone == 0) { + // do nothing + } else { + fail("percentDone should be within 0 - 100"); + } + } + } + }); + + // Check content + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + assertArrayEquals(getData().getBytes(), output.toByteArray()); + // Check progress callback + assertTrue(didReportIntermediateProgress.tryAcquire(5, TimeUnit.SECONDS)); + assertTrue(finish.tryAcquire(5, TimeUnit.SECONDS)); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteToWithNullOutput() throws Exception { + ParseCountingUriHttpBody body = + new ParseCountingUriHttpBody(makeTestUri(temporaryFolder.getRoot()), null); + body.writeTo(null); + } +} diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index 1dd65e151..108cc8d2c 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -20,6 +20,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.net.Uri; + import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -197,6 +199,43 @@ public void testSaveAsyncSuccessWithFile() throws Exception { assertEquals("content", ParseFileUtils.readFileToString(cachedFile, "UTF-8")); } + @Test + public void testSaveAsyncSuccessWithUri() throws Exception { + JSONObject json = new JSONObject(); + json.put("name", "new_file_name"); + json.put("url", "http://example.com"); + String content = json.toString(); + + ParseHttpResponse mockResponse = + new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) content.length()) + .setContent(new ByteArrayInputStream(content.getBytes())) + .build(); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + File file = new File(root, "test"); + ParseFileUtils.writeStringToFile(file, "content", "UTF-8"); + Uri uri = Uri.fromFile(file); + ParseFile.State state = + new ParseFile.State.Builder().name("file_name").mimeType("mime_type").build(); + Task task = controller.saveAsync(state, uri, null, null, null); + ParseFile.State result = ParseTaskUtils.wait(task); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertEquals("new_file_name", result.name()); + assertEquals("http://example.com", result.url()); + File cachedFile = new File(root, "new_file_name"); + assertTrue(cachedFile.exists()); + assertTrue(file.exists()); + assertEquals("content", ParseFileUtils.readFileToString(cachedFile, "UTF-8")); + } + @Test public void testSaveAsyncFailureWithByteArray() throws Exception { // TODO(grantland): Remove once we no longer rely on retry logic. diff --git a/parse/src/test/java/com/parse/ParseFileTest.java b/parse/src/test/java/com/parse/ParseFileTest.java index 11c13a5ef..0a415cfbb 100644 --- a/parse/src/test/java/com/parse/ParseFileTest.java +++ b/parse/src/test/java/com/parse/ParseFileTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.net.Uri; import android.os.Parcel; import com.parse.boltsinternal.Task; import java.io.File; @@ -58,6 +59,7 @@ public void testConstructor() throws Exception { byte[] data = "hello".getBytes(); String contentType = "content_type"; File file = temporaryFolder.newFile(name); + Uri uri = Uri.fromFile(file); // TODO(mengyan): After we have proper staging strategy, we should verify the staging file's // content is the same with the original file. @@ -90,6 +92,10 @@ public void testConstructor() throws Exception { parseFile = new ParseFile(file, contentType); assertEquals(name, parseFile.getName()); // Default assertEquals("content_type", parseFile.getState().mimeType()); + + parseFile = new ParseFile(name, uri, contentType); + assertEquals(name, parseFile.getName()); + assertEquals("content_type", parseFile.getState().mimeType()); } @Test @@ -248,6 +254,46 @@ public void testSaveAsyncSuccessWithFile() throws Exception { assertEquals(url, parseFile.getUrl()); } + @Test + public void testSaveAsyncSuccessWithUri() throws Exception { + String name = "name"; + File file = temporaryFolder.newFile(name); + Uri uri = Uri.fromFile(file); + String contentType = "content_type"; + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder().url(url).build(); + ParseFileController controller = mock(ParseFileController.class); + when(controller.saveAsync( + any(ParseFile.State.class), + any(File.class), + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))) + .thenReturn(Task.forResult(state)); + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile parseFile = new ParseFile(name, uri, contentType); + ParseTaskUtils.wait(parseFile.saveAsync(null, null, null)); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = + ArgumentCaptor.forClass(ParseFile.State.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + verify(controller, times(1)) + .saveAsync( + stateCaptor.capture(), + fileCaptor.capture(), + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class)); + assertNull(stateCaptor.getValue().url()); + assertEquals(name, stateCaptor.getValue().name()); + assertEquals(contentType, stateCaptor.getValue().mimeType()); + assertEquals(file, fileCaptor.getValue()); + // Verify the state of ParseFile has been updated + assertEquals(url, parseFile.getUrl()); + } + // TODO(grantland): testSaveAsyncNotDirtyAfterQueueAwait // TODO(grantland): testSaveAsyncSuccess // TODO(grantland): testSaveAsyncFailure diff --git a/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java b/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java new file mode 100644 index 000000000..e0e837b26 --- /dev/null +++ b/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.net.Uri; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +public class ParseUriHttpBodyTest { + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testInitializeWithUri() throws IOException { + byte[] content = {1, 1, 1, 1, 1}; + String contentType = "application/json"; + File file = temporaryFolder.newFile("name"); + ParseFileUtils.writeByteArrayToFile(file, content); + Uri uri = Uri.fromFile(file); + ParseUriHttpBody body = new ParseUriHttpBody(uri, contentType); + assertArrayEquals(content, ParseIOUtils.toByteArray(body.getContent())); + assertEquals(contentType, body.getContentType()); + assertEquals(5, body.getContentLength()); + } + + @Test + public void testWriteTo() throws IOException { + String content = "content"; + String contentType = "application/json"; + File file = temporaryFolder.newFile("name"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + Uri uri = Uri.fromFile(file); + ParseUriHttpBody body = new ParseUriHttpBody(uri, contentType); + + // Check content + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + String contentAgain = output.toString(); + assertEquals(content, contentAgain); + + // No need to check whether content input stream is closed since it is a + // ByteArrayInputStream + } +} From 29107d42a8a85a2a1defd1666228888552aa308d Mon Sep 17 00:00:00 2001 From: Rikard Teodorsson <9367038+hej2010@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:10:27 +0100 Subject: [PATCH 2/3] Query for length --- .../main/java/com/parse/ParseUriHttpBody.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseUriHttpBody.java b/parse/src/main/java/com/parse/ParseUriHttpBody.java index cd169399b..89c00ffb2 100644 --- a/parse/src/main/java/com/parse/ParseUriHttpBody.java +++ b/parse/src/main/java/com/parse/ParseUriHttpBody.java @@ -11,8 +11,10 @@ import static com.parse.Parse.getApplicationContext; import android.content.res.AssetFileDescriptor; +import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; import com.parse.http.ParseHttpBody; @@ -35,13 +37,24 @@ public ParseUriHttpBody(Uri uri, String contentType) { private static long getUriLength(Uri uri) { long length = -1; - try { - ParcelFileDescriptor parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(uri, "r"); - if (parcelFileDescriptor != null) { - length = parcelFileDescriptor.getStatSize(); - parcelFileDescriptor.close(); + + try (Cursor cursor = getApplicationContext().getContentResolver().query(uri, null, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); + if (!cursor.isNull(sizeIndex)) { + length = cursor.getLong(sizeIndex); + } + } + } + if (length == -1) { + try { + ParcelFileDescriptor parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(uri, "r"); + if (parcelFileDescriptor != null) { + length = parcelFileDescriptor.getStatSize(); + parcelFileDescriptor.close(); + } + } catch (IOException ignored) { } - } catch (IOException ignored) { } if (length == -1) { try { From 1c10476208ac320aff351f7cef0db62fe6f3186a Mon Sep 17 00:00:00 2001 From: Rikard Teodorsson <9367038+hej2010@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:46:02 +0100 Subject: [PATCH 3/3] Spotless fix --- .../com/parse/ParseCountingUriHttpBody.java | 6 +- parse/src/main/java/com/parse/ParseFile.java | 64 +++++++++---------- .../java/com/parse/ParseFileController.java | 59 +++++++++-------- .../main/java/com/parse/ParseFileUtils.java | 9 +-- .../java/com/parse/ParseRESTFileCommand.java | 1 - .../main/java/com/parse/ParseUriHttpBody.java | 18 ++++-- .../parse/ParseCountingUriHttpBodyTest.java | 8 +-- .../com/parse/ParseFileControllerTest.java | 1 - .../java/com/parse/ParseUriHttpBodyTest.java | 11 ++-- 9 files changed, 86 insertions(+), 91 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java b/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java index 87bf355aa..0b0858b60 100644 --- a/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java +++ b/parse/src/main/java/com/parse/ParseCountingUriHttpBody.java @@ -9,7 +9,6 @@ package com.parse; import android.net.Uri; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -26,7 +25,7 @@ public ParseCountingUriHttpBody(Uri uri, ProgressCallback progressCallback) { } public ParseCountingUriHttpBody( - Uri uri, String contentType, ProgressCallback progressCallback) { + Uri uri, String contentType, ProgressCallback progressCallback) { super(uri, contentType); this.progressCallback = progressCallback; } @@ -37,7 +36,8 @@ public void writeTo(OutputStream output) throws IOException { throw new IllegalArgumentException("Output stream may not be null"); } - final InputStream fileInput = Parse.getApplicationContext().getContentResolver().openInputStream(uri); + final InputStream fileInput = + Parse.getApplicationContext().getContentResolver().openInputStream(uri); try { byte[] buffer = new byte[DEFAULT_CHUNK_SIZE]; int n; diff --git a/parse/src/main/java/com/parse/ParseFile.java b/parse/src/main/java/com/parse/ParseFile.java index f79291ce1..76a95e072 100644 --- a/parse/src/main/java/com/parse/ParseFile.java +++ b/parse/src/main/java/com/parse/ParseFile.java @@ -280,38 +280,38 @@ private Task saveAsync( return Task.cancelled(); } - Task saveTask; - if (data != null) { - saveTask = - getFileController() - .saveAsync( - state, - data, - sessionToken, - progressCallbackOnMainThread( - uploadProgressCallback), - cancellationToken); - } else if (uri != null) { - saveTask = - getFileController() - .saveAsync( - state, - uri, - sessionToken, - progressCallbackOnMainThread( - uploadProgressCallback), - cancellationToken); - } else { - saveTask = - getFileController() - .saveAsync( - state, - file, - sessionToken, - progressCallbackOnMainThread( - uploadProgressCallback), - cancellationToken); - } + Task saveTask; + if (data != null) { + saveTask = + getFileController() + .saveAsync( + state, + data, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } else if (uri != null) { + saveTask = + getFileController() + .saveAsync( + state, + uri, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } else { + saveTask = + getFileController() + .saveAsync( + state, + file, + sessionToken, + progressCallbackOnMainThread( + uploadProgressCallback), + cancellationToken); + } return saveTask.onSuccessTask( task1 -> { diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index 44118bcd6..17016ae58 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -9,7 +9,6 @@ package com.parse; import android.net.Uri; - import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import java.io.File; @@ -166,11 +165,11 @@ public Task saveAsync( } public Task saveAsync( - final ParseFile.State state, - final Uri uri, - String sessionToken, - ProgressCallback uploadProgressCallback, - Task cancellationToken) { + final ParseFile.State state, + final Uri uri, + String sessionToken, + ProgressCallback uploadProgressCallback, + Task cancellationToken) { if (state.url() != null) { // !isDirty return Task.forResult(state); } @@ -179,33 +178,33 @@ public Task saveAsync( } final ParseRESTCommand command = - new ParseRESTFileCommand.Builder() - .fileName(state.name()) - .uri(uri) - .contentType(state.mimeType()) - .sessionToken(sessionToken) - .build(); + new ParseRESTFileCommand.Builder() + .fileName(state.name()) + .uri(uri) + .contentType(state.mimeType()) + .sessionToken(sessionToken) + .build(); return command.executeAsync(restClient, uploadProgressCallback, null, cancellationToken) - .onSuccess( - task -> { - JSONObject result = task.getResult(); - ParseFile.State newState = - new ParseFile.State.Builder(state) - .name(result.getString("name")) - .url(result.getString("url")) - .build(); - - // Write data to cache - try { - ParseFileUtils.writeUriToFile(getCacheFile(newState), uri); - } catch (IOException e) { - // do nothing - } + .onSuccess( + task -> { + JSONObject result = task.getResult(); + ParseFile.State newState = + new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); - return newState; - }, - ParseExecutors.io()); + // Write data to cache + try { + ParseFileUtils.writeUriToFile(getCacheFile(newState), uri); + } catch (IOException e) { + // do nothing + } + + return newState; + }, + ParseExecutors.io()); } public Task fetchAsync( diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java index b7198feee..c48f7b517 100644 --- a/parse/src/main/java/com/parse/ParseFileUtils.java +++ b/parse/src/main/java/com/parse/ParseFileUtils.java @@ -17,12 +17,7 @@ package com.parse; import android.net.Uri; - import androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -33,6 +28,8 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.List; +import org.json.JSONException; +import org.json.JSONObject; /** General file manipulation utilities. */ public class ParseFileUtils { @@ -126,7 +123,7 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti * exist. * * @param file the file to write to - * @param uri the content uri with data to write to the file + * @param uri the content uri with data to write to the file * @throws IOException in case of an I/O error * @since Commons IO 1.1 */ diff --git a/parse/src/main/java/com/parse/ParseRESTFileCommand.java b/parse/src/main/java/com/parse/ParseRESTFileCommand.java index 1826747ef..63db546c3 100644 --- a/parse/src/main/java/com/parse/ParseRESTFileCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTFileCommand.java @@ -9,7 +9,6 @@ package com.parse; import android.net.Uri; - import com.parse.http.ParseHttpBody; import com.parse.http.ParseHttpRequest; import java.io.File; diff --git a/parse/src/main/java/com/parse/ParseUriHttpBody.java b/parse/src/main/java/com/parse/ParseUriHttpBody.java index 89c00ffb2..c6ab77483 100644 --- a/parse/src/main/java/com/parse/ParseUriHttpBody.java +++ b/parse/src/main/java/com/parse/ParseUriHttpBody.java @@ -15,9 +15,7 @@ import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; - import com.parse.http.ParseHttpBody; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -38,7 +36,10 @@ public ParseUriHttpBody(Uri uri, String contentType) { private static long getUriLength(Uri uri) { long length = -1; - try (Cursor cursor = getApplicationContext().getContentResolver().query(uri, null, null, null, null, null)) { + try (Cursor cursor = + getApplicationContext() + .getContentResolver() + .query(uri, null, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); if (!cursor.isNull(sizeIndex)) { @@ -48,7 +49,8 @@ private static long getUriLength(Uri uri) { } if (length == -1) { try { - ParcelFileDescriptor parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(uri, "r"); + ParcelFileDescriptor parcelFileDescriptor = + getApplicationContext().getContentResolver().openFileDescriptor(uri, "r"); if (parcelFileDescriptor != null) { length = parcelFileDescriptor.getStatSize(); parcelFileDescriptor.close(); @@ -58,7 +60,10 @@ private static long getUriLength(Uri uri) { } if (length == -1) { try { - AssetFileDescriptor assetFileDescriptor = getApplicationContext().getContentResolver().openAssetFileDescriptor(uri, "r"); + AssetFileDescriptor assetFileDescriptor = + getApplicationContext() + .getContentResolver() + .openAssetFileDescriptor(uri, "r"); if (assetFileDescriptor != null) { length = assetFileDescriptor.getLength(); assetFileDescriptor.close(); @@ -80,7 +85,8 @@ public void writeTo(OutputStream out) throws IOException { throw new IllegalArgumentException("Output stream can not be null"); } - final InputStream fileInput = getApplicationContext().getContentResolver().openInputStream(uri); + final InputStream fileInput = + getApplicationContext().getContentResolver().openInputStream(uri); try { ParseIOUtils.copy(fileInput, out); } finally { diff --git a/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java b/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java index 8175a6448..777851e53 100644 --- a/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java +++ b/parse/src/test/java/com/parse/ParseCountingUriHttpBodyTest.java @@ -13,11 +13,6 @@ import static org.junit.Assert.fail; import android.net.Uri; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileWriter; @@ -25,6 +20,9 @@ import java.util.Arrays; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; public class ParseCountingUriHttpBodyTest { diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index 108cc8d2c..7bc9c8298 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.when; import android.net.Uri; - import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; diff --git a/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java b/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java index e0e837b26..8e472195d 100644 --- a/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java +++ b/parse/src/test/java/com/parse/ParseUriHttpBodyTest.java @@ -12,18 +12,15 @@ import static org.junit.Assert.assertEquals; import android.net.Uri; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; public class ParseUriHttpBodyTest { - @Rule - public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testInitializeWithUri() throws IOException {