/* * Copyright (C) 2014 The Android Open Source Project * * 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.android.server.telecom; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArraySet; import com.android.internal.util.Preconditions; import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * Abstract class to perform the work of binding and unbinding to the specified service interface. * Subclasses supply the service intent and component name and this class will invoke protected * methods when the class is bound, unbound, or upon failure. */ abstract class ServiceBinder { /** * Callback to notify after a binding succeeds or fails. */ interface BindCallback { void onSuccess(); void onFailure(); } /** * Listener for bind events on ServiceBinder. */ interface Listener { void onUnbind(ServiceBinderClass serviceBinder); } /** * Helper class to perform on-demand binding. */ final class Binder2 { /** * Performs an asynchronous bind to the service (only if not already bound) and executes the * specified callback. * * @param callback The callback to notify of the binding's success or failure. * @param call The call for which we are being bound. */ void bind(BindCallback callback, Call call) { Log.d(ServiceBinder.this, "bind()"); // Reset any abort request if we're asked to bind again. clearAbort(); if (!mCallbacks.isEmpty()) { // Binding already in progress, append to the list of callbacks and bail out. mCallbacks.add(callback); return; } mCallbacks.add(callback); if (mServiceConnection == null) { Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName); ServiceConnection connection = new ServiceBinderConnection(call); Log.event(call, Log.Events.BIND_CS, mComponentName); final int bindingFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE; final boolean isBound; if (mUserHandle != null) { isBound = mContext.bindServiceAsUser(serviceIntent, connection, bindingFlags, mUserHandle); } else { isBound = mContext.bindService(serviceIntent, connection, bindingFlags); } if (!isBound) { handleFailedConnection(); return; } } else { Log.d(ServiceBinder.this, "Service is already bound."); Preconditions.checkNotNull(mBinder); handleSuccessfulConnection(); } } } private final class ServiceBinderConnection implements ServiceConnection { /** * The initial call for which the service was bound. */ private Call mCall; ServiceBinderConnection(Call call) { mCall = call; } @Override public void onServiceConnected(ComponentName componentName, IBinder binder) { synchronized (mLock) { Log.i(this, "Service bound %s", componentName); Log.event(mCall, Log.Events.CS_BOUND, componentName); mCall = null; // Unbind request was queued so unbind immediately. if (mIsBindingAborted) { clearAbort(); logServiceDisconnected("onServiceConnected"); mContext.unbindService(this); handleFailedConnection(); return; } mServiceConnection = this; setBinder(binder); handleSuccessfulConnection(); } } @Override public void onServiceDisconnected(ComponentName componentName) { synchronized (mLock) { logServiceDisconnected("onServiceDisconnected"); mServiceConnection = null; clearAbort(); handleServiceDisconnected(); } } } /** The application context. */ private final Context mContext; /** The Telecom lock object. */ protected final TelecomSystem.SyncRoot mLock; /** The intent action to use when binding through {@link Context#bindService}. */ private final String mServiceAction; /** The component name of the service to bind to. */ private final ComponentName mComponentName; /** The set of callbacks waiting for notification of the binding's success or failure. */ private final Set mCallbacks = new ArraySet<>(); /** Used to bind and unbind from the service. */ private ServiceConnection mServiceConnection; /** {@link UserHandle} to use for binding, to support work profiles and multi-user. */ private UserHandle mUserHandle; /** The binder provided by {@link ServiceConnection#onServiceConnected} */ private IBinder mBinder; private int mAssociatedCallCount = 0; /** * Indicates that an unbind request was made when the service was not yet bound. If the service * successfully connects when this is true, it should be unbound immediately. */ private boolean mIsBindingAborted; /** * Set of currently registered listeners. * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is * load factor before resizing, 1 means we only expect a single thread to * access the map so make only a single shard */ private final Set mListeners = Collections.newSetFromMap( new ConcurrentHashMap(8, 0.9f, 1)); /** * Persists the specified parameters and initializes the new instance. * * @param serviceAction The intent-action used with {@link Context#bindService}. * @param componentName The component name of the service with which to bind. * @param context The context. * @param userHandle The {@link UserHandle} to use for binding. */ protected ServiceBinder(String serviceAction, ComponentName componentName, Context context, TelecomSystem.SyncRoot lock, UserHandle userHandle) { Preconditions.checkState(!TextUtils.isEmpty(serviceAction)); Preconditions.checkNotNull(componentName); mContext = context; mLock = lock; mServiceAction = serviceAction; mComponentName = componentName; mUserHandle = userHandle; } final void incrementAssociatedCallCount() { mAssociatedCallCount++; Log.v(this, "Call count increment %d, %s", mAssociatedCallCount, mComponentName.flattenToShortString()); } final void decrementAssociatedCallCount() { if (mAssociatedCallCount > 0) { mAssociatedCallCount--; Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount, mComponentName.flattenToShortString()); if (mAssociatedCallCount == 0) { unbind(); } } else { Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero", mComponentName.getClassName()); } } final int getAssociatedCallCount() { return mAssociatedCallCount; } /** * Unbinds from the service if already bound, no-op otherwise. */ final void unbind() { if (mServiceConnection == null) { // We're not yet bound, so queue up an abort request. mIsBindingAborted = true; } else { logServiceDisconnected("unbind"); mContext.unbindService(mServiceConnection); mServiceConnection = null; setBinder(null); } } final ComponentName getComponentName() { return mComponentName; } final boolean isServiceValid(String actionName) { if (mBinder == null) { Log.w(this, "%s invoked while service is unbound", actionName); return false; } return true; } final void addListener(Listener listener) { mListeners.add(listener); } final void removeListener(Listener listener) { if (listener != null) { mListeners.remove(listener); } } /** * Logs a standard message upon service disconnection. This method exists because there is no * single method called whenever the service unbinds and we want to log the same string in all * instances where that occurs. (Context.unbindService() does not cause onServiceDisconnected * to execute). * * @param sourceTag Tag to disambiguate */ private void logServiceDisconnected(String sourceTag) { Log.i(this, "Service unbound %s, from %s.", mComponentName, sourceTag); } /** * Notifies all the outstanding callbacks that the service is successfully bound. The list of * outstanding callbacks is cleared afterwards. */ private void handleSuccessfulConnection() { for (BindCallback callback : mCallbacks) { callback.onSuccess(); } mCallbacks.clear(); } /** * Notifies all the outstanding callbacks that the service failed to bind. The list of * outstanding callbacks is cleared afterwards. */ private void handleFailedConnection() { for (BindCallback callback : mCallbacks) { callback.onFailure(); } mCallbacks.clear(); } /** * Handles a service disconnection. */ private void handleServiceDisconnected() { setBinder(null); } private void clearAbort() { mIsBindingAborted = false; } /** * Sets the (private) binder and updates the child class. * * @param binder The new binder value. */ private void setBinder(IBinder binder) { if (mBinder != binder) { mBinder = binder; setServiceInterface(binder); if (binder == null) { for (Listener l : mListeners) { l.onUnbind(this); } } } } /** * Sets the service interface after the service is bound or unbound. * * @param binder The actual bound service implementation. */ protected abstract void setServiceInterface(IBinder binder); }