diff --git a/Parse/src/main/java/com/parse/ParseACL.java b/Parse/src/main/java/com/parse/ParseACL.java index c7381d563..e95cfedc2 100644 --- a/Parse/src/main/java/com/parse/ParseACL.java +++ b/Parse/src/main/java/com/parse/ParseACL.java @@ -8,12 +8,16 @@ */ package com.parse; +import android.os.Parcel; +import android.os.Parcelable; + import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * A {@code ParseACL} is used to control which users can access or modify a particular object. Each @@ -22,7 +26,7 @@ * permissions to "the public" so that, for example, any user could read a particular object but * only a particular set of users could write to that object. */ -public class ParseACL { +public class ParseACL implements Parcelable { private static final String PUBLIC_KEY = "*"; private final static String UNRESOLVED_KEY = "*unresolved"; private static final String KEY_ROLE_PREFIX = "role:"; @@ -61,6 +65,11 @@ private static class Permissions { return json; } + /* package */ void toParcel(Parcel parcel) { + parcel.writeByte(readPermission ? (byte) 1 : 0); + parcel.writeByte(writePermission ? (byte) 1 : 0); + } + /* package */ boolean getReadPermission() { return readPermission; } @@ -74,6 +83,10 @@ private static class Permissions { boolean write = object.optBoolean(WRITE_PERMISSION, false); return new Permissions(read, write); } + + /* package */ static Permissions createPermissionsFromParcel(Parcel parcel) { + return new Permissions(parcel.readByte() == 1, parcel.readByte() == 1); + } } private static ParseDefaultACLController getDefaultACLController() { @@ -205,7 +218,7 @@ public ParseACL(ParseUser owner) { } /* package for tests */ void resolveUser(ParseUser user) { - if (user != unresolvedUser) { + if (!isUnresolvedUser(user)) { return; } if (permissionsById.containsKey(UNRESOLVED_KEY)) { @@ -336,20 +349,27 @@ private void setUnresolvedWriteAccess(ParseUser user, boolean allowed) { private void prepareUnresolvedUser(ParseUser user) { // Registers a listener for the user so that when it is saved, the // unresolved ACL will be resolved. - if (this.unresolvedUser != user) { + if (!isUnresolvedUser(user)) { permissionsById.remove(UNRESOLVED_KEY); unresolvedUser = user; - user.registerSaveListener(new UserResolutionListener(this)); + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); } } + private boolean isUnresolvedUser(ParseUser other) { + // This might be a different instance, but if they have the same local id, assume it's correct. + if (other == null || unresolvedUser == null) return false; + return other == unresolvedUser || (other.getObjectId() == null && + other.getOrCreateLocalId().equals(unresolvedUser.getOrCreateLocalId())); + } + /** * Get whether the given user id is *explicitly* allowed to read this object. Even if this returns * {@code false}, the user may still be able to access it if getPublicReadAccess returns * {@code true} or a role that the user belongs to has read access. */ public boolean getReadAccess(ParseUser user) { - if (user == unresolvedUser) { + if (isUnresolvedUser(user)) { return getReadAccess(UNRESOLVED_KEY); } if (user.isLazy()) { @@ -381,7 +401,7 @@ public void setWriteAccess(ParseUser user, boolean allowed) { * {@code true} or a role that the user belongs to has write access. */ public boolean getWriteAccess(ParseUser user) { - if (user == unresolvedUser) { + if (isUnresolvedUser(user)) { return getWriteAccess(UNRESOLVED_KEY); } if (user.isLazy()) { @@ -536,4 +556,56 @@ public void done(ParseObject object, ParseException e) { /* package for tests */ Map getPermissionsById() { return permissionsById; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeByte(shared ? (byte) 1 : 0); + dest.writeInt(permissionsById.size()); + Set keys = permissionsById.keySet(); + for (String key : keys) { + dest.writeString(key); + Permissions permissions = permissionsById.get(key); + permissions.toParcel(dest); + } + dest.writeByte(unresolvedUser != null ? (byte) 1 : 0); + if (unresolvedUser != null) { + // Encoder will create a local id for unresolvedUser, so we recognize it after unparcel. + encoder.encode(unresolvedUser, dest); + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseACL createFromParcel(Parcel source) { + return new ParseACL(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseACL[] newArray(int size) { + return new ParseACL[size]; + } + }; + + /* package */ ParseACL(Parcel source, ParseParcelDecoder decoder) { + shared = source.readByte() == 1; + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + Permissions permissions = Permissions.createPermissionsFromParcel(source); + permissionsById.put(key, permissions); + } + if (source.readByte() == 1) { + unresolvedUser = (ParseUser) decoder.decode(source); + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); + } + } } diff --git a/Parse/src/main/java/com/parse/ParseAddOperation.java b/Parse/src/main/java/com/parse/ParseAddOperation.java index 8c7aab6d3..fe1959aa2 100644 --- a/Parse/src/main/java/com/parse/ParseAddOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -20,6 +22,8 @@ * An operation that adds a new element to an array field. */ /** package */ class ParseAddOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Add"; + protected final ArrayList objects = new ArrayList<>(); public ParseAddOperation(Collection coll) { @@ -29,12 +33,21 @@ public ParseAddOperation(Collection coll) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Add"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(objects)); return output; } @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java index 38bd879f7..8f19827d9 100644 --- a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -22,6 +24,8 @@ * An operation that adds a new element to an array field, only if it wasn't already present. */ /** package */ class ParseAddUniqueOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "AddUnique"; + protected final LinkedHashSet objects = new LinkedHashSet<>(); public ParseAddUniqueOperation(Collection col) { @@ -31,12 +35,21 @@ public ParseAddUniqueOperation(Collection col) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "AddUnique"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); return output; } @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override @SuppressWarnings("unchecked") public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { diff --git a/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/Parse/src/main/java/com/parse/ParseDeleteOperation.java index b6c9d335d..fb5393ac0 100644 --- a/Parse/src/main/java/com/parse/ParseDeleteOperation.java +++ b/Parse/src/main/java/com/parse/ParseDeleteOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -15,6 +17,8 @@ * An operation where a field is deleted from the object. */ /** package */ class ParseDeleteOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Delete"; + private static final ParseDeleteOperation defaultInstance = new ParseDeleteOperation(); public static ParseDeleteOperation getInstance() { @@ -27,10 +31,15 @@ private ParseDeleteOperation() { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Delete"); + output.put("__op", OP_NAME); return output; } + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + } + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { return this; diff --git a/Parse/src/main/java/com/parse/ParseFieldOperation.java b/Parse/src/main/java/com/parse/ParseFieldOperation.java index e83e0103b..bd33d7139 100644 --- a/Parse/src/main/java/com/parse/ParseFieldOperation.java +++ b/Parse/src/main/java/com/parse/ParseFieldOperation.java @@ -8,12 +8,15 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; @@ -35,6 +38,16 @@ */ Object encode(ParseEncoder objectEncoder) throws JSONException; + /** + * Writes the ParseFieldOperation to the given Parcel using the given encoder. + * + * @param dest + * The destination Parcel. + * @param parcelableEncoder + * A ParseParcelableEncoder. + */ + void encode(Parcel dest, ParseParcelEncoder parcelableEncoder); + /** * Returns a field operation that is composed of a previous operation followed by this operation. * This will not mutate either operation. However, it may return self if the current operation is @@ -73,10 +86,11 @@ private ParseFieldOperations() { } /** - * A function that creates a ParseFieldOperation from a JSONObject. + * A function that creates a ParseFieldOperation from a JSONObject or a Parcel. */ private interface ParseFieldOperationFactory { ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException; + ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder); } // A map of all known decoders. @@ -90,11 +104,11 @@ private static void registerDecoder(String opName, ParseFieldOperationFactory fa } /** - * Registers a list of default decoder functions that convert a JSONObject with an __op field into - * a ParseFieldOperation. + * Registers a list of default decoder functions that convert a JSONObject with an __op field, + * or a Parcel with a op name string, into a ParseFieldOperation. */ static void registerDefaultDecoders() { - registerDecoder("Batch", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_BATCH, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -106,49 +120,97 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } return op; } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + // Decode AddRelation and then RemoveRelation + ParseFieldOperation add = ParseFieldOperations.decode(source, decoder); + ParseFieldOperation remove = ParseFieldOperations.decode(source, decoder); + return remove.mergeWithPrevious(add); + } }); - registerDecoder("Delete", new ParseFieldOperationFactory() { + registerDecoder(ParseDeleteOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return ParseDeleteOperation.getInstance(); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return ParseDeleteOperation.getInstance(); + } }); - registerDecoder("Increment", new ParseFieldOperationFactory() { + registerDecoder(ParseIncrementOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseIncrementOperation((Number) decoder.decode(object.opt("amount"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return new ParseIncrementOperation((Number) decoder.decode(source)); + } }); - registerDecoder("Add", new ParseFieldOperationFactory() { + registerDecoder(ParseAddOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseAddOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseAddOperation(list); + } }); - registerDecoder("AddUnique", new ParseFieldOperationFactory() { + registerDecoder(ParseAddUniqueOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseAddUniqueOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseAddUniqueOperation(list); + } }); - registerDecoder("Remove", new ParseFieldOperationFactory() { + registerDecoder(ParseRemoveOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseRemoveOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseRemoveOperation(list); + } }); - registerDecoder("AddRelation", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_ADD, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -156,9 +218,19 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) List objectsList = (List) decoder.decode(objectsArray); return new ParseRelationOperation<>(new HashSet<>(objectsList), null); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(set, null); + } }); - registerDecoder("RemoveRelation", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_REMOVE, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -166,15 +238,37 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) List objectsList = (List) decoder.decode(objectsArray); return new ParseRelationOperation<>(null, new HashSet<>(objectsList)); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(null, set); + } + }); + + registerDecoder(ParseSetOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { + return null; // Not called. + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return new ParseSetOperation(decoder.decode(source)); + } }); } /** - * Converts a parsed JSON object into a PFFieldOperation. + * Converts a parsed JSON object into a ParseFieldOperation. * * @param encoded * A JSONObject containing an __op field. - * @return A PFFieldOperation. + * @return A ParseFieldOperation. */ static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) throws JSONException { String op = encoded.optString("__op"); @@ -185,6 +279,25 @@ static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) thro return factory.decode(encoded, decoder); } + /** + * Reads a ParseFieldOperation out of the given Parcel. + * + * @param source + * The source Parcel. + * @param decoder + * The given ParseParcelableDecoder. + * + * @return A ParseFieldOperation. + */ + static ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + String op = source.readString(); + ParseFieldOperationFactory factory = opDecoderMap.get(op); + if (factory == null) { + throw new RuntimeException("Unable to decode operation of type " + op); + } + return factory.decode(source, decoder); + } + /** * Converts a JSONArray into an ArrayList. */ diff --git a/Parse/src/main/java/com/parse/ParseIncrementOperation.java b/Parse/src/main/java/com/parse/ParseIncrementOperation.java index 7c39b231d..b2c56c2fa 100644 --- a/Parse/src/main/java/com/parse/ParseIncrementOperation.java +++ b/Parse/src/main/java/com/parse/ParseIncrementOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -15,6 +17,8 @@ * An operation that increases a numeric field's value by a given amount. */ /** package */ class ParseIncrementOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Increment"; + private final Number amount; public ParseIncrementOperation(Number amount) { @@ -24,12 +28,18 @@ public ParseIncrementOperation(Number amount) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Increment"); + output.put("__op", OP_NAME); output.put("amount", amount); return output; } @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 8f68fbbf3..18cf94df3 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -8,6 +8,11 @@ */ package com.parse; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -46,9 +51,10 @@ * The basic workflow for accessing existing data is to use a {@link ParseQuery} to specify which * existing data to retrieve. */ -public class ParseObject { +public class ParseObject implements Parcelable { private static final String AUTO_CLASS_NAME = "_Automatic"; /* package */ static final String VERSION_NAME = "1.15.2-SNAPSHOT"; + private static final String TAG = "ParseObject"; /* REST JSON Keys @@ -94,6 +100,14 @@ public static Init newBuilder(String className) { return new Builder(className); } + /* package */ static State createFromParcel(Parcel source, ParseParcelDecoder decoder) { + String className = source.readString(); + if ("_User".equals(className)) { + return new ParseUser.State(source, className, decoder); + } + return new State(source, className, decoder); + } + /** package */ static abstract class Init { private final String className; @@ -262,6 +276,26 @@ public State build() { availableKeys = new HashSet<>(builder.availableKeys); } + /* package */ State(Parcel parcel, String clazz, ParseParcelDecoder decoder) { + className = clazz; // Already read + objectId = parcel.readByte() == 1 ? parcel.readString() : null; + createdAt = parcel.readLong(); + long updated = parcel.readLong(); + updatedAt = updated > 0 ? updated : createdAt; + int size = parcel.readInt(); + HashMap map = new HashMap<>(); + for (int i = 0; i < size; i++) { + String key = parcel.readString(); + Object obj = decoder.decode(parcel); + map.put(key, obj); + } + serverData = Collections.unmodifiableMap(map); + isComplete = parcel.readByte() == 1; + List available = new ArrayList<>(); + parcel.readStringList(available); + availableKeys = new HashSet<>(available); + } + @SuppressWarnings("unchecked") public > T newBuilder() { return (T) new Builder(this); @@ -304,6 +338,24 @@ public Set availableKeys() { return availableKeys; } + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeString(className); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) { + dest.writeString(objectId); + } + dest.writeLong(createdAt); + dest.writeLong(updatedAt); + dest.writeInt(serverData.size()); + Set keys = serverData.keySet(); + for (String key : keys) { + dest.writeString(key); + encoder.encode(serverData.get(key), dest); + } + dest.writeByte(isComplete ? (byte) 1 : 0); + dest.writeStringList(new ArrayList<>(availableKeys)); + } + @Override public String toString() { return String.format(Locale.US, "%s@%s[" + @@ -330,12 +382,14 @@ public String toString() { // Cached State private final Map estimatedData; - private String localId; + /* package */ String localId; private final ParseMulticastDelegate saveEvent = new ParseMulticastDelegate<>(); /* package */ boolean isDeleted; + /* package */ boolean isDeleting; // Since delete ops are queued, we don't need a counter. //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. /* package */ int isDeletingEventually; + private boolean ldsEnabledWhenParceling; private static final ThreadLocal isCreatingPointerForObjectId = new ThreadLocal() { @@ -2106,6 +2160,7 @@ private Task deleteAsync(final String sessionToken, Task toAwait) { return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { + isDeleting = true; if (state.objectId() == null) { return task.cast(); // no reason to call delete since it doesn't exist } @@ -2116,6 +2171,12 @@ public Task then(Task task) throws Exception { public Task then(Task task) throws Exception { return handleDeleteResultAsync(); } + }).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + isDeleting = false; + return null; + } }); } @@ -4171,6 +4232,132 @@ public Task unpinInBackground() { public void unpin() throws ParseException { ParseTaskUtils.wait(unpinInBackground()); } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder(this)); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + synchronized (mutex) { + // Developer warnings. + ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled(); + boolean saving = hasOutstandingOperations(); + boolean deleting = isDeleting || isDeletingEventually > 0; + if (saving) { + Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will act as if these tasks have failed. This means that " + + "the subsequent call to save() will update again the same keys, and this is dangerous " + + "for certain operations, like increment(). To avoid inconsistencies, wait for operations " + + "to end before parceling."); + } + if (deleting) { + Log.w(TAG, "About to parcel a ParseObject while a delete / deleteEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will assume it's not deleted, and might incorrectly " + + "return false for isDirty(). To avoid inconsistencies, wait for operations to end " + + "before parceling."); + } + // Write className and id first, regardless of state. + dest.writeString(getClassName()); + String objectId = getObjectId(); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) dest.writeString(objectId); + // Write state and other members + state.writeToParcel(dest, encoder); + dest.writeByte(localId != null ? (byte) 1 : 0); + if (localId != null) dest.writeString(localId); + dest.writeByte(isDeleted ? (byte) 1 : 0); + // Care about dirty changes and ongoing tasks. + ParseOperationSet set; + if (hasOutstandingOperations()) { + // There's more than one set. Squash the queue, creating copies + // to preserve the original queue when LDS is enabled. + set = new ParseOperationSet(); + for (ParseOperationSet operationSet : operationSetQueue) { + ParseOperationSet copy = new ParseOperationSet(operationSet); + copy.mergeFrom(set); + set = copy; + } + } else { + set = operationSetQueue.getLast(); + } + set.setIsSaveEventually(false); + set.toParcel(dest, encoder); + // Pass a Bundle to subclasses. + Bundle bundle = new Bundle(); + onSaveInstanceState(bundle); + dest.writeBundle(bundle); + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseObject createFromParcel(Parcel source) { + return ParseObject.createFromParcel(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseObject[] newArray(int size) { + return new ParseObject[size]; + } + }; + + /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) { + String className = source.readString(); + String objectId = source.readByte() == 1 ? source.readString() : null; + // Create empty object (might be the same instance if LDS is enabled) + // and pass to decoder before unparceling child objects in State + ParseObject object = createWithoutData(className, objectId); + if (decoder instanceof ParseObjectParcelDecoder) { + ((ParseObjectParcelDecoder) decoder).addKnownObject(object); + } + State state = State.createFromParcel(source, decoder); + object.setState(state); + if (source.readByte() == 1) object.localId = source.readString(); + if (source.readByte() == 1) object.isDeleted = true; + // If object.ldsEnabledWhenParceling is true, we got this from OfflineStore. + // There is no need to restore operations in that case. + boolean restoreOperations = !object.ldsEnabledWhenParceling; + ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); + if (restoreOperations) { + for (String key : set.keySet()) { + ParseFieldOperation op = set.get(key); + object.performOperation(key, op); // Update ops and estimatedData + } + } + Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); + object.onRestoreInstanceState(bundle); + return object; + } + + /** + * Called when parceling this ParseObject. + * Subclasses can put values into the provided {@link Bundle} and receive them later + * {@link #onRestoreInstanceState(Bundle)}. Note that internal fields are already parceled by + * the framework. + * + * @param outState Bundle to host extra values + */ + protected void onSaveInstanceState(Bundle outState) {} + + /** + * Called when unparceling this ParseObject. + * Subclasses can read values from the provided {@link Bundle} that were previously put + * during {@link #onSaveInstanceState(Bundle)}. At this point the internal state is already + * recovered. + * + * @param savedState Bundle to read the values from + */ + protected void onRestoreInstanceState(Bundle savedState) {} + } // [1] Normally we should only construct the command from state when it's our turn in the diff --git a/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java new file mode 100644 index 000000000..772c47c9d --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java @@ -0,0 +1,43 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelDecoder} that remembers which + * {@code ParseObject}s have been decoded. When a pointer is found and we have already decoded + * an instance for the same object id, we use the decoded instance. + * + * This is very similar to what {@link KnownParseObjectDecoder} does for JSON. + */ +/* package */ class ParseObjectParcelDecoder extends ParseParcelDecoder { + + private Map objects = new HashMap<>(); + + public ParseObjectParcelDecoder() {} + + public void addKnownObject(ParseObject object) { + objects.put(getObjectOrLocalId(object), object); + } + + @Override + protected ParseObject decodePointer(Parcel source) { + String className = source.readString(); + String objectId = source.readString(); + if (objects.containsKey(objectId)) { + return objects.get(objectId); + } + // Should not happen if encoding was done through ParseObjectParcelEncoder. + ParseObject object = ParseObject.createWithoutData(className, objectId); + objects.put(objectId, object); + return object; + } + + private String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java b/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java new file mode 100644 index 000000000..f30e6de86 --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java @@ -0,0 +1,37 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashSet; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelEncoder} that remembers which + * {@code ParseObject}s have been encoded. If an object is found again in the object tree, + * it is encoded as a pointer rather than a full object, to avoid {@code StackOverflowError}s + * due to circular references. + */ +/* package */ class ParseObjectParcelEncoder extends ParseParcelEncoder { + + private Set ids = new HashSet<>(); + + public ParseObjectParcelEncoder() {} + + public ParseObjectParcelEncoder(ParseObject root) { + ids.add(getObjectOrLocalId(root)); + } + + @Override + protected void encodeParseObject(ParseObject object, Parcel dest) { + String id = getObjectOrLocalId(object); + if (ids.contains(id)) { + encodePointer(object.getClassName(), id, dest); + } else { + super.encodeParseObject(object, dest); + } + } + + private String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/Parse/src/main/java/com/parse/ParseOperationSet.java b/Parse/src/main/java/com/parse/ParseOperationSet.java index 3189841ce..1cddc29f4 100644 --- a/Parse/src/main/java/com/parse/ParseOperationSet.java +++ b/Parse/src/main/java/com/parse/ParseOperationSet.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -139,4 +141,29 @@ public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) return operationSet; } + + /** + * Parcels this operation set into a Parcel with the given encoder. + */ + /* package */ void toParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeString(uuid); + dest.writeByte(isSaveEventually ? (byte) 1 : 0); + dest.writeInt(size()); + for (String key : keySet()) { + dest.writeString(key); + encoder.encode(get(key), dest); + } + } + + /* package */ static ParseOperationSet fromParcel(Parcel source, ParseParcelDecoder decoder) { + ParseOperationSet set = new ParseOperationSet(source.readString()); + set.setIsSaveEventually(source.readByte() == 1); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + ParseFieldOperation op = (ParseFieldOperation) decoder.decode(source); + set.put(key, op); + } + return set; + } } diff --git a/Parse/src/main/java/com/parse/ParseParcelDecoder.java b/Parse/src/main/java/com/parse/ParseParcelDecoder.java new file mode 100644 index 000000000..cacc2344a --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseParcelDecoder.java @@ -0,0 +1,108 @@ +/* + * 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 org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A {@code ParseParcelableDecoder} can be used to unparcel objects such as + * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. + * + * This is capable of decoding objects and pointers to them. + * However, for improved behavior in the case of {@link ParseObject}s, use the stateful + * implementation {@link ParseObjectParcelDecoder}. + * + * @see ParseParcelEncoder + * @see ParseObjectParcelDecoder + */ +/* package */ class ParseParcelDecoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelDecoder INSTANCE = new ParseParcelDecoder(); + public static ParseParcelDecoder get() { + return INSTANCE; + } + + public Object decode(Parcel source) { + String type = source.readString(); + switch (type) { + + case ParseParcelEncoder.TYPE_OBJECT: + return decodeParseObject(source); + + case ParseParcelEncoder.TYPE_POINTER: + return decodePointer(source); + + case ParseParcelEncoder.TYPE_DATE: + String iso = source.readString(); + return ParseDateFormat.getInstance().parse(iso); + + case ParseParcelEncoder.TYPE_BYTES: + byte[] bytes = new byte[source.readInt()]; + source.readByteArray(bytes); + return bytes; + + case ParseParcelEncoder.TYPE_OP: + return ParseFieldOperations.decode(source, this); + + case ParseParcelEncoder.TYPE_ACL: + return new ParseACL(source, this); + + case ParseParcelEncoder.TYPE_RELATION: + return new ParseRelation<>(source, this); + + case ParseParcelEncoder.TYPE_MAP: + int size = source.readInt(); + Map map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(source.readString(), decode(source)); + } + return map; + + case ParseParcelEncoder.TYPE_COLLECTION: + int length = source.readInt(); + List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + list.add(i, decode(source)); + } + return list; + + case ParseParcelEncoder.TYPE_JSON_NULL: + return JSONObject.NULL; + + case ParseParcelEncoder.TYPE_NULL: + return null; + + case ParseParcelEncoder.TYPE_NATIVE: + return source.readValue(null); // No need for a class loader. + + default: + throw new RuntimeException("Could not unparcel objects from this Parcel."); + + } + } + + protected ParseObject decodeParseObject(Parcel source) { + return ParseObject.createFromParcel(source, this); + } + + protected ParseObject decodePointer(Parcel source) { + // By default, use createWithoutData. Overriden in subclass. + return ParseObject.createWithoutData(source.readString(), source.readString()); + } + +} diff --git a/Parse/src/main/java/com/parse/ParseParcelEncoder.java b/Parse/src/main/java/com/parse/ParseParcelEncoder.java new file mode 100644 index 000000000..f9124b91a --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -0,0 +1,141 @@ +/* + * 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 org.json.JSONObject; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A {@code ParseParcelableEncoder} can be used to parcel objects into a {@link android.os.Parcel}. + * + * This is capable of parceling {@link ParseObject}s, but the result can likely be a + * {@link StackOverflowError} due to circular references in the objects tree. + * When needing to parcel {@link ParseObject}, use the stateful {@link ParseObjectParcelEncoder}. + * + * @see ParseParcelDecoder + * @see ParseObjectParcelEncoder + */ +/* package */ class ParseParcelEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelEncoder INSTANCE = new ParseParcelEncoder(); + public static ParseParcelEncoder get() { + return INSTANCE; + } + + private static boolean isValidType(Object value) { + // This encodes to parcel what ParseEncoder does for JSON + return ParseEncoder.isValidType(value); + } + + /* package */ final static String TYPE_OBJECT = "ParseObject"; + /* package */ final static String TYPE_POINTER = "Pointer"; + /* package */ final static String TYPE_DATE = "Date"; + /* package */ final static String TYPE_BYTES = "Bytes"; + /* package */ final static String TYPE_ACL = "Acl"; + /* package */ final static String TYPE_RELATION = "Relation"; + /* package */ final static String TYPE_MAP = "Map"; + /* package */ final static String TYPE_COLLECTION = "Collection"; + /* package */ final static String TYPE_JSON_NULL = "JsonNull"; + /* package */ final static String TYPE_NULL = "Null"; + /* package */ final static String TYPE_NATIVE = "Native"; + /* package */ final static String TYPE_OP = "Operation"; + + public void encode(Object object, Parcel dest) { + try { + if (object instanceof ParseObject) { + // By default, encode as a full ParseObject. Overriden in sublasses. + encodeParseObject((ParseObject) object, dest); + + } else if (object instanceof Date) { + dest.writeString(TYPE_DATE); + dest.writeString(ParseDateFormat.getInstance().format((Date) object)); + + } else if (object instanceof byte[]) { + dest.writeString(TYPE_BYTES); + byte[] bytes = (byte[]) object; + dest.writeInt(bytes.length); + dest.writeByteArray(bytes); + + } else if (object instanceof ParseFieldOperation) { + dest.writeString(TYPE_OP); + ((ParseFieldOperation) object).encode(dest, this); + + } else if (object instanceof ParseFile) { + throw new IllegalArgumentException("Not supported yet"); + + } else if (object instanceof ParseGeoPoint) { + throw new IllegalArgumentException("Not supported yet"); + + } else if (object instanceof ParseACL) { + dest.writeString(TYPE_ACL); + ((ParseACL) object).writeToParcel(dest, this); + + } else if (object instanceof ParseRelation) { + dest.writeString(TYPE_RELATION); + ((ParseRelation) object).writeToParcel(dest, this); + + } else if (object instanceof Map) { + dest.writeString(TYPE_MAP); + @SuppressWarnings("unchecked") + Map map = (Map) object; + dest.writeInt(map.size()); + for (Map.Entry pair : map.entrySet()) { + dest.writeString(pair.getKey()); + encode(pair.getValue(), dest); + } + + } else if (object instanceof Collection) { + dest.writeString(TYPE_COLLECTION); + Collection collection = (Collection) object; + dest.writeInt(collection.size()); + for (Object item : collection) { + encode(item, dest); + } + + } else if (object == JSONObject.NULL) { + dest.writeString(TYPE_JSON_NULL); + + } else if (object == null) { + dest.writeString(TYPE_NULL); + + // String, Number, Boolean. Simply use writeValue + } else if (isValidType(object)) { + dest.writeString(TYPE_NATIVE); + dest.writeValue(object); + + } else { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + + } catch (Exception e) { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + } + + protected void encodeParseObject(ParseObject object, Parcel dest) { + dest.writeString(TYPE_OBJECT); + object.writeToParcel(dest, this); + } + + protected void encodePointer(String className, String objectId, Parcel dest) { + dest.writeString(TYPE_POINTER); + dest.writeString(className); + dest.writeString(objectId); + } +} diff --git a/Parse/src/main/java/com/parse/ParseRelation.java b/Parse/src/main/java/com/parse/ParseRelation.java index c9678ed9b..4a6a1720b 100644 --- a/Parse/src/main/java/com/parse/ParseRelation.java +++ b/Parse/src/main/java/com/parse/ParseRelation.java @@ -8,6 +8,9 @@ */ package com.parse; +import android.os.Parcel; +import android.os.Parcelable; + import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; @@ -21,7 +24,7 @@ * A class that is used to access all of the children of a many-to-many relationship. Each instance * of Parse.Relation is associated with a particular parent object and key. */ -public class ParseRelation { +public class ParseRelation implements Parcelable { private final Object mutex = new Object(); // The owning object of this ParseRelation. @@ -59,7 +62,7 @@ public class ParseRelation { } /** - * Parses a relation from JSON. + * Parses a relation from JSON with the given decoder. */ /* package */ ParseRelation(JSONObject jsonObject, ParseDecoder decoder) { this.parent = null; @@ -75,6 +78,21 @@ public class ParseRelation { } } + /** + * Creates a ParseRelation from a Parcel with the given decoder. + */ + /* package */ ParseRelation(Parcel source, ParseParcelDecoder decoder) { + if (source.readByte() == 1) this.key = source.readString(); + if (source.readByte() == 1) this.targetClass = source.readString(); + if (source.readByte() == 1) this.parentClassName = source.readString(); + if (source.readByte() == 1) this.parentObjectId = source.readString(); + if (source.readByte() == 1) this.parent = new WeakReference<>((ParseObject) decoder.decode(source)); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + knownObjects.add((ParseObject) decoder.decode(source)); + } + } + /* package */ void ensureParentAndKey(ParseObject someParent, String someKey) { synchronized (mutex) { if (parent == null) { @@ -224,4 +242,47 @@ public ParseQuery getQuery() { /* package for tests */ Set getKnownObjects() { return knownObjects; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + synchronized (mutex) { + // Fields are all nullable. + dest.writeByte(key != null ? (byte) 1 : 0); + if (key != null) dest.writeString(key); + dest.writeByte(targetClass != null ? (byte) 1 : 0); + if (targetClass != null) dest.writeString(targetClass); + dest.writeByte(parentClassName != null ? (byte) 1 : 0); + if (parentClassName != null) dest.writeString(parentClassName); + dest.writeByte(parentObjectId != null ? (byte) 1 : 0); + if (parentObjectId != null) dest.writeString(parentObjectId); + boolean has = parent != null && parent.get() != null; + dest.writeByte(has ? (byte) 1 : 0); + if (has) encoder.encode(parent.get(), dest); + dest.writeInt(knownObjects.size()); + for (ParseObject obj : knownObjects) { + encoder.encode(obj, dest); + } + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseRelation createFromParcel(Parcel source) { + return new ParseRelation(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseRelation[] newArray(int size) { + return new ParseRelation[size]; + } + }; } diff --git a/Parse/src/main/java/com/parse/ParseRelationOperation.java b/Parse/src/main/java/com/parse/ParseRelationOperation.java index 31cc0230a..a3075f30d 100644 --- a/Parse/src/main/java/com/parse/ParseRelationOperation.java +++ b/Parse/src/main/java/com/parse/ParseRelationOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -20,6 +22,10 @@ * An operation where a ParseRelation's value is modified. */ /** package */ class ParseRelationOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME_ADD = "AddRelation"; + /* package */ final static String OP_NAME_REMOVE = "RemoveRelation"; + /* package */ final static String OP_NAME_BATCH = "Batch"; + // The className of the target objects. private final String targetClass; @@ -155,19 +161,19 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { if (relationsToAdd.size() > 0) { adds = new JSONObject(); - adds.put("__op", "AddRelation"); + adds.put("__op", OP_NAME_ADD); adds.put("objects", convertSetToArray(relationsToAdd, objectEncoder)); } if (relationsToRemove.size() > 0) { removes = new JSONObject(); - removes.put("__op", "RemoveRelation"); + removes.put("__op", OP_NAME_REMOVE); removes.put("objects", convertSetToArray(relationsToRemove, objectEncoder)); } if (adds != null && removes != null) { JSONObject result = new JSONObject(); - result.put("__op", "Batch"); + result.put("__op", OP_NAME_BATCH); JSONArray ops = new JSONArray(); ops.put(adds); ops.put(removes); @@ -187,6 +193,30 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + if (relationsToAdd.isEmpty() && relationsToRemove.isEmpty()) { + throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); + } + if (relationsToAdd.size() > 0 && relationsToRemove.size() > 0) { + dest.writeString(OP_NAME_BATCH); + } + if (relationsToAdd.size() > 0) { + dest.writeString(OP_NAME_ADD); + dest.writeInt(relationsToAdd.size()); + for (ParseObject object : relationsToAdd) { + parcelableEncoder.encode(object, dest); + } + } + if (relationsToRemove.size() > 0) { + dest.writeString(OP_NAME_REMOVE); + dest.writeInt(relationsToRemove.size()); + for (ParseObject object : relationsToRemove) { + parcelableEncoder.encode(object, dest); + } + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseRemoveOperation.java b/Parse/src/main/java/com/parse/ParseRemoveOperation.java index 8f6d7b73e..0f24bd8c8 100644 --- a/Parse/src/main/java/com/parse/ParseRemoveOperation.java +++ b/Parse/src/main/java/com/parse/ParseRemoveOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -22,6 +24,8 @@ * An operation that removes every instance of an element from an array field. */ /** package */ class ParseRemoveOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Remove"; + protected final HashSet objects = new HashSet<>(); public ParseRemoveOperation(Collection coll) { @@ -31,12 +35,21 @@ public ParseRemoveOperation(Collection coll) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Remove"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); return output; } @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseSetOperation.java b/Parse/src/main/java/com/parse/ParseSetOperation.java index 00ecaa57c..b122af4ac 100644 --- a/Parse/src/main/java/com/parse/ParseSetOperation.java +++ b/Parse/src/main/java/com/parse/ParseSetOperation.java @@ -11,7 +11,12 @@ /** * An operation where a field is set to a given value regardless of its previous value. */ + +import android.os.Parcel; + /** package */ class ParseSetOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Set"; + private final Object value; public ParseSetOperation(Object newValue) { @@ -27,6 +32,12 @@ public Object encode(ParseEncoder objectEncoder) { return objectEncoder.encode(value); } + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + parcelableEncoder.encode(value, dest); + } + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { return this; diff --git a/Parse/src/main/java/com/parse/ParseUser.java b/Parse/src/main/java/com/parse/ParseUser.java index 65d11bab9..4324d6098 100644 --- a/Parse/src/main/java/com/parse/ParseUser.java +++ b/Parse/src/main/java/com/parse/ParseUser.java @@ -8,6 +8,9 @@ */ package com.parse; +import android.os.Bundle; +import android.os.Parcel; + import org.json.JSONObject; import java.util.ArrayList; @@ -38,6 +41,8 @@ public class ParseUser extends ParseObject { private static final List READ_ONLY_KEYS = Collections.unmodifiableList( Arrays.asList(KEY_SESSION_TOKEN, KEY_AUTH_DATA)); + private static final String PARCEL_KEY_IS_CURRENT_USER = "_isCurrentUser"; + /** * Constructs a query for {@code ParseUser}. * @@ -124,6 +129,11 @@ private State(Builder builder) { isNew = builder.isNew; } + /* package */ State(Parcel source, String className, ParseParcelDecoder decoder) { + super(source, className, decoder); + isNew = source.readByte() == 1; + } + @SuppressWarnings("unchecked") @Override public Builder newBuilder() { @@ -150,6 +160,12 @@ public Map> authData() { public boolean isNew() { return isNew; } + + @Override + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + super.writeToParcel(dest, encoder); + dest.writeByte(isNew ? (byte) 1 : 0); + } } // Whether the object is a currentUser. If so, it will always be persisted to disk on updates. @@ -1455,6 +1471,24 @@ public static void enableAutomaticUser() { //endregion + //region Parcelable + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + synchronized (mutex) { + outState.putBoolean(PARCEL_KEY_IS_CURRENT_USER, isCurrentUser); + } + } + + @Override + protected void onRestoreInstanceState(Bundle savedState) { + super.onRestoreInstanceState(savedState); + setIsCurrentUser(savedState.getBoolean(PARCEL_KEY_IS_CURRENT_USER, false)); + } + + //endregion + //region Legacy/Revocable Session Tokens /** diff --git a/Parse/src/test/java/com/parse/ParseACLTest.java b/Parse/src/test/java/com/parse/ParseACLTest.java index 5b66104dd..b34787a10 100644 --- a/Parse/src/test/java/com/parse/ParseACLTest.java +++ b/Parse/src/test/java/com/parse/ParseACLTest.java @@ -8,15 +8,16 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.HashMap; import java.util.Map; @@ -30,11 +31,14 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseACLTest { private final static String UNRESOLVED_KEY = "*unresolved"; @@ -162,6 +166,52 @@ public void testToJson() throws Exception { //endregion + //region parcelable + + @Test + public void testParcelable() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + ParseUser user = new ParseUser(); + user.setObjectId("userId2"); + acl.setReadAccess(user, true); + acl.setRoleWriteAccess("role", true); + acl.setShared(true); + + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + acl = ParseACL.CREATOR.createFromParcel(parcel); + + assertTrue(acl.getReadAccess("userId")); + assertTrue(acl.getReadAccess(user)); + assertTrue(acl.getRoleWriteAccess("role")); + assertTrue(acl.isShared()); + assertFalse(acl.getPublicReadAccess()); + assertFalse(acl.getPublicWriteAccess()); + } + + @Test + public void testParcelableWithUnresolvedUser() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); // Needed for unparceling ParseObjects + ParseACL acl = new ParseACL(); + ParseUser unresolved = new ParseUser(); + setLazy(unresolved); + acl.setReadAccess(unresolved, true); + + // unresolved users need a local id when parcelling and unparcelling. + // Since we don't have an Android environment, local id creation will fail. + unresolved.localId = "localId"; + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + // Do not user ParseObjectParcelDecoder because it requires local ids + acl = new ParseACL(parcel, new ParseParcelDecoder()); + assertTrue(acl.getReadAccess(unresolved)); + } + + //endregion + //region testCreateACLFromJSONObject @Test @@ -197,7 +247,11 @@ public void testResolveUserWithNewUser() throws Exception { ParseACL acl = new ParseACL(); acl.setReadAccess(unresolvedUser, true); - acl.resolveUser(new ParseUser()); + ParseUser other = new ParseUser(); + // local id creation fails if we don't have Android environment + unresolvedUser.localId = "someId"; + other.localId = "someOtherId"; + acl.resolveUser(other); // Make sure unresolvedUser is not changed assertSame(unresolvedUser, acl.getUnresolvedUser()); diff --git a/Parse/src/test/java/com/parse/ParseFileTest.java b/Parse/src/test/java/com/parse/ParseFileTest.java index 6afa4829b..8e44c79a3 100644 --- a/Parse/src/test/java/com/parse/ParseFileTest.java +++ b/Parse/src/test/java/com/parse/ParseFileTest.java @@ -43,6 +43,7 @@ public class ParseFileTest { @Before public void setup() { + ParseCorePlugins.getInstance().reset(); ParseTestUtils.setTestParseUser(); } diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index e4c906301..76b88b4c4 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -8,7 +8,12 @@ */ package com.parse; +import android.os.Parcel; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.Date; @@ -19,6 +24,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectStateTest { @Test @@ -79,6 +86,39 @@ public void testCopy() { assertTrue(copy.availableKeys().containsAll(state.availableKeys())); } + @Test + public void testParcelable() { + long updatedAt = System.currentTimeMillis(); + long createdAt = updatedAt + 10; + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .objectId("fake") + .createdAt(new Date(createdAt)) + .updatedAt(new Date(updatedAt)) + .isComplete(true) + .put("foo", "bar") + .put("baz", "qux") + .availableKeys(Arrays.asList("safe", "keys")) + .build(); + + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, ParseParcelEncoder.get()); + parcel.setDataPosition(0); + ParseObject.State copy = ParseObject.State.createFromParcel(parcel, ParseParcelDecoder.get()); + + assertEquals(state.className(), copy.className()); + assertEquals(state.objectId(), copy.objectId()); + assertEquals(state.createdAt(), copy.createdAt()); + assertEquals(state.updatedAt(), copy.updatedAt()); + assertEquals(state.isComplete(), copy.isComplete()); + assertEquals(state.keySet().size(), copy.keySet().size()); + assertEquals(state.get("foo"), copy.get("foo")); + assertEquals(state.get("baz"), copy.get("baz")); + assertEquals(state.availableKeys().size(), copy.availableKeys().size()); + assertTrue(state.availableKeys().containsAll(copy.availableKeys())); + assertTrue(copy.availableKeys().containsAll(state.availableKeys())); + } + @Test public void testAutomaticUpdatedAt() { long createdAt = System.currentTimeMillis(); diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 24aded591..1123adb26 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -8,13 +8,19 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Arrays; @@ -32,17 +38,28 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectTest { @Rule public ExpectedException thrown = ExpectedException.none(); + @Before + public void setUp() { + ParseFieldOperations.registerDefaultDecoders(); // to test JSON / Parcel decoding + } + @After public void tearDown() { ParseCorePlugins.getInstance().reset(); @@ -63,8 +80,6 @@ public void testFromJSONPayload() throws JSONException { "\"age\":33" + "}"); - ParseFieldOperations.registerDefaultDecoders(); - ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); assertEquals("GameScore", parseObject.getClassName()); assertEquals("TT1ZskATqS", parseObject.getObjectId()); @@ -93,20 +108,10 @@ public void testRevert() throws ParseException { List> tasks = new ArrayList<>(); // Mocked to let save work - ParseCurrentUserController userController = mock(ParseCurrentUserController.class); - when(userController.getAsync()).thenReturn(Task.forResult(null)); - ParseCorePlugins.getInstance().registerCurrentUserController(userController); + mockCurrentUserController(); // Mocked to simulate in-flight save - TaskCompletionSource tcs = new TaskCompletionSource(); - ParseObjectController objectController = mock(ParseObjectController.class); - when(objectController.saveAsync( - any(ParseObject.State.class), - any(ParseOperationSet.class), - anyString(), - any(ParseDecoder.class))) - .thenReturn(tcs.getTask()); - ParseCorePlugins.getInstance().registerObjectController(objectController); + TaskCompletionSource tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); @@ -165,20 +170,10 @@ public void testRevertKey() throws ParseException { List> tasks = new ArrayList<>(); // Mocked to let save work - ParseCurrentUserController userController = mock(ParseCurrentUserController.class); - when(userController.getAsync()).thenReturn(Task.forResult(null)); - ParseCorePlugins.getInstance().registerCurrentUserController(userController); + mockCurrentUserController(); // Mocked to simulate in-flight save - TaskCompletionSource tcs = new TaskCompletionSource(); - ParseObjectController objectController = mock(ParseObjectController.class); - when(objectController.saveAsync( - any(ParseObject.State.class), - any(ParseOperationSet.class), - anyString(), - any(ParseDecoder.class))) - .thenReturn(tcs.getTask()); - ParseCorePlugins.getInstance().registerObjectController(objectController); + TaskCompletionSource tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); @@ -497,4 +492,235 @@ public void testGetLongWithWrongValue() throws Exception { } //endregion + + //region testParcelable + + @Test + public void testParcelable() throws Exception { + // TODO test ParseGeoPoint and ParseFile after merge + ParseObject object = ParseObject.createWithoutData("Test", "objectId"); + object.isDeleted = true; + object.put("long", 200L); + object.put("double", 30D); + object.put("int", 50); + object.put("string", "test"); + object.put("date", new Date(200)); + object.put("null", JSONObject.NULL); + // Collection + object.put("collection", Arrays.asList("test1", "test2")); + // Pointer + ParseObject other = ParseObject.createWithoutData("Test", "otherId"); + object.put("pointer", other); + // Map + Map map = new HashMap<>(); + map.put("key1", "value"); + map.put("key2", 50); + object.put("map", map); + // Bytes + byte[] bytes = new byte[2]; + object.put("bytes", bytes); + // ACL + ParseACL acl = new ParseACL(); + acl.setReadAccess("reader", true); + object.setACL(acl); + // Relation + ParseObject related = ParseObject.createWithoutData("RelatedClass", "relatedId"); + ParseRelation rel = new ParseRelation<>(object, "relation"); + rel.add(related); + object.put("relation", rel); + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); + assertEquals(newObject.getClassName(), object.getClassName()); + assertEquals(newObject.isDeleted, object.isDeleted); + assertEquals(newObject.hasChanges(), object.hasChanges()); + assertEquals(newObject.getLong("long"), object.getLong("long")); + assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); + assertEquals(newObject.getInt("int"), object.getInt("int")); + assertEquals(newObject.getString("string"), object.getString("string")); + assertEquals(newObject.getDate("date"), object.getDate("date")); + assertEquals(newObject.get("null"), object.get("null")); + assertEquals(newObject.getList("collection"), object.getList("collection")); + assertEquals(newObject.getParseObject("pointer").getClassName(), other.getClassName()); + assertEquals(newObject.getParseObject("pointer").getObjectId(), other.getObjectId()); + assertEquals(newObject.getMap("map"), object.getMap("map")); + assertEquals(newObject.getBytes("bytes").length, bytes.length); + assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); + ParseRelation newRel = newObject.getRelation("relation"); + assertEquals(newRel.getKey(), rel.getKey()); + assertEquals(newRel.getKnownObjects().size(), rel.getKnownObjects().size()); + newRel.hasKnownObject(related); + } + + @Test + public void testRecursiveParcel() throws Exception { + ParseObject object = new ParseObject("Test"); + object.setObjectId("id"); + object.put("self", object); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, new ParseObjectParcelEncoder(object)); + parcel.setDataPosition(0); + ParseObject newObject = ParseObject.createFromParcel(parcel, new ParseObjectParcelDecoder()); + assertEquals(newObject.getObjectId(), "id"); + assertEquals(newObject.getParseObject("self").getObjectId(), "id"); + assertEquals(newObject.getParseObject("self").getParseObject("self").getObjectId(), "id"); + } + + @Test + public void testParcelWhileSaving() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); + + // Create multiple ParseOperationSets + List> tasks = new ArrayList<>(); + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + object.put("key", "value"); + object.put("number", 5); + tasks.add(object.saveInBackground()); + + object.put("key", "newValue"); + object.increment("number", 6); + tasks.add(object.saveInBackground()); + + object.increment("number", -1); + tasks.add(object.saveInBackground()); + + // Ensure Log.w is called... + assertTrue(object.hasOutstandingOperations()); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(other.isDirty("key")); + assertTrue(other.isDirty("number")); + assertEquals(other.getString("key"), "newValue"); + assertEquals(other.getNumber("number"), 10); + // By design, when LDS is off, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.hasOutstandingOperations()); + + // Force finish save operations on the old instance. + tcs.setResult(null); + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + + @Test + public void testParcelWhileSavingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + + object.put("key", "value"); + object.increment("number", 3); + Task saveTask = object.saveInBackground(); + assertTrue(object.hasOutstandingOperations()); // Saving + assertFalse(object.isDirty()); // Not dirty because it's saving + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.hasOutstandingOperations()); // Still saving + assertFalse(other.isDirty()); // Still not dirty + assertEquals(other.getNumber("number"), 3); + + tcs.setResult(null); + saveTask.waitForCompletion(); + Parse.setLocalDatastore(null); + } + + @Test + public void testParcelWhileDeleting() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + Task deleteTask = object.deleteInBackground(); + + // ensure Log.w is called.. + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + // By design, when LDS is off, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.isDeleting); + assertTrue(object.isDeleting); + + tcs.setResult(null); + deleteTask.waitForCompletion(); + assertFalse(object.isDeleting); + assertTrue(object.isDeleted); + } + + @Test + public void testParcelWhileDeletingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + Task deleteTask = object.deleteInBackground(); + + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.isDeleting); // Still deleting + + tcs.setResult(null); + deleteTask.waitForCompletion(); // complete deletion on original object. + assertFalse(other.isDeleting); + assertTrue(other.isDeleted); + Parse.setLocalDatastore(null); + } + + //endregion + + private static void mockCurrentUserController() { + ParseCurrentUserController userController = mock(ParseCurrentUserController.class); + when(userController.getCurrentSessionTokenAsync()).thenReturn(Task.forResult("token")); + when(userController.getAsync()).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(userController); + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForSave() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.saveAsync( + any(ParseObject.State.class), any(ParseOperationSet.class), + anyString(), any(ParseDecoder.class)) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForDelete() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.deleteAsync( + any(ParseObject.State.class), anyString()) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } } diff --git a/Parse/src/test/java/com/parse/ParseRelationTest.java b/Parse/src/test/java/com/parse/ParseRelationTest.java index 2cb4c134d..11fb7b05c 100644 --- a/Parse/src/test/java/com/parse/ParseRelationTest.java +++ b/Parse/src/test/java/com/parse/ParseRelationTest.java @@ -8,11 +8,16 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import static org.junit.Assert.assertEquals; @@ -24,6 +29,8 @@ import static org.mockito.Mockito.when; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseRelationTest { @Rule @@ -77,6 +84,36 @@ public void testConstructorWithJSONAndDecoder() throws Exception { //endregion + //region testParcelable + + @Test + public void testParcelable() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); + ParseRelation relation = new ParseRelation<>("Test"); + ParseObject parent = new ParseObject("Parent"); + parent.setObjectId("parentId"); + relation.ensureParentAndKey(parent, "key"); + ParseObject inner = new ParseObject("Test"); + inner.setObjectId("innerId"); + relation.add(inner); + + Parcel parcel = Parcel.obtain(); + relation.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + //noinspection unchecked + ParseRelation newRelation = ParseRelation.CREATOR.createFromParcel(parcel); + assertEquals(newRelation.getTargetClass(), "Test"); + assertEquals(newRelation.getKey(), "key"); + assertEquals(newRelation.getParent().getClassName(), "Parent"); + assertEquals(newRelation.getParent().getObjectId(), "parentId"); + assertEquals(newRelation.getKnownObjects().size(), 1); + + // This would fail assertTrue(newRelation.hasKnownObject(inner)). + // That is because ParseRelation uses == to check for known objects. + } + + //endregion + //region testEnsureParentAndKey @Test diff --git a/Parse/src/test/java/com/parse/ParseUserTest.java b/Parse/src/test/java/com/parse/ParseUserTest.java index f8eae0f22..8a275b408 100644 --- a/Parse/src/test/java/com/parse/ParseUserTest.java +++ b/Parse/src/test/java/com/parse/ParseUserTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -95,6 +97,39 @@ public void testImmutableKeys() { } } + // region Parcelable + + @Test + public void testOnSaveRestoreState() throws Exception { + ParseUser user = new ParseUser(); + user.setObjectId("objId"); + user.setIsCurrentUser(true); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isCurrentUser()); + } + + @Test + public void testParcelableState() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .objectId("test") + .isNew(true) + .build(); + ParseUser user = ParseObject.from(state); + assertTrue(user.isNew()); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isNew()); + } + + // endregion + //region SignUpAsync @Test