ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFile.java

775 lines
26 KiB
Java

/*
* 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.os.Parcel;
import android.os.Parcelable;
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;
import bolts.TaskCompletionSource;
/**
* {@code ParseFile} is a local representation of a file that is saved to the Parse cloud.
* <p/>
* 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}.
* <p/>
* Example:
* <pre>
* ParseFile file = new ParseFile("hello".getBytes());
* file.save();
*
* ParseObject object = new ParseObject("TestObject");
* object.put("file", file);
* object.save();
* </pre>
*/
public class ParseFile implements Parcelable {
/* 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<Void>() {
@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());
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());
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);
}
/**
* Creates a new file instance from a {@link Parcel} source. This is used when unparceling
* a non-dirty ParseFile. Subclasses that need Parcelable behavior should provide their own
* {@link android.os.Parcelable.Creator} and override this constructor.
*
* @param source
* the source Parcel
*/
protected ParseFile(Parcel source) {
this(source, ParseParcelDecoder.get());
}
/**
* Creates a new file instance from a {@link Parcel} using the given {@link ParseParcelDecoder}.
* The decoder is currently unused, but it might be in the future, plus this is the pattern we
* are using in parcelable classes.
*
* @param source the parcel
* @param decoder the decoder
*/
ParseFile(Parcel source, ParseParcelDecoder decoder) {
this(new State.Builder()
.url(source.readString())
.name(source.readString())
.mimeType(source.readByte() == 1 ? source.readString() : null)
.build());
}
/* 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<Void> saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback,
Task<Void> toAwait, final Task<Void> 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<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
if (!isDirty()) {
return Task.forResult(null);
}
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
Task<ParseFile.State> 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<State, Task<Void>>() {
@Override
public Task<Void> then(Task<State> 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<Void> saveInBackground(final ProgressCallback uploadProgressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
final String sessionToken = task.getResult();
return saveAsync(sessionToken, uploadProgressCallback, cts.getTask());
}
}).continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
cts.trySetResult(null); // release
currentTasks.remove(cts);
return task;
}
});
}
/* package */ Task<Void> saveAsync(final String sessionToken,
final ProgressCallback uploadProgressCallback, final Task<Void> cancellationToken) {
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> 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<Void> 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<byte[]> getDataInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<byte[]>>() {
@Override
public Task<byte[]> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation<File, byte[]>() {
@Override
public byte[] then(Task<File> task) throws Exception {
File file = task.getResult();
try {
return ParseFileUtils.readFileToByteArray(file);
} catch (IOException e) {
// do nothing
}
return null;
}
});
}
}).continueWithTask(new Continuation<byte[], Task<byte[]>>() {
@Override
public Task<byte[]> then(Task<byte[]> 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<byte[]> 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.
* <strong>Note: </strong> 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.
* <strong>Note: </strong> 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<File> getFileInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<File>>() {
@Override
public Task<File> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask());
}
}).continueWithTask(new Continuation<File, Task<File>>() {
@Override
public Task<File> then(Task<File> 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.
* <strong>Note: </strong> 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<File> 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.
* <strong>Note: </strong> 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.
* <strong>Note: </strong> 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<InputStream> getDataStreamInBackground(final ProgressCallback progressCallback) {
final TaskCompletionSource<Void> cts = new TaskCompletionSource<>();
currentTasks.add(cts);
return taskQueue.enqueue(new Continuation<Void, Task<InputStream>>() {
@Override
public Task<InputStream> then(Task<Void> toAwait) throws Exception {
return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation<File, InputStream>() {
@Override
public InputStream then(Task<File> task) throws Exception {
return new FileInputStream(task.getResult());
}
});
}
}).continueWithTask(new Continuation<InputStream, Task<InputStream>>() {
@Override
public Task<InputStream> then(Task<InputStream> 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<InputStream> 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<File> fetchInBackground(
final ProgressCallback progressCallback,
Task<Void> toAwait,
final Task<Void> cancellationToken) {
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
return toAwait.continueWithTask(new Continuation<Void, Task<File>>() {
@Override
public Task<File> then(Task<Void> task) throws Exception {
if (cancellationToken != null && cancellationToken.isCancelled()) {
return Task.cancelled();
}
return getFileController().fetchAsync(
state,
null,
progressCallbackOnMainThread(progressCallback),
cancellationToken);
}
});
}
/**
* Cancels the operations for this {@code ParseFile} if they are still in the task queue. However,
* if a network request has already been started for an operation, the network request will not
* be canceled.
*/
//TODO (grantland): Deprecate and replace with CancellationToken
public void cancel() {
Set<TaskCompletionSource<?>> tasks = new HashSet<>(currentTasks);
for (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;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcel(dest, ParseParcelEncoder.get());
}
void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
if (isDirty()) {
throw new RuntimeException("Unable to parcel an unsaved ParseFile.");
}
dest.writeString(getUrl()); // Not null
dest.writeString(getName()); // Not null
String type = state.mimeType(); // Nullable
dest.writeByte(type != null ? (byte) 1 : 0);
if (type != null) {
dest.writeString(type);
}
}
public final static Creator<ParseFile> CREATOR = new Creator<ParseFile>() {
@Override
public ParseFile createFromParcel(Parcel source) {
return new ParseFile(source);
}
@Override
public ParseFile[] newArray(int size) {
return new ParseFile[size];
}
};
}