1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
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
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.server.telecom;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.os.IBinder;
24import android.os.IInterface;
25
26import com.android.internal.util.Preconditions;
27import com.google.common.base.Strings;
28
29import com.google.common.collect.Sets;
30
31import java.util.Collections;
32import java.util.Set;
33import java.util.concurrent.ConcurrentHashMap;
34
35/**
36 * Abstract class to perform the work of binding and unbinding to the specified service interface.
37 * Subclasses supply the service intent and component name and this class will invoke protected
38 * methods when the class is bound, unbound, or upon failure.
39 */
40abstract class ServiceBinder<ServiceInterface extends IInterface> {
41
42    /**
43     * Callback to notify after a binding succeeds or fails.
44     */
45    interface BindCallback {
46        void onSuccess();
47        void onFailure();
48    }
49
50    /**
51     * Listener for bind events on ServiceBinder.
52     */
53    interface Listener<ServiceBinderClass extends ServiceBinder<?>> {
54        void onUnbind(ServiceBinderClass serviceBinder);
55    }
56
57    /**
58     * Helper class to perform on-demand binding.
59     */
60    final class Binder {
61        /**
62         * Performs an asynchronous bind to the service (only if not already bound) and executes the
63         * specified callback.
64         *
65         * @param callback The callback to notify of the binding's success or failure.
66         */
67        void bind(BindCallback callback) {
68            ThreadUtil.checkOnMainThread();
69            Log.d(ServiceBinder.this, "bind()");
70
71            // Reset any abort request if we're asked to bind again.
72            clearAbort();
73
74            if (!mCallbacks.isEmpty()) {
75                // Binding already in progress, append to the list of callbacks and bail out.
76                mCallbacks.add(callback);
77                return;
78            }
79
80            mCallbacks.add(callback);
81            if (mServiceConnection == null) {
82                Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName);
83                ServiceConnection connection = new ServiceBinderConnection();
84
85                Log.d(ServiceBinder.this, "Binding to service with intent: %s", serviceIntent);
86                if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
87                    handleFailedConnection();
88                    return;
89                }
90            } else {
91                Log.d(ServiceBinder.this, "Service is already bound.");
92                Preconditions.checkNotNull(mBinder);
93                handleSuccessfulConnection();
94            }
95        }
96    }
97
98    private final class ServiceBinderConnection implements ServiceConnection {
99        @Override
100        public void onServiceConnected(ComponentName componentName, IBinder binder) {
101            ThreadUtil.checkOnMainThread();
102            Log.i(this, "Service bound %s", componentName);
103
104            // Unbind request was queued so unbind immediately.
105            if (mIsBindingAborted) {
106                clearAbort();
107                logServiceDisconnected("onServiceConnected");
108                mContext.unbindService(this);
109                handleFailedConnection();
110                return;
111            }
112
113            mServiceConnection = this;
114            setBinder(binder);
115            handleSuccessfulConnection();
116        }
117
118        @Override
119        public void onServiceDisconnected(ComponentName componentName) {
120            logServiceDisconnected("onServiceDisconnected");
121
122            mServiceConnection = null;
123            clearAbort();
124
125            handleServiceDisconnected();
126        }
127    }
128
129    /** The application context. */
130    private final Context mContext;
131
132    /** The intent action to use when binding through {@link Context#bindService}. */
133    private final String mServiceAction;
134
135    /** The component name of the service to bind to. */
136    private final ComponentName mComponentName;
137
138    /** The set of callbacks waiting for notification of the binding's success or failure. */
139    private final Set<BindCallback> mCallbacks = Sets.newHashSet();
140
141    /** Used to bind and unbind from the service. */
142    private ServiceConnection mServiceConnection;
143
144    /** The binder provided by {@link ServiceConnection#onServiceConnected} */
145    private IBinder mBinder;
146
147    private int mAssociatedCallCount = 0;
148
149    /**
150     * Indicates that an unbind request was made when the service was not yet bound. If the service
151     * successfully connects when this is true, it should be unbound immediately.
152     */
153    private boolean mIsBindingAborted;
154
155    /**
156     * Set of currently registered listeners.
157     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
158     * load factor before resizing, 1 means we only expect a single thread to
159     * access the map so make only a single shard
160     */
161    private final Set<Listener> mListeners = Collections.newSetFromMap(
162            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
163
164    /**
165     * Persists the specified parameters and initializes the new instance.
166     *
167     * @param serviceAction The intent-action used with {@link Context#bindService}.
168     * @param componentName The component name of the service with which to bind.
169     * @param context The context.
170     */
171    protected ServiceBinder(String serviceAction, ComponentName componentName, Context context) {
172        Preconditions.checkState(!Strings.isNullOrEmpty(serviceAction));
173        Preconditions.checkNotNull(componentName);
174
175        mContext = context;
176        mServiceAction = serviceAction;
177        mComponentName = componentName;
178    }
179
180    final void incrementAssociatedCallCount() {
181        mAssociatedCallCount++;
182        Log.v(this, "Call count increment %d, %s", mAssociatedCallCount,
183                mComponentName.flattenToShortString());
184    }
185
186    final void decrementAssociatedCallCount() {
187        if (mAssociatedCallCount > 0) {
188            mAssociatedCallCount--;
189            Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
190                    mComponentName.flattenToShortString());
191
192            if (mAssociatedCallCount == 0) {
193                unbind();
194            }
195        } else {
196            Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
197                    mComponentName.getClassName());
198        }
199    }
200
201    final int getAssociatedCallCount() {
202        return mAssociatedCallCount;
203    }
204
205    /**
206     * Unbinds from the service if already bound, no-op otherwise.
207     */
208    final void unbind() {
209        ThreadUtil.checkOnMainThread();
210
211        if (mServiceConnection == null) {
212            // We're not yet bound, so queue up an abort request.
213            mIsBindingAborted = true;
214        } else {
215            logServiceDisconnected("unbind");
216            mContext.unbindService(mServiceConnection);
217            mServiceConnection = null;
218            setBinder(null);
219        }
220    }
221
222    final ComponentName getComponentName() {
223        return mComponentName;
224    }
225
226    final boolean isServiceValid(String actionName) {
227        if (mBinder == null) {
228            Log.w(this, "%s invoked while service is unbound", actionName);
229            return false;
230        }
231
232        return true;
233    }
234
235    final void addListener(Listener listener) {
236        mListeners.add(listener);
237    }
238
239    final void removeListener(Listener listener) {
240        if (listener != null) {
241            mListeners.remove(listener);
242        }
243    }
244
245    /**
246     * Logs a standard message upon service disconnection. This method exists because there is no
247     * single method called whenever the service unbinds and we want to log the same string in all
248     * instances where that occurs.  (Context.unbindService() does not cause onServiceDisconnected
249     * to execute).
250     *
251     * @param sourceTag Tag to disambiguate
252     */
253    private void logServiceDisconnected(String sourceTag) {
254        Log.i(this, "Service unbound %s, from %s.", mComponentName, sourceTag);
255    }
256
257    /**
258     * Notifies all the outstanding callbacks that the service is successfully bound. The list of
259     * outstanding callbacks is cleared afterwards.
260     */
261    private void handleSuccessfulConnection() {
262        for (BindCallback callback : mCallbacks) {
263            callback.onSuccess();
264        }
265        mCallbacks.clear();
266    }
267
268    /**
269     * Notifies all the outstanding callbacks that the service failed to bind. The list of
270     * outstanding callbacks is cleared afterwards.
271     */
272    private void handleFailedConnection() {
273        for (BindCallback callback : mCallbacks) {
274            callback.onFailure();
275        }
276        mCallbacks.clear();
277    }
278
279    /**
280     * Handles a service disconnection.
281     */
282    private void handleServiceDisconnected() {
283        setBinder(null);
284    }
285
286    private void clearAbort() {
287        mIsBindingAborted = false;
288    }
289
290    /**
291     * Sets the (private) binder and updates the child class.
292     *
293     * @param binder The new binder value.
294     */
295    private void setBinder(IBinder binder) {
296        if (mBinder != binder) {
297            mBinder = binder;
298
299            setServiceInterface(binder);
300
301            if (binder == null) {
302                for (Listener l : mListeners) {
303                    l.onUnbind(this);
304                }
305            }
306        }
307    }
308
309    /**
310     * Sets the service interface after the service is bound or unbound.
311     *
312     * @param binder The actual bound service implementation.
313     */
314    protected abstract void setServiceInterface(IBinder binder);
315}
316