/* * 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.external.client.android; import com.google.ipc.invalidation.external.client.SystemResources.Logger; 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.InvalidationBinder; import com.google.ipc.invalidation.external.client.android.service.InvalidationService; import com.google.ipc.invalidation.external.client.android.service.Request; import com.google.ipc.invalidation.external.client.android.service.Request.Action; import com.google.ipc.invalidation.external.client.android.service.Response; 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.ObjectId; import android.accounts.Account; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.RemoteException; import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Implementation of the {@code InvalidationClient} interface for Android. Instances of the class * are obtained using {@link AndroidClientFactory#create} or {@link AndroidClientFactory#resume}. * <p> * The class provides implementations of the {@code InvalidationClient} methods that delegate to the * invalidation service running on the device using the bound service model defined in * {@link InvalidationService}. * */ final class AndroidInvalidationClientImpl implements AndroidInvalidationClient { /** Logger */ private static final Logger logger = AndroidLogger.forTag("InvClient"); /** * The application context associated with the client. */ public final Context context; /** * Contains the device-unique client key associated with this client. */ private final String clientKey; /** * Contains the client type for this client. */ private final int clientType; /** * The Account associated with this client. May be {@code null} for resumed clients. */ private Account account; /** * The authentication type that is used to authenticate the client. */ private String authType; /** * A service binder used to bind to the invalidation service. */ private final InvalidationBinder serviceBinder; /** * The {@code InvalidationListener} service class that handles events for this client. May be * {@code null} for resumed clients. */ private final Class<? extends AndroidInvalidationListener> listenerClass; /** * The number of callers that are sharing a reference to this client instance. Used to decide when * the service binding can be safely released. */ private AtomicInteger refcnt = new AtomicInteger(0); /** Whether {@link #release} was ever called with a non-positive {@code refcnt}. */ AtomicBoolean wasOverReleasedForTest = new AtomicBoolean(false); /** * Creates a new invalidation client with the provided client key and account that sends * invalidation events to the specified component. * * @param context the execution context for the client. * @param clientKey a unique id that identifies the created client within the scope of the * application. * @param account the user account associated with the client. * @param listenerClass the {@link AndroidInvalidationListener} subclass that will handle * invalidation events. */ AndroidInvalidationClientImpl(Context context, String clientKey, int clientType, Account account, String authType, Class<? extends AndroidInvalidationListener> listenerClass) { this.context = context; this.clientKey = clientKey; this.clientType = clientType; this.account = account; this.authType = authType; this.listenerClass = listenerClass; this.serviceBinder = new InvalidationBinder(context); } /** * Constructs a resumed invalidation client with the provided client key and context. * * @param context the application context for the client. * @param clientKey a unique id that identifies the resumed client within the scope of the device. */ AndroidInvalidationClientImpl(Context context, String clientKey) { this.clientKey = clientKey; this.context = context; this.account = null; this.authType = null; this.listenerClass = null; this.clientType = -1; this.serviceBinder = new InvalidationBinder(context); } /** * Returns the {@link Context} within which the client was created or resumed. */ Context getContext() { return context; } @Override public String getClientKey() { return clientKey; } /** * Returns the event listener class associated with the client or {@code null} if unknown (when * resumed). */ Class<? extends AndroidInvalidationListener> getListenerClass() { return listenerClass; } @Override public void start() { Request request = Request.newBuilder(Action.START).setClientKey(clientKey).build(); executeServiceRequest(request); } @Override public void stop() { Request request = Request.newBuilder(Action.STOP).setClientKey(clientKey).build(); executeServiceRequest(request); } /** * Registers to receive invalidation notifications for an object. * * @param objectId object id. */ @Override public void register(ObjectId objectId) { Request request = Request.newBuilder(Action.REGISTER).setClientKey(clientKey).setObjectId(objectId).build(); executeServiceRequest(request); } /** * Registers to receive invalidation notifications for a collection of objects. * * @param objectIds object id collection. */ @Override public void register(Collection<ObjectId> objectIds) { Request request = Request.newBuilder(Action.REGISTER).setClientKey(clientKey).setObjectIds(objectIds).build(); executeServiceRequest(request); } /** * Unregisters to disable receipt of invalidations on an object. * * @param objectId object id. */ @Override public void unregister(ObjectId objectId) { Request request = Request.newBuilder(Action.UNREGISTER).setClientKey(clientKey).setObjectId(objectId).build(); executeServiceRequest(request); } /** * Unregisters to disable receipt of invalidations for a collection of objects. * * @param objectIds object id collection. */ @Override public void unregister(Collection<ObjectId> objectIds) { Request request = Request .newBuilder(Action.UNREGISTER) .setClientKey(clientKey) .setObjectIds(objectIds) .build(); executeServiceRequest(request); } @Override public void acknowledge(AckHandle ackHandle) { Request request = Request .newBuilder(Action.ACKNOWLEDGE) .setClientKey(clientKey) .setAckHandle(ackHandle) .build(); executeServiceRequest(request); } @Override public void release() { // Release the binding and remove from the client factory when the last reference is // released. final int refsRemaining = refcnt.decrementAndGet(); if (refsRemaining < 0) { wasOverReleasedForTest.set(true); logger.warning("Over-release of client %s", clientKey); } else if (refsRemaining == 0) { AndroidClientFactory.release(clientKey); serviceBinder.release(); } } @Override public void destroy() { Request request = Request .newBuilder(Action.DESTROY) .setClientKey(clientKey) .build(); executeServiceRequest(request); } /** * Called to initialize a newly created client instance with the invalidation service. */ void initialize() { // Create an intent that can be used to fire listener events back to the // provided listener service. Use setComponent and not setPackage/setClass so the // intent is guaranteed to be valid even if the service is not in the same application Intent eventIntent = new Intent(Event.LISTENER_INTENT); ComponentName component = new ComponentName(context.getPackageName(), listenerClass.getName()); eventIntent.setComponent(component); Request request = Request .newBuilder(Action.CREATE) .setClientKey(clientKey) .setClientType(clientType) .setAccount(account) .setAuthType(authType) .setIntent(eventIntent) .build(); executeServiceRequest(request); addReference(); } /** * Called to resume an existing client instance with the invalidation service. Iff * {@code sendTiclResumeRequest}, a request is sent to the invalidatation service to ensure * that the Ticl is loaded. */ void initResumed(boolean sendTiclResumeRequest) { if (sendTiclResumeRequest) { Request request = Request.newBuilder(Action.RESUME).setClientKey(clientKey).build(); executeServiceRequest(request); } addReference(); } /** * Called to indicate that a client instance is being returned as a reference. */ void addReference() { refcnt.incrementAndGet(); } /** * Returns the number of references to this client instance. */ int getReferenceCountForTest() { return refcnt.get(); } /** * Returns {@code true} if the client has a binding to the invalidation service. */ boolean hasServiceBindingForTest() { return serviceBinder.isBoundForTest(); } /** * Executes a request against the invalidation service and does common error processing against * the resulting response. If unable to connect to the service or an error status is received from * it, a warning will be logged and the request will be dropped. * * @param request the request to execute. */ private void executeServiceRequest(final Request request) { serviceBinder.runWhenBound(new BoundWork<InvalidationService>() { @Override public void run(InvalidationService service) { Bundle outBundle = new Bundle(); try { service.handleRequest(request.getBundle(), outBundle); } catch (RemoteException exception) { logger.warning("Remote exeption executing request %s: %s", request, exception.getMessage()); } Response response = new Response(outBundle); response.warnOnFailure(); } }); } }