blob: 04cbf3f69d5803d41fb4799b1012c707ded4581b [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.common.base.Preconditions;
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.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
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.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.
*/
ByteString 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.getObjectId());
isRegistered = objectStateProto.getIsRegistered();
if (objectStateProto.hasPayload()) {
payload = objectStateProto.getPayload();
}
if (objectStateProto.hasHighestVersion()) {
highestVersion = objectStateProto.getHighestVersion();
}
if (objectStateProto.hasInvalidationTimeMillis()) {
invalidationTimeMillis = objectStateProto.getInvalidationTimeMillis();
}
isBackground = objectStateProto.getIsBackground();
}
ObjectState(ObjectId objectId, boolean isRegistered) {
this.objectId = objectId;
this.isRegistered = isRegistered;
}
ObjectStateProto serialize() {
ObjectStateProto.Builder builder = ObjectStateProto.newBuilder()
.setObjectId(ExampleListenerState.serializeObjectId(objectId))
.setIsRegistered(isRegistered)
.setIsBackground(isBackground);
if (payload != null) {
builder.setPayload(payload);
}
if (highestVersion != null) {
builder.setHighestVersion(highestVersion.longValue());
}
if (invalidationTimeMillis != null) {
builder.setInvalidationTimeMillis(invalidationTimeMillis.longValue());
}
return builder.build();
}
@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.toStringUtf8());
}
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) {
this.trackedObjects = Preconditions.checkNotNull(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.getObjectStateList()) {
ObjectState objectState = new ObjectState(objectStateProto);
trackedObjects.put(objectState.objectId, objectState);
}
clientId = stateProto.hasClientId() ? stateProto.getClientId().toByteArray() : null;
}
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("Illegal base 64 encoding. data='%s', error='%s'", data,
exception.getMessage()));
return null;
}
try {
ExampleListenerStateProto proto = ExampleListenerStateProto.parseFrom(bytes);
return proto;
} catch (InvalidProtocolBufferException exception) {
Log.e(TAG, String.format("Error parsing state bytes. data='%s', error='%s'", data,
exception.getMessage()));
return null;
}
}
/** Serializes example listener state to string. */
public String serialize() {
ExampleListenerStateProto.Builder builder = ExampleListenerStateProto.newBuilder();
for (ObjectState objectState : trackedObjects.values()) {
ObjectStateProto objectStateProto = objectState.serialize();
builder.addObjectState(objectStateProto);
}
if (clientId != null) {
builder.setClientId(ByteString.copyFrom(clientId));
}
ExampleListenerStateProto proto = builder.build();
return Base64.encodeToString(proto.toByteArray(), 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 == null) ? null : ByteString.copyFrom(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.getSource(), objectIdProto.getName().toByteArray());
}
/** Serializes the given object id. */
static ObjectIdProto serializeObjectId(ObjectId objectId) {
return ObjectIdProto.newBuilder()
.setSource(objectId.getSource())
.setName(ByteString.copyFrom(objectId.getName()))
.build();
}
/** 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();
}
}