blob: 12ce9be5aef21b76ee38d3edfa7b6138d07d774f [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.ipc.invalidation.examples.android2;
import com.google.ipc.invalidation.examples.android2.ExampleListenerProto.ExampleListenerStateProto;
import com.google.ipc.invalidation.examples.android2.ExampleListenerProto.ExampleListenerStateProto.ObjectIdProto;
import com.google.ipc.invalidation.examples.android2.ExampleListenerProto.ExampleListenerStateProto.ObjectStateProto;
import com.google.ipc.invalidation.external.client.types.ObjectId;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import com.google.protobuf.nano.MessageNano;
import android.util.Base64;
import android.util.Log;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Wrapper around persistent state for {@link ExampleListener}.
*
*/
public class ExampleListenerState {
/** Wrapper around persistent state for an object tracked by the {@link ExampleListener}. */
private static class ObjectState {
/** Object id for the object being tracked. */
final ObjectId objectId;
/** Indicates whether the example listener wants to be registered for this object. */
boolean isRegistered;
/**
* Payload of the invalidation with the highest version received so far. {@code null} before
* any invalidations have been received or after an unknown-version invalidation is received.
*/
byte[] payload;
/**
* Highest version invalidation received so far. {@code null} before any invalidations have
* been received or after an unknown-version invalidation is received.
*/
Long highestVersion;
/** Wall time in milliseconds at which most recent invalidation was received. */
Long invalidationTimeMillis;
/** Indicates whether the last invalidation received was a background invalidation. */
boolean isBackground;
ObjectState(ObjectStateProto objectStateProto) {
objectId = deserializeObjectId(objectStateProto.objectId);
isRegistered = objectStateProto.isRegistered;
payload = objectStateProto.payload;
highestVersion = objectStateProto.highestVersion;
invalidationTimeMillis = objectStateProto.invalidationTimeMillis;
isBackground = objectStateProto.isBackground;
}
ObjectState(ObjectId objectId, boolean isRegistered) {
this.objectId = objectId;
this.isRegistered = isRegistered;
}
ObjectStateProto serialize() {
ObjectStateProto proto = new ObjectStateProto();
proto.objectId = serializeObjectId(objectId);
proto.isRegistered = isRegistered;
proto.isBackground = isBackground;
proto.payload = payload;
proto.highestVersion = highestVersion;
proto.invalidationTimeMillis = invalidationTimeMillis;
return proto;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
toString(builder);
return builder.toString();
}
void toString(StringBuilder builder) {
builder.append(isRegistered ? "REG " : "UNREG ").append(objectId);
if (payload != null) {
builder.append(", |payload|=").append(payload.length);
}
if (highestVersion != null) {
builder.append(", highestVersion=").append(highestVersion.longValue());
}
if (isBackground) {
builder.append(", isBackground");
}
if (invalidationTimeMillis != null) {
builder.append(", invalidationTime=").append(new Date(invalidationTimeMillis.longValue()));
}
}
}
/** The tag used for logging in the listener state class. */
private static final String TAG = "TEA2:ELS";
/** Number of objects we're interested in tracking by default. */
static final int NUM_INTERESTING_OBJECTS = 4;
/** Object source for objects the client is initially tracking. */
private static final int DEMO_SOURCE = 4;
/** Prefix for object names the client is initially tracking. */
private static final String OBJECT_ID_PREFIX = "Obj";
/** State for all tracked objects. */
private final Map<ObjectId, ObjectState> trackedObjects;
/** Client id reported by {@code AndroidListener#ready} call. */
private byte[] clientId;
private ExampleListenerState(Map<ObjectId, ObjectState> trackedObjects,
byte[] clientId) {
if (trackedObjects == null) {
throw new NullPointerException();
}
this.trackedObjects = trackedObjects;
this.clientId = clientId;
}
public static ExampleListenerState deserialize(String data) {
HashMap<ObjectId, ObjectState> trackedObjects = new HashMap<ObjectId, ObjectState>();
byte[] clientId;
ExampleListenerStateProto stateProto = tryParseStateProto(data);
if (stateProto == null) {
// By default, we're interested in objects with ids Obj1, Obj2, ...
for (int i = 1; i <= NUM_INTERESTING_OBJECTS; ++i) {
ObjectId objectId = ObjectId.newInstance(DEMO_SOURCE, (OBJECT_ID_PREFIX + i).getBytes());
trackedObjects.put(objectId, new ObjectState(objectId, true));
}
clientId = null;
} else {
// Load interesting objects from state proto.
for (ObjectStateProto objectStateProto : stateProto.objectState) {
ObjectState objectState = new ObjectState(objectStateProto);
trackedObjects.put(objectState.objectId, objectState);
}
clientId = stateProto.clientId;
}
return new ExampleListenerState(trackedObjects, clientId);
}
/** Returns proto serialized in data or null if it cannot be decoded. */
private static ExampleListenerStateProto tryParseStateProto(String data) {
if (data == null) {
return null;
}
final byte[] bytes;
try {
bytes = Base64.decode(data, Base64.DEFAULT);
} catch (IllegalArgumentException exception) {
Log.e(TAG, String.format(Locale.ROOT, "Illegal base 64 encoding. data='%s', error='%s'", data,
exception.getMessage()));
return null;
}
try {
ExampleListenerStateProto proto =
MessageNano.mergeFrom(new ExampleListenerStateProto(), bytes);
return proto;
} catch (InvalidProtocolBufferNanoException exception) {
Log.e(TAG, String.format(Locale.ROOT, "Error parsing state bytes. data='%s', error='%s'",
data, exception.getMessage()));
return null;
}
}
/** Serializes example listener state to string. */
public String serialize() {
ExampleListenerStateProto proto = new ExampleListenerStateProto();
proto.objectState = new ObjectStateProto[trackedObjects.size()];
int index = 0;
for (ObjectState objectState : trackedObjects.values()) {
ObjectStateProto objectStateProto = objectState.serialize();
proto.objectState[index++] = objectStateProto;
}
proto.clientId = clientId;
return Base64.encodeToString(MessageNano.toByteArray(proto), Base64.DEFAULT);
}
Iterable<ObjectId> getInterestingObjects() {
List<ObjectId> interestingObjects = new ArrayList<ObjectId>(trackedObjects.size());
for (ObjectState objectState : trackedObjects.values()) {
if (objectState.isRegistered) {
interestingObjects.add(objectState.objectId);
}
}
return interestingObjects;
}
byte[] getClientId() {
return clientId;
}
/** Sets the client id passed to the example listener via the {@code ready()} call. */
void setClientId(byte[] value) {
clientId = value;
}
/**
* Returns {@code true} if the state indicates a registration should exist for the given object.
*/
boolean isInterestedInObject(ObjectId objectId) {
ObjectState objectState = trackedObjects.get(objectId);
return (objectState != null) && objectState.isRegistered;
}
/** Updates state for the given object to indicate it should be registered. */
boolean addObjectOfInterest(ObjectId objectId) {
ObjectState objectState = trackedObjects.get(objectId);
if (objectState == null) {
objectState = new ObjectState(objectId, true);
trackedObjects.put(objectId, objectState);
return true;
}
if (objectState.isRegistered) {
return false;
}
objectState.isRegistered = true;
return true;
}
/** Updates state for the given object to indicate it should not be registered. */
boolean removeObjectOfInterest(ObjectId objectId) {
ObjectState objectState = trackedObjects.get(objectId);
if (objectState == null) {
return false;
}
if (objectState.isRegistered) {
objectState.isRegistered = false;
return true;
}
return false;
}
/** Updates state for an object after an unknown-version invalidation is received. */
void informUnknownVersionInvalidation(ObjectId objectId) {
ObjectState objectState = getObjectStateForInvalidation(objectId);
objectState.invalidationTimeMillis = System.currentTimeMillis();
objectState.highestVersion = null;
objectState.payload = null;
}
/** Updates state for an object after an invalidation is received. */
void informInvalidation(ObjectId objectId, long version, byte[] payload,
boolean isBackground) {
ObjectState objectState = getObjectStateForInvalidation(objectId);
if (objectState.highestVersion == null || objectState.highestVersion.longValue() < version) {
objectState.highestVersion = version;
objectState.payload = payload;
objectState.invalidationTimeMillis = System.currentTimeMillis();
objectState.isBackground = isBackground;
}
}
/**
* Updates state when an invalidate all request is received (unknown version is marked for all
* objects).
*/
public void informInvalidateAll() {
for (ObjectState objectState : trackedObjects.values()) {
informUnknownVersionInvalidation(objectState.objectId);
}
}
/** Returns existing object state for an object or updates state. */
private ObjectState getObjectStateForInvalidation(ObjectId objectId) {
ObjectState objectState = trackedObjects.get(objectId);
if (objectState == null) {
// Invalidation for unregistered object.
objectState = new ObjectState(objectId, false);
trackedObjects.put(objectId, objectState);
}
return objectState;
}
/** Returns an object given its serialized form. */
static ObjectId deserializeObjectId(ObjectIdProto objectIdProto) {
return ObjectId.newInstance(objectIdProto.source, objectIdProto.name);
}
/** Serializes the given object id. */
static ObjectIdProto serializeObjectId(ObjectId objectId) {
ObjectIdProto proto = new ObjectIdProto();
proto.source = objectId.getSource();
proto.name = objectId.getName();
return proto;
}
/** Clears all state for the example listener. */
void clear() {
trackedObjects.clear();
clientId = null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (clientId != null) {
builder.append("ready!\n");
}
for (ObjectState objectState : trackedObjects.values()) {
objectState.toString(builder);
builder.append("\n");
}
return builder.toString();
}
}