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