2 * Copyright 2011 Google Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.google.ipc.invalidation.external.client.android.service;
19 import com.google.common.base.Preconditions;
20 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.ServiceConnection;
26 import android.os.IBinder;
28 import java.util.LinkedList;
29 import java.util.Queue;
33 * Abstract base class that assists in making connections to a bound service. Subclasses can define
34 * a concrete binding to a particular bound service interface by binding to an explicit type on
35 * declaration, providing a public constructor, and providing an implementation of the
36 * {@link #asInterface} method.
38 * This class has two main methods: {@link #runWhenBound} and {@link #release()}.
39 * {@code runWhenBound} submits a {@code receiver} to be invoked once the service is bound,
40 * initiating a bind if necessary. {@code release} releases the binding if one exists.
42 * Interestingly, invocations of receivers passed to {@code runWhenBound} and calls to
43 * {@code release} will be executed in program order. I.e., a call to runWhenBound followed by
44 * a call to release will result in the receiver passed to runWhenBound being invoked before the
45 * release, even if the binder had to wait for the service to be bound.
47 * It is legal to call runWhenBound after a call to release.
49 * @param <BoundService> the bound service interface associated with the binder.
52 public abstract class ServiceBinder<BoundService> {
54 * Interface for a work unit to be executed when the service is bound.
55 * @param <ServiceType> the bound service interface type
57 public interface BoundWork<ServiceType> {
58 /** Function called with the bound service once the service is bound. */
59 void run(ServiceType service);
62 private static final Logger logger = AndroidLogger.forTag("InvServiceBinder");
64 /** Intent that can be used to bind to the service */
65 private final Intent serviceIntent;
67 /** Class that represents the bound service interface */
68 private final Class<BoundService> serviceClass;
70 /** Name of the component that implements the service interface. */
71 private final String componentClassName;
73 /** Work waiting to be run when the service becomes bound. */
74 private final Queue<BoundWork<BoundService>> pendingWork =
75 new LinkedList<BoundWork<BoundService>>();
77 /** Used to synchronize. */
78 private final Object lock = new Object();
80 /** Bound service instance held by the binder or {@code null} if not bound. */
81 private BoundService serviceInstance = null;
83 /** Context to use when binding and unbinding. */
84 private final Context context;
86 /** Whether bindService has been called. */
87 private boolean hasCalledBind = false;
89 /** Whether we are currently executing an event from the queue. */
90 private boolean queueHandlingInProgress = false;
92 /** Number of times {@link #startBind()} has been called, for tests. */
93 private int numStartBindForTest = 0;
96 * Service connection implementation that handles connection/disconnection
97 * events for the binder.
99 private final ServiceConnection serviceConnection = new ServiceConnection() {
102 public void onServiceConnected(ComponentName serviceName, IBinder binder) {
103 logger.fine("onServiceConnected: %s", serviceName);
104 synchronized (lock) {
105 // Once the service is bound, save it and run any work that was waiting for it.
106 serviceInstance = asInterface(binder);
112 public void onServiceDisconnected(ComponentName serviceName) {
113 logger.fine("onServiceDisconnected: %s", serviceClass);
114 synchronized (lock) {
115 serviceInstance = null;
121 * Constructs a new ServiceBinder that uses the provided intent to bind to the service of the
122 * specific type. Subclasses should expose a public constructor that passes the appropriate intent
123 * and type into this constructor.
125 * @param context context to use for (un)binding.
126 * @param serviceIntent intent that can be used to connect to the bound service.
127 * @param serviceClass interface exposed by the bound service.
128 * @param componentClassName name of component implementing the bound service. If non-null, then
129 * an explicit binding to the named component within the same class is guaranteed.
131 protected ServiceBinder(Context context, Intent serviceIntent, Class<BoundService> serviceClass,
132 String componentClassName) {
133 this.context = Preconditions.checkNotNull(context);
134 this.serviceIntent = Preconditions.checkNotNull(serviceIntent);
135 this.serviceClass = Preconditions.checkNotNull(serviceClass);
136 this.componentClassName = componentClassName;
139 /** Returns the intent used to bind to the service */
140 public Intent getIntent() {
142 if (componentClassName == null) {
143 return serviceIntent;
145 bindIntent = new Intent(serviceIntent);
146 bindIntent.setClassName(context, componentClassName);
150 /** Runs {@code receiver} when the service becomes bound. */
151 public void runWhenBound(BoundWork<BoundService> receiver) {
152 synchronized (lock) {
153 pendingWork.add(receiver);
158 /** Unbinds the service associated with the binder. No-op if not bound. */
159 public void release() {
160 synchronized (lock) {
161 if (!hasCalledBind) {
162 logger.fine("Release is a no-op since not bound: %s", serviceClass);
165 // We need to release using a runWhenBound to avoid having a release jump ahead of
166 // pending work waiting for a bind (i.e., to preserve program order).
167 runWhenBound(new BoundWork<BoundService>() {
169 public void run(BoundService ignored) {
170 synchronized (lock) {
172 logger.fine("Unbinding %s from %s", serviceClass, serviceInstance);
174 context.unbindService(serviceConnection);
175 } catch (IllegalArgumentException exception) {
176 logger.fine("Exception unbinding from %s: %s", serviceClass,
177 exception.getMessage());
179 // Clear the now-stale reference and reset hasCalledBind so that we will initiate a
180 // bind on a subsequent call to runWhenBound.
181 serviceInstance = null;
182 hasCalledBind = false;
184 // This isn't necessarily wrong, but it's slightly odd.
185 if (!pendingWork.isEmpty()) {
186 logger.info("Still have %s work items in release of %s", pendingWork.size(),
196 * Returns {@code true} if the service binder is currently connected to the
199 public boolean isBoundForTest() {
200 synchronized (lock) {
201 return hasCalledBind;
206 public String toString() {
207 synchronized (lock) {
208 return this.getClass().getSimpleName() + "[" + serviceIntent + "]";
212 /** Returns the number of times {@code startBind} has been called, for tests. */
213 public int getNumStartBindForTest() {
214 return numStartBindForTest;
218 * Recursively process the queue of {@link #pendingWork}. Initiates a bind to the service if
219 * required. Else, if the service instance is available, removes the head of the queue and invokes
220 * it with the service instance.
222 * Note: this function differs from {@link #runWhenBound} only in that {@code runWhenBound}
223 * enqueues into {@link #pendingWork}.
225 private void handleQueue() {
226 if (queueHandlingInProgress) {
227 // Someone called back into runWhenBound from receiver.accept. We don't want to start another
228 // recursive call, since we're already handling the queue.
231 if (pendingWork.isEmpty()) {
232 // Recursive base case.
235 if (!hasCalledBind) {
236 // Initiate a bind if not bound.
237 Preconditions.checkState(serviceInstance == null,
238 "Bind not called but service instance is set: %s", serviceClass);
240 // May fail, but does its own logging. If it fails, we will never dispatch the work in the
241 // queue, but it's unclear what we can do in this case other than log.
245 if (serviceInstance == null) {
246 // Wait for the service to become bound if it is not yet available and a bind is in progress.
247 Preconditions.checkState(hasCalledBind, "No service instance and not waiting for bind: %s",
252 // Service is bound and available. Remove and invoke the head of the queue, then recurse to
253 // process the rest. We recurse because the head of the queue may have been a release(), which
254 // would have unbound the service, and we would need to reinvoke the binding code.
255 BoundWork<BoundService> work = pendingWork.remove();
256 queueHandlingInProgress = true;
257 work.run(serviceInstance);
258 queueHandlingInProgress = false;
263 * Binds to the service associated with the binder within the provided context. Returns whether
264 * binding was successfully initiated.
266 private boolean startBind() {
267 Preconditions.checkState(!hasCalledBind, "Bind already called for %s", serviceClass);
268 ++numStartBindForTest;
269 Intent bindIntent = getIntent();
270 if (!context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
271 logger.severe("Unable to bind to service: %s", bindIntent);
274 hasCalledBind = true;
278 /** Returns a bound service stub of the expected type. */
279 protected abstract BoundService asInterface(IBinder binder);