1568 lines
58 KiB
Java
1568 lines
58 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.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.text.TextUtils;
|
|
import android.util.Pair;
|
|
|
|
import com.parse.OfflineQueryLogic.ConstraintMatcher;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import java.util.WeakHashMap;
|
|
|
|
import bolts.Capture;
|
|
import bolts.Continuation;
|
|
import bolts.Task;
|
|
import bolts.TaskCompletionSource;
|
|
|
|
/** package */ class OfflineStore {
|
|
|
|
/**
|
|
* SQLite has a max of 999 SQL variables in a single statement.
|
|
*/
|
|
private static final int MAX_SQL_VARIABLES = 999;
|
|
|
|
/**
|
|
* Extends the normal JSON -> ParseObject decoding to also deal with placeholders for new objects
|
|
* that have been saved offline.
|
|
*/
|
|
private class OfflineDecoder extends ParseDecoder {
|
|
// A map of UUID -> Task that will be finished once the given ParseObject is loaded.
|
|
// The Tasks should all be finished before decode is called.
|
|
private Map<String, Task<ParseObject>> offlineObjects;
|
|
|
|
private OfflineDecoder(Map<String, Task<ParseObject>> offlineObjects) {
|
|
this.offlineObjects = offlineObjects;
|
|
}
|
|
|
|
@Override
|
|
public Object decode(Object object) {
|
|
// If we see an offline id, make sure to decode it.
|
|
if (object instanceof JSONObject
|
|
&& ((JSONObject) object).optString("__type").equals("OfflineObject")) {
|
|
String uuid = ((JSONObject) object).optString("uuid");
|
|
return offlineObjects.get(uuid).getResult();
|
|
}
|
|
|
|
/*
|
|
* Embedded objects can't show up here, because we never stored them that way offline.
|
|
*/
|
|
|
|
return super.decode(object);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An encoder that can encode objects that are available offline. After using this encoder, you
|
|
* must call whenFinished() and wait for its result to be finished before the results of the
|
|
* encoding will be valid.
|
|
*/
|
|
private class OfflineEncoder extends ParseEncoder {
|
|
private ParseSQLiteDatabase db;
|
|
private ArrayList<Task<Void>> tasks = new ArrayList<>();
|
|
private final Object tasksLock = new Object();
|
|
|
|
/**
|
|
* Creates an encoder.
|
|
*
|
|
* @param db
|
|
* A database connection to use.
|
|
*/
|
|
public OfflineEncoder(ParseSQLiteDatabase db) {
|
|
this.db = db;
|
|
}
|
|
|
|
/**
|
|
* The results of encoding an object with this encoder will not be valid until the task returned
|
|
* by this method is finished.
|
|
*/
|
|
public Task<Void> whenFinished() {
|
|
return Task.whenAll(tasks).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> ignore) throws Exception {
|
|
synchronized (tasksLock) {
|
|
// It might be better to return an aggregate error here.
|
|
for (Task<Void> task : tasks) {
|
|
if (task.isFaulted() || task.isCancelled()) {
|
|
return task;
|
|
}
|
|
}
|
|
tasks.clear();
|
|
return Task.forResult(null);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Implements an encoding strategy for Parse Objects that uses offline ids when necessary.
|
|
*/
|
|
@Override
|
|
public JSONObject encodeRelatedObject(ParseObject object) {
|
|
try {
|
|
if (object.getObjectId() != null) {
|
|
JSONObject result = new JSONObject();
|
|
result.put("__type", "Pointer");
|
|
result.put("objectId", object.getObjectId());
|
|
result.put("className", object.getClassName());
|
|
return result;
|
|
}
|
|
|
|
final JSONObject result = new JSONObject();
|
|
result.put("__type", "OfflineObject");
|
|
synchronized (tasksLock) {
|
|
tasks.add(getOrCreateUUIDAsync(object, db).onSuccess(new Continuation<String, Void>() {
|
|
@Override
|
|
public Void then(Task<String> task) throws Exception {
|
|
result.put("uuid", task.getResult());
|
|
return null;
|
|
}
|
|
}));
|
|
}
|
|
return result;
|
|
} catch (JSONException e) {
|
|
// This can literally never happen.
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock for all members of the store.
|
|
final private Object lock = new Object();
|
|
|
|
// Helper for accessing the database.
|
|
final private OfflineSQLiteOpenHelper helper;
|
|
|
|
/**
|
|
* In-memory map of UUID -> ParseObject. This is used so that we can always return the same
|
|
* instance for a given object. The only objects in this map are ones that are in the database.
|
|
*/
|
|
final private WeakValueHashMap<String, ParseObject> uuidToObjectMap = new WeakValueHashMap<>();
|
|
|
|
/**
|
|
* In-memory map of ParseObject -> UUID. This is used to that when we see an unsaved ParseObject
|
|
* that's already in the database, we can update the same record in the database. It stores a Task
|
|
* instead of the String, because one thread may want to reserve the spot. Once the task is
|
|
* finished, there will be a row for this UUID in the database.
|
|
*/
|
|
final private WeakHashMap<ParseObject, Task<String>> objectToUuidMap = new WeakHashMap<>();
|
|
|
|
/**
|
|
* In-memory set of ParseObjects that have been fetched from the local database already. If the
|
|
* object is in the map, a fetch of it has been started. If the value is a finished task, then the
|
|
* fetch was completed.
|
|
*/
|
|
final private WeakHashMap<ParseObject, Task<ParseObject>> fetchedObjects = new WeakHashMap<>();
|
|
|
|
/**
|
|
* Used by the static method to create the singleton.
|
|
*/
|
|
/* package */ OfflineStore(Context context) {
|
|
this(new OfflineSQLiteOpenHelper(context));
|
|
}
|
|
|
|
/* package */ OfflineStore(OfflineSQLiteOpenHelper helper) {
|
|
this.helper = helper;
|
|
}
|
|
|
|
/**
|
|
* Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object
|
|
* and adds a new row to the database for the object with no data.
|
|
*/
|
|
private Task<String> getOrCreateUUIDAsync(final ParseObject object, ParseSQLiteDatabase db) {
|
|
final String newUUID = UUID.randomUUID().toString();
|
|
final TaskCompletionSource<String> tcs = new TaskCompletionSource<>();
|
|
|
|
synchronized (lock) {
|
|
Task<String> uuidTask = objectToUuidMap.get(object);
|
|
if (uuidTask != null) {
|
|
return uuidTask;
|
|
}
|
|
|
|
// The object doesn't have a UUID yet, so we're gonna have to make one.
|
|
objectToUuidMap.put(object, tcs.getTask());
|
|
uuidToObjectMap.put(newUUID, object);
|
|
fetchedObjects.put(object, tcs.getTask().onSuccess(new Continuation<String, ParseObject>() {
|
|
@Override
|
|
public ParseObject then(Task<String> task) throws Exception {
|
|
return object;
|
|
}
|
|
}));
|
|
}
|
|
|
|
/*
|
|
* We need to put a placeholder row in the database so that later on, the save can just be an
|
|
* update. This could be a pointer to an object that itself never gets saved offline, in which
|
|
* case the consumer will just have to deal with that.
|
|
*/
|
|
ContentValues values = new ContentValues();
|
|
values.put(OfflineSQLiteOpenHelper.KEY_UUID, newUUID);
|
|
values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, object.getClassName());
|
|
db.insertOrThrowAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values).continueWith(
|
|
new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
// This will signal that the UUID does represent a row in the database.
|
|
tcs.setResult(newUUID);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
return tcs.getTask();
|
|
}
|
|
|
|
/**
|
|
* Gets an unfetched pointer to an object in the db, based on its uuid. The object may or may not
|
|
* be in memory, but it must be in the database. If it is already in memory, that instance will be
|
|
* returned. Since this is only for creating pointers to objects that are referenced by other
|
|
* objects in the data store, that's a fair assumption.
|
|
*
|
|
* @param uuid
|
|
* The object to retrieve.
|
|
* @param db
|
|
* The database instance to retrieve from.
|
|
* @return The object with that UUID.
|
|
*/
|
|
private <T extends ParseObject> Task<T> getPointerAsync(final String uuid,
|
|
ParseSQLiteDatabase db) {
|
|
synchronized (lock) {
|
|
@SuppressWarnings("unchecked")
|
|
T existing = (T) uuidToObjectMap.get(uuid);
|
|
if (existing != null) {
|
|
return Task.forResult(existing);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We want to just return the pointer, but we have to look in the database to know if there's
|
|
* something with this classname and object id already.
|
|
*/
|
|
|
|
String[] select = { OfflineSQLiteOpenHelper.KEY_CLASS_NAME, OfflineSQLiteOpenHelper.KEY_OBJECT_ID };
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
|
|
String[] args = { uuid };
|
|
return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess(
|
|
new Continuation<Cursor, T>() {
|
|
@Override
|
|
public T then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = task.getResult();
|
|
cursor.moveToFirst();
|
|
if (cursor.isAfterLast()) {
|
|
cursor.close();
|
|
throw new IllegalStateException("Attempted to find non-existent uuid " + uuid);
|
|
}
|
|
|
|
synchronized (lock) {
|
|
// We need to check again since another task might have come around and added it to
|
|
// the map.
|
|
//TODO (grantland): Maybe we should insert a Task that is resolved when the query
|
|
// completes like we do in getOrCreateUUIDAsync?
|
|
@SuppressWarnings("unchecked")
|
|
T existing = (T) uuidToObjectMap.get(uuid);
|
|
if (existing != null) {
|
|
return existing;
|
|
}
|
|
|
|
String className = cursor.getString(0);
|
|
String objectId = cursor.getString(1);
|
|
cursor.close();
|
|
@SuppressWarnings("unchecked")
|
|
T pointer = (T) ParseObject.createWithoutData(className, objectId);
|
|
/*
|
|
* If it doesn't have an objectId, we don't really need the UUID, and this simplifies
|
|
* some other logic elsewhere if we only update the map for new objects.
|
|
*/
|
|
if (objectId == null) {
|
|
uuidToObjectMap.put(uuid, pointer);
|
|
objectToUuidMap.put(pointer, Task.forResult(uuid));
|
|
}
|
|
return pointer;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Runs a ParseQuery against the store's contents.
|
|
*
|
|
* @return The objects that match the query's constraints.
|
|
*/
|
|
/* package for OfflineQueryLogic */ <T extends ParseObject> Task<List<T>> findAsync(
|
|
ParseQuery.State<T> query,
|
|
ParseUser user,
|
|
ParsePin pin,
|
|
ParseSQLiteDatabase db) {
|
|
return findAsync(query, user, pin, false, db);
|
|
}
|
|
|
|
/**
|
|
* Runs a ParseQuery against the store's contents. May cause any instances of T to get fetched
|
|
* from the offline database. TODO(klimt): Should the query consider objects that are in memory,
|
|
* but not in the offline store?
|
|
*
|
|
* @param query The query.
|
|
* @param user The user making the query.
|
|
* @param pin (Optional) The pin we are querying across. If null, all pins.
|
|
* @param isCount True if we are doing a count.
|
|
* @param db The SQLiteDatabase.
|
|
* @param <T> Subclass of ParseObject.
|
|
* @return The objects that match the query's constraints.
|
|
*/
|
|
private <T extends ParseObject> Task<List<T>> findAsync(
|
|
final ParseQuery.State<T> query,
|
|
final ParseUser user,
|
|
final ParsePin pin,
|
|
final boolean isCount,
|
|
final ParseSQLiteDatabase db) {
|
|
/*
|
|
* This is currently unused, but is here to allow future querying across objects that are in the
|
|
* process of being deleted eventually.
|
|
*/
|
|
final boolean includeIsDeletingEventually = false;
|
|
|
|
final OfflineQueryLogic queryLogic = new OfflineQueryLogic(this);
|
|
|
|
final List<T> results = new ArrayList<>();
|
|
|
|
Task<Cursor> queryTask;
|
|
if (pin == null) {
|
|
String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS;
|
|
String[] select = { OfflineSQLiteOpenHelper.KEY_UUID };
|
|
String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?";
|
|
if (!includeIsDeletingEventually) {
|
|
where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0";
|
|
}
|
|
String[] args = { query.className() };
|
|
|
|
queryTask = db.queryAsync(table, select, where, args);
|
|
} else {
|
|
Task<String> uuidTask = objectToUuidMap.get(pin);
|
|
if (uuidTask == null) {
|
|
// Pin was never saved locally, therefore there won't be any results.
|
|
return Task.forResult(results);
|
|
}
|
|
|
|
queryTask = uuidTask.onSuccessTask(new Continuation<String, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<String> task) throws Exception {
|
|
String uuid = task.getResult();
|
|
|
|
String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + " A " +
|
|
" INNER JOIN " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + " B " +
|
|
" ON A." + OfflineSQLiteOpenHelper.KEY_UUID + "=B." + OfflineSQLiteOpenHelper.KEY_UUID;
|
|
String[] select = {"A." + OfflineSQLiteOpenHelper.KEY_UUID};
|
|
String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" +
|
|
" AND " + OfflineSQLiteOpenHelper.KEY_KEY + "=?";
|
|
if (!includeIsDeletingEventually) {
|
|
where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0";
|
|
}
|
|
String[] args = { query.className(), uuid };
|
|
|
|
return db.queryAsync(table, select, where, args);
|
|
}
|
|
});
|
|
}
|
|
|
|
return queryTask.onSuccessTask(new Continuation<Cursor, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = task.getResult();
|
|
List<String> uuids = new ArrayList<>();
|
|
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
|
|
uuids.add(cursor.getString(0));
|
|
}
|
|
cursor.close();
|
|
|
|
// Find objects that match the where clause.
|
|
final ConstraintMatcher<T> matcher = queryLogic.createMatcher(query, user);
|
|
|
|
Task<Void> checkedAllObjects = Task.forResult(null);
|
|
for (final String uuid : uuids) {
|
|
final Capture<T> object = new Capture<>();
|
|
|
|
checkedAllObjects = checkedAllObjects.onSuccessTask(new Continuation<Void, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<Void> task) throws Exception {
|
|
return getPointerAsync(uuid, db);
|
|
}
|
|
}).onSuccessTask(new Continuation<T, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<T> task) throws Exception {
|
|
object.set(task.getResult());
|
|
return fetchLocallyAsync(object.get(), db);
|
|
}
|
|
}).onSuccessTask(new Continuation<T, Task<Boolean>>() {
|
|
@Override
|
|
public Task<Boolean> then(Task<T> task) throws Exception {
|
|
if (!object.get().isDataAvailable()) {
|
|
return Task.forResult(false);
|
|
}
|
|
return matcher.matchesAsync(object.get(), db);
|
|
}
|
|
}).onSuccess(new Continuation<Boolean, Void>() {
|
|
@Override
|
|
public Void then(Task<Boolean> task) {
|
|
if (task.getResult()) {
|
|
results.add(object.get());
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
return checkedAllObjects;
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> then(Task<Void> task) throws Exception {
|
|
// Sort by any sort operators.
|
|
OfflineQueryLogic.sort(results, query);
|
|
|
|
// Apply the skip.
|
|
List<T> trimmedResults = results;
|
|
int skip = query.skip();
|
|
if (!isCount && skip >= 0) {
|
|
skip = Math.min(query.skip(), trimmedResults.size());
|
|
trimmedResults = trimmedResults.subList(skip, trimmedResults.size());
|
|
}
|
|
|
|
// Trim to the limit.
|
|
int limit = query.limit();
|
|
if (!isCount && limit >= 0 && trimmedResults.size() > limit) {
|
|
trimmedResults = trimmedResults.subList(0, limit);
|
|
}
|
|
|
|
// Fetch the includes.
|
|
Task<Void> fetchedIncludesTask = Task.forResult(null);
|
|
for (final T object : trimmedResults) {
|
|
fetchedIncludesTask = fetchedIncludesTask.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return OfflineQueryLogic.fetchIncludesAsync(OfflineStore.this, object, query, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
final List<T> finalTrimmedResults = trimmedResults;
|
|
return fetchedIncludesTask.onSuccess(new Continuation<Void, List<T>>() {
|
|
@Override
|
|
public List<T> then(Task<Void> task) throws Exception {
|
|
return finalTrimmedResults;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the data for the given object from the offline database. Returns a task that will be
|
|
* completed if data for the object was available. If the object is not in the cache, the task
|
|
* will be faulted, with a CACHE_MISS error.
|
|
*
|
|
* @param object
|
|
* The object to fetch.
|
|
* @param db
|
|
* A database connection to use.
|
|
*/
|
|
/* package for OfflineQueryLogic */ <T extends ParseObject> Task<T> fetchLocallyAsync(
|
|
final T object,
|
|
final ParseSQLiteDatabase db) {
|
|
final TaskCompletionSource<T> tcs = new TaskCompletionSource<>();
|
|
Task<String> uuidTask;
|
|
|
|
synchronized (lock) {
|
|
if (fetchedObjects.containsKey(object)) {
|
|
/*
|
|
* The object has already been fetched from the offline store, so any data that's in there
|
|
* is already reflected in the in-memory version. There's nothing more to do.
|
|
*/
|
|
//noinspection unchecked
|
|
return (Task<T>) fetchedObjects.get(object);
|
|
}
|
|
|
|
/*
|
|
* Put a placeholder so that anyone else who attempts to fetch this object will just wait for
|
|
* this call to finish doing it.
|
|
*/
|
|
//noinspection unchecked
|
|
fetchedObjects.put(object, (Task<ParseObject>) tcs.getTask());
|
|
|
|
uuidTask = objectToUuidMap.get(object);
|
|
}
|
|
String className = object.getClassName();
|
|
String objectId = object.getObjectId();
|
|
|
|
/*
|
|
* If this gets set, then it will contain data from the offline store that needs to be merged
|
|
* into the existing object in memory.
|
|
*/
|
|
Task<String> jsonStringTask = Task.forResult(null);
|
|
|
|
if (objectId == null) {
|
|
// This Object has never been saved to Parse.
|
|
if (uuidTask == null) {
|
|
/*
|
|
* This object was not pulled from the data store or previously saved to it, so there's
|
|
* nothing that can be fetched from it. This isn't an error, because it's really convenient
|
|
* to try to fetch objects from the offline store just to make sure they are up-to-date, and
|
|
* we shouldn't force developers to specially handle this case.
|
|
*/
|
|
} else {
|
|
/*
|
|
* This object is a new ParseObject that is known to the data store, but hasn't been
|
|
* fetched. The only way this could happen is if the object had previously been stored in
|
|
* the offline store, then the object was removed from memory (maybe by rebooting), and then
|
|
* a object with a pointer to it was fetched, so we only created the pointer. We need to
|
|
* pull the data out of the database using the UUID.
|
|
*/
|
|
final String[] select = { OfflineSQLiteOpenHelper.KEY_JSON };
|
|
final String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
|
|
final Capture<String> uuid = new Capture<>();
|
|
jsonStringTask = uuidTask.onSuccessTask(new Continuation<String, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<String> task) throws Exception {
|
|
uuid.set(task.getResult());
|
|
String[] args = { uuid.get() };
|
|
return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args);
|
|
}
|
|
}).onSuccess(new Continuation<Cursor, String>() {
|
|
@Override
|
|
public String then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = task.getResult();
|
|
cursor.moveToFirst();
|
|
if (cursor.isAfterLast()) {
|
|
cursor.close();
|
|
throw new IllegalStateException("Attempted to find non-existent uuid " + uuid.get());
|
|
}
|
|
String json = cursor.getString(0);
|
|
cursor.close();
|
|
|
|
return json;
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
if (uuidTask != null) {
|
|
/*
|
|
* This object is an existing ParseObject, and we must've already pulled its data out of the
|
|
* offline store, or else we wouldn't know its UUID. This should never happen.
|
|
*/
|
|
tcs.setError(new IllegalStateException("This object must have already been "
|
|
+ "fetched from the local datastore, but isn't marked as fetched."));
|
|
synchronized (lock) {
|
|
// Forget we even tried to fetch this object, so that retries will actually... retry.
|
|
fetchedObjects.remove(object);
|
|
}
|
|
return tcs.getTask();
|
|
}
|
|
|
|
/*
|
|
* We've got a pointer to an existing ParseObject, but we've never pulled its data out of the
|
|
* offline store. Since fetching from the server forces a fetch from the offline store, that
|
|
* means this is a pointer. We need to try to find any existing entry for this object in the
|
|
* database.
|
|
*/
|
|
String[] select = { OfflineSQLiteOpenHelper.KEY_JSON, OfflineSQLiteOpenHelper.KEY_UUID };
|
|
String where =
|
|
String.format("%s = ? AND %s = ?", OfflineSQLiteOpenHelper.KEY_CLASS_NAME,
|
|
OfflineSQLiteOpenHelper.KEY_OBJECT_ID);
|
|
String[] args = { className, objectId };
|
|
jsonStringTask =
|
|
db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess(
|
|
new Continuation<Cursor, String>() {
|
|
@Override
|
|
public String then(Task<Cursor> task) throws Exception {
|
|
Cursor cursor = task.getResult();
|
|
cursor.moveToFirst();
|
|
if (cursor.isAfterLast()) {
|
|
/*
|
|
* This is a pointer that came from Parse that references an object that has
|
|
* never been saved in the offline store before. This just means there's no data
|
|
* in the store that needs to be merged into the object.
|
|
*/
|
|
cursor.close();
|
|
throw new ParseException(ParseException.CACHE_MISS,
|
|
"This object is not available in the offline cache.");
|
|
}
|
|
|
|
// we should fetch its data and record its UUID for future reference.
|
|
String jsonString = cursor.getString(0);
|
|
String newUUID = cursor.getString(1);
|
|
cursor.close();
|
|
|
|
synchronized (lock) {
|
|
/*
|
|
* It's okay to put this object into the uuid map. No one will try to fetch
|
|
* it, because it's already in the fetchedObjects map. And no one will try to
|
|
* save to it without fetching it first, so everything should be just fine.
|
|
*/
|
|
objectToUuidMap.put(object, Task.forResult(newUUID));
|
|
uuidToObjectMap.put(newUUID, object);
|
|
}
|
|
|
|
return jsonString;
|
|
}
|
|
});
|
|
}
|
|
|
|
return jsonStringTask.onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String jsonString = task.getResult();
|
|
if (jsonString == null) {
|
|
/*
|
|
* This means we tried to fetch an object from the database that was never actually saved
|
|
* locally. This probably means that its parent object was saved locally and we just
|
|
* created a pointer to this object. This should be considered a cache miss.
|
|
*/
|
|
return Task.forError(new ParseException(ParseException.CACHE_MISS,
|
|
"Attempted to fetch an object offline which was never saved to the offline cache."));
|
|
}
|
|
final JSONObject json;
|
|
try {
|
|
/*
|
|
* We can assume that whatever is in the database is the last known server state. The only
|
|
* things to maintain from the in-memory object are any changes since the object was last
|
|
* put in the database.
|
|
*/
|
|
json = new JSONObject(jsonString);
|
|
} catch (JSONException e) {
|
|
return Task.forError(e);
|
|
}
|
|
|
|
// Fetch all the offline objects before we decode.
|
|
final Map<String, Task<ParseObject>> offlineObjects = new HashMap<>();
|
|
|
|
(new ParseTraverser() {
|
|
@Override
|
|
protected boolean visit(Object object) {
|
|
if (object instanceof JSONObject
|
|
&& ((JSONObject) object).optString("__type").equals("OfflineObject")) {
|
|
String uuid = ((JSONObject) object).optString("uuid");
|
|
offlineObjects.put(uuid, getPointerAsync(uuid, db));
|
|
}
|
|
return true;
|
|
}
|
|
}).setTraverseParseObjects(false).setYieldRoot(false).traverse(json);
|
|
|
|
return Task.whenAll(offlineObjects.values()).onSuccess(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
object.mergeREST(object.getState(), json, new OfflineDecoder(offlineObjects));
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<Void> task) throws Exception {
|
|
if (task.isCancelled()) {
|
|
tcs.setCancelled();
|
|
} else if (task.isFaulted()) {
|
|
tcs.setError(task.getError());
|
|
} else {
|
|
tcs.setResult(object);
|
|
}
|
|
return tcs.getTask();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the data for the given object from the offline database. Returns a task that will be
|
|
* completed if data for the object was available. If the object is not in the cache, the task
|
|
* will be faulted, with a CACHE_MISS error.
|
|
*
|
|
* @param object
|
|
* The object to fetch.
|
|
*/
|
|
/* package */ <T extends ParseObject> Task<T> fetchLocallyAsync(final T object) {
|
|
return runWithManagedConnection(new SQLiteDatabaseCallable<Task<T>>() {
|
|
@Override
|
|
public Task<T> call(ParseSQLiteDatabase db) {
|
|
return fetchLocallyAsync(object, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stores a single object in the local database. If the object is a pointer, isn't dirty, and has
|
|
* an objectId already, it may not be saved, since it would provide no useful data.
|
|
*
|
|
* @param object
|
|
* The object to save.
|
|
* @param db
|
|
* A database connection to use.
|
|
*/
|
|
private Task<Void> saveLocallyAsync(
|
|
final String key, final ParseObject object, final ParseSQLiteDatabase db) {
|
|
// If this is just a clean, unfetched pointer known to Parse, then there is nothing to save.
|
|
if (object.getObjectId() != null && !object.isDataAvailable() && !object.hasChanges()
|
|
&& !object.hasOutstandingOperations()) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
final Capture<String> uuidCapture = new Capture<>();
|
|
|
|
// Make sure we have a UUID for the object to be saved.
|
|
return getOrCreateUUIDAsync(object, db).onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String uuid = task.getResult();
|
|
uuidCapture.set(uuid);
|
|
return updateDataForObjectAsync(uuid, object, db);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
final ContentValues values = new ContentValues();
|
|
values.put(OfflineSQLiteOpenHelper.KEY_KEY, key);
|
|
values.put(OfflineSQLiteOpenHelper.KEY_UUID, uuidCapture.get());
|
|
return db.insertWithOnConflict(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, values,
|
|
SQLiteDatabase.CONFLICT_IGNORE);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stores an object (and optionally, every object it points to recursively) in the local database.
|
|
* If any of the objects have not been fetched from Parse, they will not be stored. However, if
|
|
* they have changed data, the data will be retained. To get the objects back later, you can use a
|
|
* ParseQuery with a cache policy that uses the local cache, or you can create an unfetched
|
|
* pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. If you modify
|
|
* the object after saving it locally, such as by fetching it or saving it, those changes will
|
|
* automatically be applied to the cache.
|
|
*
|
|
* Any objects previously stored with the same key will be removed from the local database.
|
|
*
|
|
* @param object Root object
|
|
* @param includeAllChildren {@code true} to recursively save all pointers.
|
|
* @param db DB connection
|
|
* @return A Task that will be resolved when saving is complete
|
|
*/
|
|
private Task<Void> saveLocallyAsync(
|
|
final ParseObject object, final boolean includeAllChildren, final ParseSQLiteDatabase db) {
|
|
final ArrayList<ParseObject> objectsInTree = new ArrayList<>();
|
|
// Fetch all objects locally in case they are being re-added
|
|
if (!includeAllChildren) {
|
|
objectsInTree.add(object);
|
|
} else {
|
|
(new ParseTraverser() {
|
|
@Override
|
|
protected boolean visit(Object object) {
|
|
if (object instanceof ParseObject) {
|
|
objectsInTree.add((ParseObject) object);
|
|
}
|
|
return true;
|
|
}
|
|
}).setYieldRoot(true).setTraverseParseObjects(true).traverse(object);
|
|
}
|
|
|
|
return saveLocallyAsync(object, objectsInTree, db);
|
|
}
|
|
|
|
|
|
private Task<Void> saveLocallyAsync(
|
|
final ParseObject object, List<ParseObject> children, final ParseSQLiteDatabase db) {
|
|
final List<ParseObject> objects = children != null
|
|
? new ArrayList<>(children)
|
|
: new ArrayList<ParseObject>();
|
|
if (!objects.contains(object)) {
|
|
objects.add(object);
|
|
}
|
|
|
|
// Call saveLocallyAsync for each of them individually.
|
|
final List<Task<Void>> tasks = new ArrayList<>();
|
|
for (ParseObject obj : objects) {
|
|
tasks.add(fetchLocallyAsync(obj, db).makeVoid());
|
|
}
|
|
|
|
return Task.whenAll(tasks).continueWithTask(new Continuation<Void, Task<String>>() {
|
|
@Override
|
|
public Task<String> then(Task<Void> task) throws Exception {
|
|
return objectToUuidMap.get(object);
|
|
}
|
|
}).onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String uuid = task.getResult();
|
|
if (uuid == null) {
|
|
// The root object was never stored in the offline store, so nothing to unpin.
|
|
return null;
|
|
}
|
|
|
|
// Delete all objects locally corresponding to the key we're trying to use in case it was
|
|
// used before (overwrite)
|
|
return unpinAsync(uuid, db);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<String>>() {
|
|
@Override
|
|
public Task<String> then(Task<Void> task) throws Exception {
|
|
return getOrCreateUUIDAsync(object, db);
|
|
}
|
|
}).onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String uuid = task.getResult();
|
|
|
|
// Call saveLocallyAsync for each of them individually.
|
|
final List<Task<Void>> tasks = new ArrayList<>();
|
|
for (ParseObject obj : objects) {
|
|
tasks.add(saveLocallyAsync(uuid, obj, db));
|
|
}
|
|
|
|
return Task.whenAll(tasks);
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> unpinAsync(final ParseObject object, final ParseSQLiteDatabase db) {
|
|
Task<String> uuidTask = objectToUuidMap.get(object);
|
|
if (uuidTask == null) {
|
|
// The root object was never stored in the offline store, so nothing to unpin.
|
|
return Task.forResult(null);
|
|
}
|
|
return uuidTask.continueWithTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
final String uuid = task.getResult();
|
|
if (uuid == null) {
|
|
// The root object was never stored in the offline store, so nothing to unpin.
|
|
return Task.forResult(null);
|
|
}
|
|
return unpinAsync(uuid, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> unpinAsync(final String key, final ParseSQLiteDatabase db) {
|
|
final List<String> uuidsToDelete = new LinkedList<>();
|
|
// A continueWithTask that ends with "return task" is essentially a try-finally.
|
|
return Task.forResult((Void) null).continueWithTask(new Continuation<Void, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<Void> task) throws Exception {
|
|
// Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1
|
|
String sql = "SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES +
|
|
" WHERE " + OfflineSQLiteOpenHelper.KEY_KEY + "=? AND " + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" +
|
|
" SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES +
|
|
" GROUP BY " + OfflineSQLiteOpenHelper.KEY_UUID +
|
|
" HAVING COUNT(" + OfflineSQLiteOpenHelper.KEY_UUID + ")=1" +
|
|
")";
|
|
String[] args = {key};
|
|
return db.rawQueryAsync(sql, args);
|
|
}
|
|
}).onSuccessTask(new Continuation<Cursor, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Cursor> task) throws Exception {
|
|
// DELETE FROM Objects
|
|
|
|
Cursor cursor = task.getResult();
|
|
while (cursor.moveToNext()) {
|
|
uuidsToDelete.add(cursor.getString(0));
|
|
}
|
|
cursor.close();
|
|
|
|
return deleteObjects(uuidsToDelete, db);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// DELETE FROM Dependencies
|
|
String where = OfflineSQLiteOpenHelper.KEY_KEY + "=?";
|
|
String[] args = {key};
|
|
return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args);
|
|
}
|
|
}).onSuccess(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
synchronized (lock) {
|
|
// Remove uuids from memory
|
|
for (String uuid : uuidsToDelete) {
|
|
ParseObject object = uuidToObjectMap.get(uuid);
|
|
if (object != null) {
|
|
objectToUuidMap.remove(object);
|
|
uuidToObjectMap.remove(uuid);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> deleteObjects(final List<String> uuids, final ParseSQLiteDatabase db) {
|
|
if (uuids.size() <= 0) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
// SQLite has a max 999 SQL variables in a statement, so we need to split it up into manageable
|
|
// chunks. We can do this because we're already in a transaction.
|
|
if (uuids.size() > MAX_SQL_VARIABLES) {
|
|
return deleteObjects(uuids.subList(0, MAX_SQL_VARIABLES), db).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return deleteObjects(uuids.subList(MAX_SQL_VARIABLES, uuids.size()), db);
|
|
}
|
|
});
|
|
}
|
|
|
|
String[] placeholders = new String[uuids.size()];
|
|
for (int i = 0; i < placeholders.length; i++) {
|
|
placeholders[i] = "?";
|
|
}
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + TextUtils.join(",", placeholders) + ")";
|
|
// dynamic args
|
|
String[] args = uuids.toArray(new String[uuids.size()]);
|
|
return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args);
|
|
}
|
|
|
|
/**
|
|
* Takes an object that has been fetched from the database before and updates it with whatever
|
|
* data is in memory. This will only be used when data comes back from the server after a fetch or
|
|
* a save.
|
|
*/
|
|
/* package */ Task<Void> updateDataForObjectAsync(final ParseObject object) {
|
|
Task<ParseObject> fetched;
|
|
// Make sure the object is fetched.
|
|
synchronized (lock) {
|
|
fetched = fetchedObjects.get(object);
|
|
if (fetched == null) {
|
|
return Task.forError(new IllegalStateException(
|
|
"An object cannot be updated if it wasn't fetched."));
|
|
}
|
|
}
|
|
return fetched.continueWithTask(new Continuation<ParseObject, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParseObject> task) throws Exception {
|
|
if (task.isFaulted()) {
|
|
// Catch CACHE_MISS
|
|
//noinspection ThrowableResultOfMethodCallIgnored
|
|
if (task.getError() instanceof ParseException
|
|
&& ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) {
|
|
return Task.forResult(null);
|
|
}
|
|
return task.makeVoid();
|
|
}
|
|
|
|
return helper.getWritableDatabaseAsync().continueWithTask(new Continuation<ParseSQLiteDatabase, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParseSQLiteDatabase> task) throws Exception {
|
|
final ParseSQLiteDatabase db = task.getResult();
|
|
return db.beginTransactionAsync().onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return updateDataForObjectAsync(object, db).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return db.setTransactionSuccessfulAsync();
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
// } finally {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
db.endTransactionAsync();
|
|
db.closeAsync();
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> updateDataForObjectAsync(
|
|
final ParseObject object,
|
|
final ParseSQLiteDatabase db) {
|
|
// Make sure the object has a UUID.
|
|
Task<String> uuidTask;
|
|
synchronized (lock) {
|
|
uuidTask = objectToUuidMap.get(object);
|
|
if (uuidTask == null) {
|
|
// It was fetched, but it has no UUID. That must mean it isn't actually in the database.
|
|
return Task.forResult(null);
|
|
}
|
|
}
|
|
return uuidTask.onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String uuid = task.getResult();
|
|
return updateDataForObjectAsync(uuid, object, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> updateDataForObjectAsync(
|
|
final String uuid,
|
|
final ParseObject object,
|
|
final ParseSQLiteDatabase db) {
|
|
// Now actually encode the object as JSON.
|
|
OfflineEncoder encoder = new OfflineEncoder(db);
|
|
final JSONObject json = object.toRest(encoder);
|
|
|
|
return encoder.whenFinished().onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// Put the JSON in the database.
|
|
String className = object.getClassName();
|
|
String objectId = object.getObjectId();
|
|
int isDeletingEventually = json.getInt(ParseObject.KEY_IS_DELETING_EVENTUALLY);
|
|
|
|
final ContentValues values = new ContentValues();
|
|
values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, className);
|
|
values.put(OfflineSQLiteOpenHelper.KEY_JSON, json.toString());
|
|
if (objectId != null) {
|
|
values.put(OfflineSQLiteOpenHelper.KEY_OBJECT_ID, objectId);
|
|
}
|
|
values.put(OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY, isDeletingEventually);
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
|
|
String[] args = {uuid};
|
|
return db.updateAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values, where, args).makeVoid();
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ Task<Void> deleteDataForObjectAsync(final ParseObject object) {
|
|
return helper.getWritableDatabaseAsync().continueWithTask(new Continuation<ParseSQLiteDatabase, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParseSQLiteDatabase> task) throws Exception {
|
|
final ParseSQLiteDatabase db = task.getResult();
|
|
return db.beginTransactionAsync().onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return deleteDataForObjectAsync(object, db).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return db.setTransactionSuccessfulAsync();
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
// } finally {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
db.endTransactionAsync();
|
|
db.closeAsync();
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> deleteDataForObjectAsync(final ParseObject object, final ParseSQLiteDatabase db) {
|
|
final Capture<String> uuid = new Capture<>();
|
|
|
|
// Make sure the object has a UUID.
|
|
Task<String> uuidTask;
|
|
synchronized (lock) {
|
|
uuidTask = objectToUuidMap.get(object);
|
|
if (uuidTask == null) {
|
|
// It was fetched, but it has no UUID. That must mean it isn't actually in the database.
|
|
return Task.forResult(null);
|
|
}
|
|
}
|
|
uuidTask = uuidTask.onSuccessTask(new Continuation<String, Task<String>>() {
|
|
@Override
|
|
public Task<String> then(Task<String> task) throws Exception {
|
|
uuid.set(task.getResult());
|
|
return task;
|
|
}
|
|
});
|
|
|
|
// If the object was the root of a pin, unpin it.
|
|
Task<Void> unpinTask = uuidTask.onSuccessTask(new Continuation<String, Task<Cursor>>() {
|
|
@Override
|
|
public Task<Cursor> then(Task<String> task) throws Exception {
|
|
// Find all the roots for this object.
|
|
String[] select = { OfflineSQLiteOpenHelper.KEY_KEY };
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
|
|
String[] args = { uuid.get() };
|
|
return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, select, where, args);
|
|
}
|
|
}).onSuccessTask(new Continuation<Cursor, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Cursor> task) throws Exception {
|
|
// Try to unpin this object from the pin label if it's a root of the ParsePin.
|
|
Cursor cursor = task.getResult();
|
|
List<String> uuids = new ArrayList<>();
|
|
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
|
|
uuids.add(cursor.getString(0));
|
|
}
|
|
cursor.close();
|
|
|
|
List<Task<Void>> tasks = new ArrayList<>();
|
|
for (final String uuid : uuids) {
|
|
Task<Void> unpinTask = getPointerAsync(uuid, db).onSuccessTask(new Continuation<ParseObject, Task<ParsePin>>() {
|
|
@Override
|
|
public Task<ParsePin> then(Task<ParseObject> task) throws Exception {
|
|
ParsePin pin = (ParsePin) task.getResult();
|
|
return fetchLocallyAsync(pin, db);
|
|
}
|
|
}).continueWithTask(new Continuation<ParsePin, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParsePin> task) throws Exception {
|
|
ParsePin pin = task.getResult();
|
|
|
|
List<ParseObject> modified = pin.getObjects();
|
|
if (modified == null || !modified.contains(object)) {
|
|
return task.makeVoid();
|
|
}
|
|
|
|
modified.remove(object);
|
|
if (modified.size() == 0) {
|
|
return unpinAsync(uuid, db);
|
|
}
|
|
|
|
pin.setObjects(modified);
|
|
return saveLocallyAsync(pin, true, db);
|
|
}
|
|
});
|
|
tasks.add(unpinTask);
|
|
}
|
|
|
|
return Task.whenAll(tasks);
|
|
}
|
|
});
|
|
|
|
// Delete the object from the Local Datastore in case it wasn't the root of a pin.
|
|
return unpinTask.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
|
|
String[] args = {uuid.get()};
|
|
return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
|
|
String[] args = {uuid.get()};
|
|
return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
synchronized (lock) {
|
|
// Clean up
|
|
//TODO (grantland): we should probably clean up uuidToObjectMap and objectToUuidMap, but
|
|
// getting the uuid requires a task and things might get a little funky...
|
|
fetchedObjects.remove(object);
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
//region ParsePin
|
|
|
|
private Task<ParsePin> getParsePin(final String name, ParseSQLiteDatabase db) {
|
|
ParseQuery.State<ParsePin> query = new ParseQuery.State.Builder<>(ParsePin.class)
|
|
.whereEqualTo(ParsePin.KEY_NAME, name)
|
|
.build();
|
|
|
|
/* We need to call directly to the OfflineStore since we don't want/need a user to query for
|
|
* ParsePins
|
|
*/
|
|
return findAsync(query, null, null, db).onSuccess(new Continuation<List<ParsePin>, ParsePin>() {
|
|
@Override
|
|
public ParsePin then(Task<List<ParsePin>> task) throws Exception {
|
|
ParsePin pin = null;
|
|
if (task.getResult() != null && task.getResult().size() > 0) {
|
|
pin = task.getResult().get(0);
|
|
}
|
|
|
|
//TODO (grantland): What do we do if there are more than 1 result?
|
|
|
|
if (pin == null) {
|
|
pin = ParseObject.create(ParsePin.class);
|
|
pin.setName(name);
|
|
}
|
|
return pin;
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ <T extends ParseObject> Task<Void> pinAllObjectsAsync(
|
|
final String name,
|
|
final List<T> objects,
|
|
final boolean includeChildren) {
|
|
return runWithManagedTransaction(new SQLiteDatabaseCallable<Task<Void>>() {
|
|
@Override
|
|
public Task<Void> call(ParseSQLiteDatabase db) {
|
|
return pinAllObjectsAsync(name, objects, includeChildren, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private <T extends ParseObject> Task<Void> pinAllObjectsAsync(
|
|
final String name,
|
|
final List<T> objects,
|
|
final boolean includeChildren,
|
|
final ParseSQLiteDatabase db) {
|
|
if (objects == null || objects.size() == 0) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
return getParsePin(name, db).onSuccessTask(new Continuation<ParsePin, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParsePin> task) throws Exception {
|
|
ParsePin pin = task.getResult();
|
|
|
|
//TODO (grantland): change to use relations. currently the related PO are only getting saved
|
|
// offline as pointers.
|
|
// ParseRelation<ParseObject> relation = pin.getRelation(KEY_OBJECTS);
|
|
// relation.add(object);
|
|
|
|
// Hack to store collections in a pin
|
|
List<ParseObject> modified = pin.getObjects();
|
|
if (modified == null) {
|
|
modified = new ArrayList<ParseObject>(objects);
|
|
} else {
|
|
for (ParseObject object : objects) {
|
|
if (!modified.contains(object)) {
|
|
modified.add(object);
|
|
}
|
|
}
|
|
}
|
|
pin.setObjects(modified);
|
|
|
|
if (includeChildren) {
|
|
return saveLocallyAsync(pin, true, db);
|
|
}
|
|
return saveLocallyAsync(pin, pin.getObjects(), db);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ <T extends ParseObject> Task<Void> unpinAllObjectsAsync(
|
|
final String name,
|
|
final List<T> objects) {
|
|
return runWithManagedTransaction(new SQLiteDatabaseCallable<Task<Void>>() {
|
|
@Override
|
|
public Task<Void> call(ParseSQLiteDatabase db) {
|
|
return unpinAllObjectsAsync(name, objects, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private <T extends ParseObject> Task<Void> unpinAllObjectsAsync(
|
|
String name,
|
|
final List<T> objects,
|
|
final ParseSQLiteDatabase db) {
|
|
if (objects == null || objects.size() == 0) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
return getParsePin(name, db).onSuccessTask(new Continuation<ParsePin, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParsePin> task) throws Exception {
|
|
ParsePin pin = task.getResult();
|
|
|
|
//TODO (grantland): change to use relations. currently the related PO are only getting saved
|
|
// offline as pointers.
|
|
// ParseRelation<ParseObject> relation = pin.getRelation(KEY_OBJECTS);
|
|
// relation.remove(object);
|
|
|
|
// Hack to store collections in a pin
|
|
List<ParseObject> modified = pin.getObjects();
|
|
if (modified == null) {
|
|
// Unpin a pin that doesn't exist. Wat?
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
modified.removeAll(objects);
|
|
if (modified.size() == 0) {
|
|
return unpinAsync(pin, db);
|
|
}
|
|
pin.setObjects(modified);
|
|
|
|
return saveLocallyAsync(pin, true, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ Task<Void> unpinAllObjectsAsync(final String name) {
|
|
return runWithManagedTransaction(new SQLiteDatabaseCallable<Task<Void>>() {
|
|
@Override
|
|
public Task<Void> call(ParseSQLiteDatabase db) {
|
|
return unpinAllObjectsAsync(name, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private Task<Void> unpinAllObjectsAsync(final String name, final ParseSQLiteDatabase db) {
|
|
return getParsePin(name, db).continueWithTask(new Continuation<ParsePin, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParsePin> task) throws Exception {
|
|
if (task.isFaulted()) {
|
|
return task.makeVoid();
|
|
}
|
|
ParsePin pin = task.getResult();
|
|
return unpinAsync(pin, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ <T extends ParseObject> Task<List<T>> findFromPinAsync(
|
|
final String name,
|
|
final ParseQuery.State<T> state,
|
|
final ParseUser user) {
|
|
return runWithManagedConnection(new SQLiteDatabaseCallable<Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> call(ParseSQLiteDatabase db) {
|
|
return findFromPinAsync(name, state, user, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private <T extends ParseObject> Task<List<T>> findFromPinAsync(
|
|
final String name,
|
|
final ParseQuery.State<T> state,
|
|
final ParseUser user,
|
|
final ParseSQLiteDatabase db) {
|
|
Task<ParsePin> task;
|
|
if (name != null) {
|
|
task = getParsePin(name, db);
|
|
} else {
|
|
task = Task.forResult(null);
|
|
}
|
|
return task.onSuccessTask(new Continuation<ParsePin, Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> then(Task<ParsePin> task) throws Exception {
|
|
ParsePin pin = task.getResult();
|
|
return findAsync(state, user, pin, false, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ <T extends ParseObject> Task<Integer> countFromPinAsync(
|
|
final String name,
|
|
final ParseQuery.State<T> state,
|
|
final ParseUser user) {
|
|
return runWithManagedConnection(new SQLiteDatabaseCallable<Task<Integer>>() {
|
|
@Override
|
|
public Task<Integer> call(ParseSQLiteDatabase db) {
|
|
return countFromPinAsync(name, state, user, db);
|
|
}
|
|
});
|
|
}
|
|
|
|
private <T extends ParseObject> Task<Integer> countFromPinAsync(
|
|
final String name,
|
|
final ParseQuery.State<T> state,
|
|
final ParseUser user,
|
|
final ParseSQLiteDatabase db) {
|
|
Task<ParsePin> task;
|
|
if (name != null) {
|
|
task = getParsePin(name, db);
|
|
} else {
|
|
task = Task.forResult(null);
|
|
}
|
|
return task.onSuccessTask(new Continuation<ParsePin, Task<Integer>>() {
|
|
@Override
|
|
public Task<Integer> then(Task<ParsePin> task) throws Exception {
|
|
ParsePin pin = task.getResult();
|
|
return findAsync(state, user, pin, true, db).onSuccess(new Continuation<List<T>, Integer>() {
|
|
@Override
|
|
public Integer then(Task<List<T>> task) throws Exception {
|
|
return task.getResult().size();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region Single Instance
|
|
|
|
/**
|
|
* In-memory map of (className, objectId) -> ParseObject. This is used so that we can always
|
|
* return the same instance for a given object. Objects in this map may or may not be in the
|
|
* database.
|
|
*/
|
|
private final WeakValueHashMap<Pair<String, String>, ParseObject>
|
|
classNameAndObjectIdToObjectMap = new WeakValueHashMap<>();
|
|
|
|
/**
|
|
* This should be called by the ParseObject constructor notify the store that there is an object
|
|
* with this className and objectId.
|
|
*/
|
|
/* package */ void registerNewObject(ParseObject object) {
|
|
synchronized (lock) {
|
|
String objectId = object.getObjectId();
|
|
if (objectId != null) {
|
|
String className = object.getClassName();
|
|
Pair<String, String> classNameAndObjectId = Pair.create(className, objectId);
|
|
classNameAndObjectIdToObjectMap.put(classNameAndObjectId, object);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* package */ void unregisterObject(ParseObject object) {
|
|
synchronized (lock) {
|
|
String objectId = object.getObjectId();
|
|
if (objectId != null) {
|
|
classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), objectId));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This should only ever be called from ParseObject.createWithoutData().
|
|
*
|
|
* @return a pair of ParseObject and Boolean. The ParseObject is the object. The Boolean is true
|
|
* iff the object was newly created.
|
|
*/
|
|
/* package */ ParseObject getObject(String className, String objectId) {
|
|
if (objectId == null) {
|
|
throw new IllegalStateException("objectId cannot be null.");
|
|
}
|
|
|
|
Pair<String, String> classNameAndObjectId = Pair.create(className, objectId);
|
|
// This lock should never be held by anyone doing disk or database access.
|
|
synchronized (lock) {
|
|
return classNameAndObjectIdToObjectMap.get(classNameAndObjectId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When an object is finished saving, it gets an objectId. Then it should call this method to
|
|
* clean up the bookeeping around ids.
|
|
*/
|
|
/* package */ void updateObjectId(ParseObject object, String oldObjectId, String newObjectId) {
|
|
if (oldObjectId != null) {
|
|
if (oldObjectId.equals(newObjectId)) {
|
|
return;
|
|
}
|
|
/**
|
|
* Special case for re-saving installation if it was deleted on the server
|
|
* @see ParseInstallation#saveAsync(String, Task)
|
|
*/
|
|
if (object instanceof ParseInstallation
|
|
&& newObjectId == null) {
|
|
synchronized (lock) {
|
|
classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), oldObjectId));
|
|
}
|
|
return;
|
|
} else {
|
|
throw new RuntimeException("objectIds cannot be changed in offline mode.");
|
|
}
|
|
}
|
|
|
|
String className = object.getClassName();
|
|
Pair<String, String> classNameAndNewObjectId = Pair.create(className, newObjectId);
|
|
|
|
synchronized (lock) {
|
|
// See if there's already an entry for the new object id.
|
|
ParseObject existing = classNameAndObjectIdToObjectMap.get(classNameAndNewObjectId);
|
|
if (existing != null && existing != object) {
|
|
throw new RuntimeException("Attempted to change an objectId to one that's "
|
|
+ "already known to the Offline Store.");
|
|
}
|
|
|
|
// Okay, all clear to add the new reference.
|
|
classNameAndObjectIdToObjectMap.put(classNameAndNewObjectId, object);
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
|
|
/**
|
|
* Wraps SQLite operations with a managed SQLite connection.
|
|
*/
|
|
private <T> Task<T> runWithManagedConnection(final SQLiteDatabaseCallable<Task<T>> callable) {
|
|
return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation<ParseSQLiteDatabase, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<ParseSQLiteDatabase> task) throws Exception {
|
|
final ParseSQLiteDatabase db = task.getResult();
|
|
return callable.call(db).continueWithTask(new Continuation<T, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<T> task) throws Exception {
|
|
db.closeAsync();
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wraps SQLite operations with a managed SQLite connection and transaction.
|
|
*/
|
|
private Task<Void> runWithManagedTransaction(final SQLiteDatabaseCallable<Task<Void>> callable) {
|
|
return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation<ParseSQLiteDatabase, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParseSQLiteDatabase> task) throws Exception {
|
|
final ParseSQLiteDatabase db = task.getResult();
|
|
return db.beginTransactionAsync().onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return callable.call(db).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return db.setTransactionSuccessfulAsync();
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
db.endTransactionAsync();
|
|
db.closeAsync();
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private interface SQLiteDatabaseCallable<T> {
|
|
T call(ParseSQLiteDatabase db);
|
|
}
|
|
|
|
/*
|
|
* Methods for testing.
|
|
*/
|
|
|
|
/**
|
|
* Clears all in-memory caches so that data must be retrieved from disk.
|
|
*/
|
|
void simulateReboot() {
|
|
synchronized (lock) {
|
|
uuidToObjectMap.clear();
|
|
objectToUuidMap.clear();
|
|
classNameAndObjectIdToObjectMap.clear();
|
|
fetchedObjects.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the database on disk.
|
|
*/
|
|
void clearDatabase(Context context) {
|
|
helper.clearDatabase(context);
|
|
}
|
|
}
|