/*
* 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.ticl.android;
import com.google.common.base.Preconditions;
import com.google.ipc.invalidation.external.client.InvalidationClient;
import com.google.ipc.invalidation.external.client.InvalidationListener;
import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.android.AndroidInvalidationClient;
import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
import com.google.ipc.invalidation.external.client.android.service.Event;
import com.google.ipc.invalidation.external.client.android.service.ListenerBinder;
import com.google.ipc.invalidation.external.client.android.service.ListenerService;
import com.google.ipc.invalidation.external.client.android.service.ServiceBinder.BoundWork;
import com.google.ipc.invalidation.external.client.types.AckHandle;
import com.google.ipc.invalidation.external.client.types.ErrorInfo;
import com.google.ipc.invalidation.external.client.types.Invalidation;
import com.google.ipc.invalidation.external.client.types.ObjectId;
import com.google.ipc.invalidation.ticl.InvalidationClientCore;
import com.google.ipc.invalidation.ticl.InvalidationClientImpl;
import com.google.protos.ipc.invalidation.AndroidState.ClientMetadata;
import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP;
import android.accounts.Account;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.http.AndroidHttpClient;
import java.util.Collection;
import java.util.Random;
/**
* A bidirectional client proxy that wraps and delegates requests to a TICL instance and routes
* events generated by the TICL back to the associated listener.
*
*/
class AndroidClientProxy implements AndroidInvalidationClient {
private static final Logger logger = AndroidLogger.forTag("InvClientProxy");
/**
* A reverse proxy for delegating raised invalidation events back to the client (via the
* associated service).
*/
class AndroidListenerProxy implements InvalidationListener {
/** Binder that can be use to bind back to event listener service */
final ListenerBinder binder;
/**
* Creates a new listener reverse proxy.
*/
private AndroidListenerProxy() {
this.binder = new ListenerBinder(service, Event.LISTENER_INTENT, metadata.getListenerClass());
}
@Override
public void ready(InvalidationClient client) {
Event event = Event.newBuilder(Event.Action.READY).setClientKey(clientKey).build();
sendEvent(event);
}
@Override
public void informRegistrationStatus(
InvalidationClient client, ObjectId objectId, RegistrationState regState) {
Event event = Event.newBuilder(Event.Action.INFORM_REGISTRATION_STATUS)
.setClientKey(clientKey).setObjectId(objectId).setRegistrationState(regState).build();
sendEvent(event);
}
@Override
public void informRegistrationFailure(
InvalidationClient client, ObjectId objectId, boolean isTransient, String errorMessage) {
Event event = Event.newBuilder(Event.Action.INFORM_REGISTRATION_FAILURE)
.setClientKey(clientKey).setObjectId(objectId).setIsTransient(isTransient)
.setError(errorMessage).build();
sendEvent(event);
}
@Override
public void invalidate(
InvalidationClient client, Invalidation invalidation, AckHandle ackHandle) {
Event event = Event.newBuilder(Event.Action.INVALIDATE)
.setClientKey(clientKey).setInvalidation(invalidation).setAckHandle(ackHandle).build();
sendEvent(event);
}
@Override
public void invalidateAll(InvalidationClient client, AckHandle ackHandle) {
Event event = Event.newBuilder(Event.Action.INVALIDATE_ALL)
.setClientKey(clientKey).setAckHandle(ackHandle).build();
sendEvent(event);
}
@Override
public void invalidateUnknownVersion(
InvalidationClient client, ObjectId objectId, AckHandle ackHandle) {
Event event = Event.newBuilder(Event.Action.INVALIDATE_UNKNOWN)
.setClientKey(clientKey).setObjectId(objectId).setAckHandle(ackHandle).build();
sendEvent(event);
}
@Override
public void reissueRegistrations(InvalidationClient client, byte[] prefix, int prefixLength) {
Event event = Event.newBuilder(Event.Action.REISSUE_REGISTRATIONS)
.setClientKey(clientKey).setPrefix(prefix, prefixLength).build();
sendEvent(event);
}
@Override
public void informError(InvalidationClient client, ErrorInfo errorInfo) {
Event event = Event.newBuilder(Event.Action.INFORM_ERROR)
.setClientKey(clientKey).setErrorInfo(errorInfo).build();
sendEvent(event);
}
/**
* Releases any resources associated with the proxy listener.
*/
public void release() {
binder.release();
}
/**
* Send event messages to application clients and provides common processing of the response.
*/
private void sendEvent(final Event event) {
binder.runWhenBound(new BoundWork<ListenerService>() {
@Override
public void run(ListenerService listenerService) {
logger.fine("Sending %s event to %s", event.getAction(), clientKey);
service.sendEvent(listenerService, event);
}
});
}
}
/** The service associated with this proxy */
private final AndroidInvalidationService service;
/** the client key for this client proxy */
private final String clientKey;
/** The invalidation client to delegate requests to */
final InvalidationClient delegate;
/** The reverse listener proxy for this client proxy */
private final AndroidListenerProxy listener;
/** The stored state associated with this client */
private final ClientMetadata metadata;
/** The channel for this client */
private final AndroidChannel channel;
/** The system resources for this client */
private final SystemResources resources;
/** The HTTP client used by the underlying channel */
private final AndroidHttpClient httpClient;
/** {@code true} if client is started */
private boolean started;
/**
* Creates a new client proxy instance.
*
* @param service the service within which the client proxy is executing.
* @param storage the storage instance that contains client metadata and can be used to read or
* write client properties.
*/
AndroidClientProxy(AndroidInvalidationService service, AndroidStorage storage,
ClientConfigP config) {
this.service = service;
this.metadata = storage.getClientMetadata();
this.clientKey = metadata.getClientKey();
this.listener = new AndroidListenerProxy();
this.httpClient = AndroidChannel.getDefaultHttpClient(service);
this.channel = new AndroidChannel(this, httpClient, service);
this.resources =
AndroidResourcesFactory.createResourcesBuilder(clientKey, channel, storage).build();
String applicationName = getApplicationNameWithVersion(service,
storage.getClientMetadata().getListenerPkg());
this.delegate = createClient(resources, metadata.getClientType(), clientKey.getBytes(),
applicationName, listener, config);
}
/**
* Returns the application name string to pass to the Ticl, computed as a combination of the
* listener package and the application version.
*/
static String getApplicationNameWithVersion(Context context, String listenerPackage) {
String appVersion = "unknown";
try {
PackageInfo packageInfo =
context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String retrievedVersion = packageInfo.versionName;
if (retrievedVersion != null) {
appVersion = retrievedVersion;
}
} catch (NameNotFoundException exception) {
// AndroidLogger does not use setSystemResources, so it's safe to use the logger here.
logger.warning("Cannot retrieve current application version: %s", exception);
}
return listenerPackage + "#" + appVersion;
}
public final Account getAccount() {
return new Account(metadata.getAccountName(), metadata.getAccountType());
}
public final String getAuthType() {
return metadata.getAuthType();
}
@Override
public final String getClientKey() {
return metadata.getClientKey();
}
/** Returns the android service that is asociated with this proxy. */
final AndroidInvalidationService getService() {
return service;
}
/** Returns the network channel for this proxy. */
final AndroidChannel getChannel() {
return channel;
}
/** Returns the underlying invalidation client instance or {@code null} */
final InvalidationClient getDelegate() {
return delegate;
}
/** Returns the invalidation listener for this proxy */
final AndroidListenerProxy getListener() {
return listener;
}
/** Returns the storage used by the proxy. */
final AndroidStorage getStorage() {
return (AndroidStorage) resources.getStorage();
}
boolean isStarted() {
return started;
}
@Override
public void start() {
if (started) {
logger.info("Not starting Ticl since already started");
return;
}
resources.start();
delegate.start();
started = true;
}
@Override
public void stop() {
// When a client is stopped, stop the TICL and its resources and remove it from the client
// manager. This means that any subsequent requests (like another start) will be executed
// against a clean TICL instance w/ no preexisting state from before the stop.
if (!started) {
logger.info("Not stopping Ticl since already stopped");
return;
}
stopTicl();
resources.stop();
AndroidInvalidationService.getClientManager().remove(clientKey);
}
@Override
public void register(Collection<ObjectId> objectIds) {
delegate.register(objectIds);
}
@Override
public void register(ObjectId objectId) {
delegate.register(objectId);
}
@Override
public void unregister(Collection<ObjectId> objectIds) {
delegate.unregister(objectIds);
}
@Override
public void unregister(ObjectId objectId) {
delegate.unregister(objectId);
}
@Override
public void acknowledge(AckHandle ackHandle) {
delegate.acknowledge(ackHandle);
}
/**
* Called when the client proxy is being removed from memory and will no longer be in use.
* Releases any resources associated with the client proxy.
*/
@Override
public void release() {
// Release the listener associated with the proxy
listener.release();
// Stop system resources associated with the client
if (resources.isStarted()) {
resources.stop();
}
// Close the HTTP client
httpClient.close();
}
@Override
public void destroy() {
// Stop the client if started. This will also remove the client from the client manager
if (started) {
stop();
}
// Delete the storage associated with the client
AndroidStorage storage = (AndroidStorage) resources.getStorage();
storage.delete();
// Remove any cached instance for this client.
AndroidInvalidationService.getClientManager().remove(clientKey);
}
/**
* Creates a new InvalidationClient instance that the proxy will delegate requests to and listen
* for events from.
*/
// Overridden by tests to inject mock clients or for listener interception
InvalidationClient createClient(SystemResources resources, int clientType, byte[] clientName,
String applicationName, InvalidationListener listener, ClientConfigP config) {
// We always use C2DM, so set the channel-supports-offline-delivery bit on our config.
final ClientConfigP.Builder configBuilder;
if (config == null) {
configBuilder = InvalidationClientCore.createConfig();
} else {
configBuilder = ClientConfigP.newBuilder(config);
}
configBuilder.setChannelSupportsOfflineDelivery(true);
config = configBuilder.build();
Random random = new Random(resources.getInternalScheduler().getCurrentTimeMs());
return new InvalidationClientImpl(resources, random, clientType, clientName, config,
applicationName, listener);
}
/** Stops the underlying TICL instance but does not stop system resources. */
void stopTicl() {
Preconditions.checkState(started);
delegate.stop();
started = false;
}
}