/* * 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.service; import com.google.common.base.Preconditions; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import java.util.LinkedList; import java.util.Queue; /** * Abstract base class that assists in making connections to a bound service. Subclasses can define * a concrete binding to a particular bound service interface by binding to an explicit type on * declaration, providing a public constructor, and providing an implementation of the * {@link #asInterface} method. * <p> * This class has two main methods: {@link #runWhenBound} and {@link #release()}. * {@code runWhenBound} submits a {@code receiver} to be invoked once the service is bound, * initiating a bind if necessary. {@code release} releases the binding if one exists. * <p> * Interestingly, invocations of receivers passed to {@code runWhenBound} and calls to * {@code release} will be executed in program order. I.e., a call to runWhenBound followed by * a call to release will result in the receiver passed to runWhenBound being invoked before the * release, even if the binder had to wait for the service to be bound. * <p> * It is legal to call runWhenBound after a call to release. * * @param <BoundService> the bound service interface associated with the binder. * */ public abstract class ServiceBinder<BoundService> { /** * Interface for a work unit to be executed when the service is bound. * @param <ServiceType> the bound service interface type */ public interface BoundWork<ServiceType> { /** Function called with the bound service once the service is bound. */ void run(ServiceType service); } /** Logger */ private static final Logger logger = AndroidLogger.forTag("InvServiceBinder"); /** Intent that can be used to bind to the service */ private final Intent serviceIntent; /** Class that represents the bound service interface */ private final Class<BoundService> serviceClass; /** Name of the component that implements the service interface. */ private final String componentClassName; /** Work waiting to be run when the service becomes bound. */ private final Queue<BoundWork<BoundService>> pendingWork = new LinkedList<BoundWork<BoundService>>(); /** Used to synchronize. */ private final Object lock = new Object(); /** Bound service instance held by the binder or {@code null} if not bound. */ private BoundService serviceInstance = null; /** Context to use when binding and unbinding. */ private final Context context; /** Whether bindService has been called. */ private boolean hasCalledBind = false; /** Whether we are currently executing an event from the queue. */ private boolean queueHandlingInProgress = false; /** Number of times {@link #startBind()} has been called, for tests. */ private int numStartBindForTest = 0; /** * Service connection implementation that handles connection/disconnection * events for the binder. */ private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName serviceName, IBinder binder) { logger.fine("onServiceConnected: %s", serviceName); synchronized (lock) { // Once the service is bound, save it and run any work that was waiting for it. serviceInstance = asInterface(binder); handleQueue(); } } @Override public void onServiceDisconnected(ComponentName serviceName) { logger.fine("onServiceDisconnected: %s", serviceClass); synchronized (lock) { serviceInstance = null; } } }; /** * Constructs a new ServiceBinder that uses the provided intent to bind to the service of the * specific type. Subclasses should expose a public constructor that passes the appropriate intent * and type into this constructor. * * @param context context to use for (un)binding. * @param serviceIntent intent that can be used to connect to the bound service. * @param serviceClass interface exposed by the bound service. * @param componentClassName name of component implementing the bound service. If non-null, then * an explicit binding to the named component within the same class is guaranteed. */ protected ServiceBinder(Context context, Intent serviceIntent, Class<BoundService> serviceClass, String componentClassName) { this.context = Preconditions.checkNotNull(context); this.serviceIntent = Preconditions.checkNotNull(serviceIntent); this.serviceClass = Preconditions.checkNotNull(serviceClass); this.componentClassName = componentClassName; } /** Returns the intent used to bind to the service */ public Intent getIntent() { Intent bindIntent; if (componentClassName == null) { return serviceIntent; } bindIntent = new Intent(serviceIntent); bindIntent.setClassName(context, componentClassName); return bindIntent; } /** Runs {@code receiver} when the service becomes bound. */ public void runWhenBound(BoundWork<BoundService> receiver) { synchronized (lock) { pendingWork.add(receiver); handleQueue(); } } /** Unbinds the service associated with the binder. No-op if not bound. */ public void release() { synchronized (lock) { if (!hasCalledBind) { logger.fine("Release is a no-op since not bound: %s", serviceClass); return; } // We need to release using a runWhenBound to avoid having a release jump ahead of // pending work waiting for a bind (i.e., to preserve program order). runWhenBound(new BoundWork<BoundService>() { @Override public void run(BoundService ignored) { synchronized (lock) { // Do the unbind. logger.fine("Unbinding %s from %s", serviceClass, serviceInstance); try { context.unbindService(serviceConnection); } catch (IllegalArgumentException exception) { logger.fine("Exception unbinding from %s: %s", serviceClass, exception.getMessage()); } // Clear the now-stale reference and reset hasCalledBind so that we will initiate a // bind on a subsequent call to runWhenBound. serviceInstance = null; hasCalledBind = false; // This isn't necessarily wrong, but it's slightly odd. if (!pendingWork.isEmpty()) { logger.info("Still have %s work items in release of %s", pendingWork.size(), serviceClass); } } } }); } } /** * Returns {@code true} if the service binder is currently connected to the * bound service. */ public boolean isBoundForTest() { synchronized (lock) { return hasCalledBind; } } @Override public String toString() { synchronized (lock) { return this.getClass().getSimpleName() + "[" + serviceIntent + "]"; } } /** Returns the number of times {@code startBind} has been called, for tests. */ public int getNumStartBindForTest() { return numStartBindForTest; } /** * Recursively process the queue of {@link #pendingWork}. Initiates a bind to the service if * required. Else, if the service instance is available, removes the head of the queue and invokes * it with the service instance. * <p> * Note: this function differs from {@link #runWhenBound} only in that {@code runWhenBound} * enqueues into {@link #pendingWork}. */ private void handleQueue() { if (queueHandlingInProgress) { // Someone called back into runWhenBound from receiver.accept. We don't want to start another // recursive call, since we're already handling the queue. return; } if (pendingWork.isEmpty()) { // Recursive base case. return; } if (!hasCalledBind) { // Initiate a bind if not bound. Preconditions.checkState(serviceInstance == null, "Bind not called but service instance is set: %s", serviceClass); // May fail, but does its own logging. If it fails, we will never dispatch the work in the // queue, but it's unclear what we can do in this case other than log. startBind(); return; } if (serviceInstance == null) { // Wait for the service to become bound if it is not yet available and a bind is in progress. Preconditions.checkState(hasCalledBind, "No service instance and not waiting for bind: %s", serviceClass); return; } // Service is bound and available. Remove and invoke the head of the queue, then recurse to // process the rest. We recurse because the head of the queue may have been a release(), which // would have unbound the service, and we would need to reinvoke the binding code. BoundWork<BoundService> work = pendingWork.remove(); queueHandlingInProgress = true; work.run(serviceInstance); queueHandlingInProgress = false; handleQueue(); } /** * Binds to the service associated with the binder within the provided context. Returns whether * binding was successfully initiated. */ private boolean startBind() { Preconditions.checkState(!hasCalledBind, "Bind already called for %s", serviceClass); ++numStartBindForTest; Intent bindIntent = getIntent(); if (!context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) { logger.severe("Unable to bind to service: %s", bindIntent); return false; } hasCalledBind = true; return true; } /** Returns a bound service stub of the expected type. */ protected abstract BoundService asInterface(IBinder binder); }