diff --git a/Parse/src/main/java/com/parse/GetDataCallback.java b/Parse/src/main/java/com/parse/GetDataCallback.java index b779b2e87..1dee628aa 100644 --- a/Parse/src/main/java/com/parse/GetDataCallback.java +++ b/Parse/src/main/java/com/parse/GetDataCallback.java @@ -37,3 +37,4 @@ public interface GetDataCallback extends ParseCallback2 @Override public void done(byte[] data, ParseException e); } + diff --git a/Parse/src/main/java/com/parse/GetDataStreamCallback.java b/Parse/src/main/java/com/parse/GetDataStreamCallback.java new file mode 100644 index 000000000..aee829ba6 --- /dev/null +++ b/Parse/src/main/java/com/parse/GetDataStreamCallback.java @@ -0,0 +1,41 @@ +/* + * 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 java.io.InputStream; + +/** + * A {@code GetDataStreamCallback} is used to run code after a {@link ParseFile} fetches its data on + * a background thread. + *

+ * The easiest way to use a {@code GetDataStreamCallback} is through an anonymous inner class. + * Override the {@code done} function to specify what the callback should do after the fetch is + * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * file.getDataStreamInBackground(new GetDataStreamCallback() {
+ *   public void done(InputSteam input, ParseException e) {
+ *     // ...
+ *   }
+ * });
+ * 
+ */ +public interface GetDataStreamCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param input + * The data that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + public void done(InputStream input, ParseException e); +} diff --git a/Parse/src/main/java/com/parse/GetFileCallback.java b/Parse/src/main/java/com/parse/GetFileCallback.java new file mode 100644 index 000000000..430fd0883 --- /dev/null +++ b/Parse/src/main/java/com/parse/GetFileCallback.java @@ -0,0 +1,41 @@ +/* + * 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 java.io.File; + +/** + * A {@code GetFileCallback} is used to run code after a {@link ParseFile} fetches its data on + * a background thread. + *

+ * The easiest way to use a {@code GetFileCallback} is through an anonymous inner class. + * Override the {@code done} function to specify what the callback should do after the fetch is + * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * file.getFileInBackground(new GetFileCallback() {
+ *   public void done(File file, ParseException e) {
+ *     // ...
+ *   }
+ * });
+ * 
+ */ +public interface GetFileCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param file + * The data that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + public void done(File file, ParseException e); +} diff --git a/Parse/src/main/java/com/parse/ParseFile.java b/Parse/src/main/java/com/parse/ParseFile.java index c8b693fc5..2080fd45c 100644 --- a/Parse/src/main/java/com/parse/ParseFile.java +++ b/Parse/src/main/java/com/parse/ParseFile.java @@ -12,7 +12,9 @@ import org.json.JSONObject; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -405,78 +407,133 @@ public void saveInBackground(SaveCallback callback) { } /** - * Synchronously gets the data for this object. You probably want to use - * {@link #getDataInBackground} instead unless you're already in a background thread. + * Synchronously gets the data from cache if available or fetches its content from the network. + * You probably want to use {@link #getDataInBackground()} instead unless you're already in a + * background thread. */ public byte[] getData() throws ParseException { return ParseTaskUtils.wait(getDataInBackground()); } - private Task getDataAsync(final ProgressCallback progressCallback, Task toAwait, - final Task cancellationToken) { + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code ProgressCallback} will be called periodically with progress updates. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the data has been fetched. + */ + public Task getDataInBackground(final ProgressCallback progressCallback) { // If data is already available, just return immediately. if (data != null) { // in-memory return Task.forResult(data); } - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } - // Wait for our turn in the queue, and return immediately if data is now available. - return toAwait.continueWithTask(new Continuation>() { + final Task.TaskCompletionSource cts = Task.create(); + currentTasks.add(cts); + + return taskQueue.enqueue(new Continuation>() { @Override - public Task then(Task task) throws Exception { + public Task then(Task toAwait) throws Exception { // If data is already available, just return immediately. if (data != null) { // in-memory return Task.forResult(data); } - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } - return getFileController().fetchAsync( - state, - null, - progressCallbackOnMainThread(progressCallback), - cancellationToken).onSuccess(new Continuation() { - @Override - public byte[] then(Task task) throws Exception { - File file = task.getResult(); - try { - data = ParseFileUtils.readFileToByteArray(file); - return data; - } catch (IOException e) { - // do nothing - } - return null; - } - }); + return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { + @Override + public byte[] then(Task task) throws Exception { + File file = task.getResult(); + try { + data = ParseFileUtils.readFileToByteArray(file); + return data; + } catch (IOException e) { + // do nothing + } + return null; + } + }); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; } }); } /** - * Gets the data for this object in a background thread. `progressCallback` is guaranteed to be - * called with 100 before dataCallback is called. + * Asynchronously gets the data from cache if available or fetches its content from the network. * - * @param progressCallback - * A ProgressCallback that is called periodically with progress updates. * @return A Task that is resolved when the data has been fetched. */ - public Task getDataInBackground(final ProgressCallback progressCallback) { + public Task getDataInBackground() { + return getDataInBackground((ProgressCallback) null); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code ProgressCallback} will be called periodically with progress updates. + * A {@code GetDataCallback} will be called when the get completes. + * + * @param dataCallback + * A {@code GetDataCallback} that is called when the get completes. + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + */ + public void getDataInBackground(GetDataCallback dataCallback, + final ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(progressCallback), dataCallback); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code GetDataCallback} will be called when the get completes. + * + * @param dataCallback + * A {@code GetDataCallback} that is called when the get completes. + */ + public void getDataInBackground(GetDataCallback dataCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(), dataCallback); + } + + /** + * Synchronously gets the file pointer from cache if available or fetches its content from the + * network. You probably want to use {@link #getFileInBackground()} instead unless you're already + * in a background thread. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + */ + public File getFile() throws ParseException { + return ParseTaskUtils.wait(getFileInBackground()); + } + + /** + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code ProgressCallback} will be called periodically with progress updates. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the file pointer of this object has been fetched. + */ + public Task getFileInBackground(final ProgressCallback progressCallback) { final Task.TaskCompletionSource cts = Task.create(); currentTasks.add(cts); - return taskQueue.enqueue(new Continuation>() { + return taskQueue.enqueue(new Continuation>() { @Override - public Task then(Task toAwait) throws Exception { - return getDataAsync(progressCallback, toAwait, cts.getTask()); + public Task then(Task toAwait) throws Exception { + return fetchInBackground(progressCallback, toAwait, cts.getTask()); } - }).continueWithTask(new Continuation>() { + }).continueWithTask(new Continuation>() { @Override - public Task then(Task task) throws Exception { + public Task then(Task task) throws Exception { cts.trySetResult(null); // release currentTasks.remove(cts); return task; @@ -485,37 +542,154 @@ public Task then(Task task) throws Exception { } /** - * Gets the data for this object in a background thread. `progressCallback` is guaranteed to be - * called with 100 before dataCallback is called. + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. * * @return A Task that is resolved when the data has been fetched. */ - public Task getDataInBackground() { - return getDataInBackground((ProgressCallback) null); + public Task getFileInBackground() { + return getFileInBackground((ProgressCallback)null); } /** - * Gets the data for this object in a background thread. `progressCallback` is guaranteed to be - * called with 100 before dataCallback is called. + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code GetFileCallback} will be called when the get completes. + * The {@code ProgressCallback} will be called periodically with progress updates. + * The {@code ProgressCallback} is guaranteed to be called with 100 before the + * {@code GetFileCallback} is called. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. * - * @param dataCallback - * A GetDataCallback that is called when the get completes. + * @param fileCallback + * A {@code GetFileCallback} that is called when the get completes. * @param progressCallback - * A ProgressCallback that is called periodically with progress updates. + * A {@code ProgressCallback} that is called periodically with progress updates. */ - public void getDataInBackground(GetDataCallback dataCallback, + public void getFileInBackground(GetFileCallback fileCallback, final ProgressCallback progressCallback) { - ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(progressCallback), dataCallback); + ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(progressCallback), fileCallback); } /** - * Gets the data for this object in a background thread. + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code GetFileCallback} will be called when the get completes. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. * - * @param dataCallback - * A GetDataCallback that is called when the get completes. + * @param fileCallback + * A {@code GetFileCallback} that is called when the get completes. */ - public void getDataInBackground(GetDataCallback dataCallback) { - ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(), dataCallback); + public void getFileInBackground(GetFileCallback fileCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(), fileCallback); + } + + /** + * Synchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * You probably want to use {@link #getDataStreamInBackground} instead unless you're already in a + * background thread. + */ + public InputStream getDataStream() throws ParseException { + return ParseTaskUtils.wait(getDataStreamInBackground()); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code ProgressCallback} will be called periodically with progress updates. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the data stream of this object has been fetched. + */ + public Task getDataStreamInBackground(final ProgressCallback progressCallback) { + final Task.TaskCompletionSource cts = Task.create(); + currentTasks.add(cts); + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { + @Override + public InputStream then(Task task) throws Exception { + return new FileInputStream(task.getResult()); + } + }); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; + } + }); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * + * @return A Task that is resolved when the data stream has been fetched. + */ + public Task getDataStreamInBackground() { + return getDataStreamInBackground((ProgressCallback) null); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code GetDataStreamCallback} will be called when the get completes. The + * {@code ProgressCallback} will be called periodically with progress updates. The + * {@code ProgressCallback} is guaranteed to be called with 100 before + * {@code GetDataStreamCallback} is called. + * + * @param dataStreamCallback + * A {@code GetDataStreamCallback} that is called when the get completes. + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + */ + public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback, + final ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync( + getDataStreamInBackground(progressCallback), dataStreamCallback); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code GetDataStreamCallback} will be called when the get completes. + * + * @param dataStreamCallback + * A {@code GetDataStreamCallback} that is called when the get completes. + */ + public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataStreamInBackground(), dataStreamCallback); + } + + private Task fetchInBackground( + final ProgressCallback progressCallback, + Task toAwait, + final Task cancellationToken) { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + return getFileController().fetchAsync( + state, + null, + progressCallbackOnMainThread(progressCallback), + cancellationToken); + } + }); } /** @@ -534,7 +708,6 @@ public void cancel() { /* * Encode/Decode */ - @SuppressWarnings("unused") /* package */ ParseFile(JSONObject json, ParseDecoder decoder) { this(new State.Builder().name(json.optString("name")).url(json.optString("url")).build()); diff --git a/Parse/src/test/java/com/parse/ParseFileTest.java b/Parse/src/test/java/com/parse/ParseFileTest.java index 668545504..0df277a0f 100644 --- a/Parse/src/test/java/com/parse/ParseFileTest.java +++ b/Parse/src/test/java/com/parse/ParseFileTest.java @@ -17,6 +17,7 @@ import org.mockito.Matchers; import java.io.File; +import java.io.InputStream; import java.util.Arrays; import java.util.List; @@ -28,6 +29,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -254,7 +256,112 @@ public void testSaveAsyncSuccessWithFile() throws Exception { //endregion - // TODO(grantland): testGetDataAsync (same as saveAsync) + + //region testGetDataAsync + + @Test + public void testGetDataAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + byte[] data = ParseTaskUtils.wait(parseFile.getDataInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), data); + } + + @Test + public void testGetDataStreamAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + InputStream dataStream = ParseTaskUtils.wait(parseFile.getDataStreamInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(dataStream)); + } + + @Test + public void testGetFileAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + File fetchedFile = ParseTaskUtils.wait(parseFile.getFileInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseFileUtils.readFileToByteArray(fetchedFile)); + } + + //endregion @Test public void testTaskQueuedMethods() throws Exception {