com.parse.ParseFile Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of parse-android Show documentation
Show all versions of parse-android Show documentation
A library that gives you access to the powerful Parse cloud platform from your Android app.
/*
* 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 org.json.JSONException;
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;
import java.util.concurrent.Callable;
import bolts.Continuation;
import bolts.Task;
/**
* {@code ParseFile} is a local representation of a file that is saved to the Parse cloud.
*
* The workflow is to construct a {@code ParseFile} with data and optionally a filename. Then save
* it and set it as a field on a {@link ParseObject}.
*
* Example:
*
* ParseFile file = new ParseFile("hello".getBytes());
* file.save();
*
* ParseObject object = new ParseObject("TestObject");
* object.put("file", file);
* object.save();
*
*/
public class ParseFile {
// We limit the size of ParseFile data to be 10mb.
/* package */ static final int MAX_FILE_SIZE = 10 * 1048576;
/* package for tests */ static ParseFileController getFileController() {
return ParseCorePlugins.getInstance().getFileController();
}
private static ProgressCallback progressCallbackOnMainThread(
final ProgressCallback progressCallback) {
if (progressCallback == null) {
return null;
}
return new ProgressCallback() {
@Override
public void done(final Integer percentDone) {
Task.call(new Callable() {
@Override
public Void call() throws Exception {
progressCallback.done(percentDone);
return null;
}
}, ParseExecutors.main());
}
};
}
/* package */ static class State {
/* package */ static class Builder {
private String name;
private String mimeType;
private String url;
public Builder() {
// do nothing
}
public Builder(State state) {
name = state.name();
mimeType = state.mimeType();
url = state.url();
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
public Builder url(String url) {
this.url = url;
return this;
}
public State build() {
return new State(this);
}
}
private final String name;
private final String contentType;
private final String url;
private State(Builder builder) {
name = builder.name != null ? builder.name : "file";
contentType = builder.mimeType;
url = builder.url;
}
public String name() {
return name;
}
public String mimeType() {
return contentType;
}
public String url() {
return url;
}
}
private State state;
/**
* Staging of {@code ParseFile}'s data is stored in memory until the {@code ParseFile} has been
* successfully synced with the server.
*/
/* package for tests */ byte[] data;
/* package for tests */ File file;
/* package for tests */ final TaskQueue taskQueue = new TaskQueue();
private Set.TaskCompletionSource> currentTasks = Collections.synchronizedSet(
new HashSet.TaskCompletionSource>());
/**
* Creates a new file from a file pointer.
*
* @param file
* The file.
*/
public ParseFile(File file) {
this(file, null);
}
/**
* Creates a new file from a file pointer, and content type. Content type will be used instead of
* auto-detection by file extension.
*
* @param file
* The file.
* @param contentType
* The file's content type.
*/
public ParseFile(File file, String contentType) {
this(new State.Builder().name(file.getName()).mimeType(contentType).build());
if (file.length() > MAX_FILE_SIZE) {
throw new IllegalArgumentException(String.format("ParseFile must be less than %d bytes",
MAX_FILE_SIZE));
}
this.file = file;
}
/**
* Creates a new file from a byte array, 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 data
* The file's data.
* @param contentType
* The file's content type.
*/
public ParseFile(String name, byte[] data, String contentType) {
this(new State.Builder().name(name).mimeType(contentType).build());
if (data.length > MAX_FILE_SIZE) {
throw new IllegalArgumentException(String.format("ParseFile must be less than %d bytes",
MAX_FILE_SIZE));
}
this.data = data;
}
/**
* Creates a new file from a byte array.
*
* @param data
* The file's data.
*/
public ParseFile(byte[] data) {
this(null, data, null);
}
/**
* Creates a new file from a byte array and a name. Giving a name with a proper file extension
* (e.g. ".png") is ideal because it allows Parse to deduce the content type of the file and set
* appropriate HTTP headers when it is fetched.
*
* @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 data
* The file's data.
*/
public ParseFile(String name, byte[] data) {
this(name, data, null);
}
/**
* Creates a new file from a byte array, and content type. Content type will be used instead of
* auto-detection by file extension.
*
* @param data
* The file's data.
* @param contentType
* The file's content type.
*/
public ParseFile(byte[] data, String contentType) {
this(null, data, contentType);
}
/* package for tests */ ParseFile(State state) {
this.state = state;
}
/* package for tests */ State getState() {
return state;
}
/**
* The filename. Before save is called, this is just the filename given by the user (if any).
* After save is called, that name gets prefixed with a unique identifier.
*
* @return The file's name.
*/
public String getName() {
return state.name();
}
/**
* Whether the file still needs to be saved.
*
* @return Whether the file needs to be saved.
*/
public boolean isDirty() {
return state.url() == null;
}
/**
* Whether the file has available data.
*/
public boolean isDataAvailable() {
return data != null || getFileController().isDataAvailable(state);
}
/**
* This returns the url of the file. It's only available after you save or after you get the file
* from a ParseObject.
*
* @return The url of the file.
*/
public String getUrl() {
return state.url();
}
/**
* Saves the file to the Parse cloud synchronously.
*/
public void save() throws ParseException {
ParseTaskUtils.wait(saveInBackground());
}
private Task saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback,
Task toAwait, final Task cancellationToken) {
// If the file isn't dirty, just return immediately.
if (!isDirty()) {
return Task.forResult(null);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
// Wait for our turn in the queue, then check state to decide whether to no-op.
return toAwait.continueWithTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
if (!isDirty()) {
return Task.forResult(null);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
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);
}
return saveTask.onSuccessTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
state = task.getResult();
// Since we have successfully uploaded the file, we do not need to hold the file pointer
// anymore.
data = null;
file = null;
return task.makeVoid();
}
});
}
});
}
/**
* Saves the file to the Parse cloud in a background thread.
* `progressCallback` is guaranteed to be called with 100 before saveCallback is called.
*
* @param uploadProgressCallback
* A ProgressCallback that is called periodically with progress updates.
* @return A Task that will be resolved when the save completes.
*/
public Task saveInBackground(final ProgressCallback uploadProgressCallback) {
final Task.TaskCompletionSource cts = Task.create();
currentTasks.add(cts);
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
final String sessionToken = task.getResult();
return saveAsync(sessionToken, uploadProgressCallback, cts.getTask());
}
}).continueWithTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/* package */ Task saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback, final Task cancellationToken) {
return taskQueue.enqueue(new Continuation>() {
@Override
public Task then(Task toAwait) throws Exception {
return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken);
}
});
}
/**
* Saves the file to the Parse cloud in a background thread.
*
* @return A Task that will be resolved when the save completes.
*/
public Task saveInBackground() {
return saveInBackground((ProgressCallback) null);
}
/**
* Saves the file to the Parse cloud in a background thread.
* `progressCallback` is guaranteed to be called with 100 before saveCallback is called.
*
* @param saveCallback
* A SaveCallback that gets called when the save completes.
* @param progressCallback
* A ProgressCallback that is called periodically with progress updates.
*/
public void saveInBackground(final SaveCallback saveCallback,
ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(progressCallback), saveCallback);
}
/**
* Saves the file to the Parse cloud in a background thread.
*
* @param callback
* A SaveCallback that gets called when the save completes.
*/
public void saveInBackground(SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback);
}
/**
* 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());
}
/**
* 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) {
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 byte[] then(Task task) throws Exception {
File file = task.getResult();
try {
return ParseFileUtils.readFileToByteArray(file);
} 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;
}
});
}
/**
* Asynchronously gets the data from cache if available or fetches its content from the network.
*
* @return A Task that is resolved when the data has been fetched.
*/
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>() {
@Override
public Task then(Task toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask());
}
}).continueWithTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/**
* 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 getFileInBackground() {
return getFileInBackground((ProgressCallback) null);
}
/**
* 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 fileCallback
* A {@code GetFileCallback} that is called when the get completes.
* @param progressCallback
* A {@code ProgressCallback} that is called periodically with progress updates.
*/
public void getFileInBackground(GetFileCallback fileCallback,
final ProgressCallback progressCallback) {
ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(progressCallback), fileCallback);
}
/**
* 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 fileCallback
* A {@code GetFileCallback} that is called when the get completes.
*/
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);
}
});
}
/**
* Cancels the current network request and callbacks whether it's uploading or fetching data from
* the server.
*/
//TODO (grantland): Deprecate and replace with CancellationToken
public void cancel() {
Set.TaskCompletionSource> tasks = new HashSet<>(currentTasks);
for (Task>.TaskCompletionSource tcs : tasks) {
tcs.trySetCancelled();
}
currentTasks.removeAll(tasks);
}
/*
* Encode/Decode
*/
@SuppressWarnings("unused")
/* package */ ParseFile(JSONObject json, ParseDecoder decoder) {
this(new State.Builder().name(json.optString("name")).url(json.optString("url")).build());
}
/* package */ JSONObject encode() throws JSONException {
JSONObject json = new JSONObject();
json.put("__type", "File");
json.put("name", getName());
String url = getUrl();
if (url == null) {
throw new IllegalStateException("Unable to encode an unsaved ParseFile.");
}
json.put("url", getUrl());
return json;
}
}