/* * 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; } }