Adding the lookup logic for call-service selectors.
Change-Id: If18628f9b4854b8055ec43d8e33c0c6f822454cd
diff --git a/src/com/android/telecomm/CallServiceFinder.java b/src/com/android/telecomm/CallServiceFinder.java
index 898877f..4ee73c3 100644
--- a/src/com/android/telecomm/CallServiceFinder.java
+++ b/src/com/android/telecomm/CallServiceFinder.java
@@ -71,6 +71,8 @@
/**
* Used to generate unique lookup-cycle identifiers. Incremented upon initiateLookup calls.
+ * TODO(gilad): If at all useful, consider porting the cycle ID concept to switchboard and
+ * have it centralized/shared between the two finders.
*/
private int mNextLookupId = 0;
@@ -87,8 +89,8 @@
* timeout occurs, the complete set of (available) call services is passed to the switchboard
* for further processing of outgoing calls etc. When the timeout occurs before all responds
* are received, the partial (potentially empty) set gets passed (to the switchboard) instead.
- * Note that cached providers do not require finding and hence are excluded from this count.
- * Also noteworthy is that providers are dynamically removed from this set as they register.
+ * Cached providers do not require finding and hence are excluded from this set. Entries are
+ * removed from this set as providers register.
*/
private Set<ComponentName> mUnregisteredProviders;
@@ -151,7 +153,7 @@
int providerCount = providerNames.size();
int unregisteredProviderCount = mUnregisteredProviders.size();
- Log.i(TAG, "Found " + providerCount + " implementations for ICallServiceProvider, "
+ Log.i(TAG, "Found " + providerCount + " implementations of ICallServiceProvider, "
+ unregisteredProviderCount + " of which are not currently registered.");
if (unregisteredProviderCount == 0) {
@@ -198,7 +200,7 @@
* @param lookupId The lookup-cycle ID.
* @param context The relevant application context.
*/
- void bindProvider(
+ private void bindProvider(
final ComponentName providerName, final int lookupId, Context context) {
Preconditions.checkNotNull(providerName);
@@ -263,6 +265,8 @@
* current lookup cycle.
* TODO(santoscordon): Consider replacing this method's use of synchronized with a Handler
* queue.
+ * TODO(gilad): Santos has some POC code to make synchronized (below) unnecessary, either
+ * move to use that or remove this to-do.
*
* @param lookupId The lookup-cycle ID.
* @param providerName The component name of the relevant provider.
@@ -276,7 +280,7 @@
List<ICallService> callServices) {
// TODO(santoscordon): When saving the call services into this class, also add code to
- // unregister (remove) the call services upon disconnect. Potenially use RemoteCallbackList.
+ // unregister (remove) the call services upon disconnect. Potentially use RemoteCallbackList.
if (mUnregisteredProviders.remove(providerName)) {
mProviderRegistry.add(provider);
@@ -314,6 +318,7 @@
*/
private void updateSwitchboard() {
ThreadUtil.checkOnMainThread();
+
// TODO(gilad): More here.
mSwitchboard.setCallServices(null);
}
diff --git a/src/com/android/telecomm/CallServiceSelectorFinder.java b/src/com/android/telecomm/CallServiceSelectorFinder.java
new file mode 100644
index 0000000..50a4393
--- /dev/null
+++ b/src/com/android/telecomm/CallServiceSelectorFinder.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 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.telecomm;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.telecomm.ICallServiceSelector;
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class to retrieve {@link ICallServiceSelector} implementations on the device and
+ * asynchronously bind to them. Each lookup cycle is time-boxed, hence selectors too slow
+ * to bind are effectively omitted from the set that is passed back to {@link Switchboard}.
+ */
+final class CallServiceSelectorFinder {
+
+ private static final String TAG = CallServiceSelectorFinder.class.getSimpleName();
+
+ /**
+ * The longest period in milliseconds each lookup cycle is allowed to span over, see
+ * {@link #mLookupTerminator}.
+ * TODO(gilad): Likely requires tuning.
+ */
+ private static final int LOOKUP_TIMEOUT_MS = 100;
+
+ /**
+ * Used to retrieve all known ICallServiceSelector implementations from the framework.
+ */
+ private static final String CALL_SERVICE_SELECTOR_CLASS_NAME =
+ ICallServiceSelector.class.getName();
+
+ /**
+ * Used to interrupt lookup cycles that didn't terminate naturally within the allowed
+ * period, see LOOKUP_TIMEOUT.
+ */
+ private final Runnable mLookupTerminator = new Runnable() {
+ @Override
+ public void run() {
+ terminateLookup();
+ }
+ };
+
+ /** Used to run code (e.g. messages, Runnables) on the main (UI) thread. */
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ private final Switchboard mSwitchboard;
+
+ /**
+ * Determines whether or not a lookup cycle is already running.
+ */
+ private boolean mIsLookupInProgress = false;
+
+ /**
+ * Used to generate unique lookup-cycle identifiers. Incremented upon initiateLookup calls.
+ * TODO(gilad): If at all useful, consider porting the cycle ID concept to switchboard and
+ * have it centralized/shared between the two finders.
+ */
+ private int mNextLookupId = 0;
+
+ /**
+ * The set of bound call-service selectors. Only populated via initiateLookup scenarios.
+ * Selectors should only be removed upon unbinding.
+ */
+ private Set<ICallServiceSelector> mSelectorRegistry = Sets.newHashSet();
+
+ /**
+ * Stores the names of the selectors to bind to in one lookup cycle. The set size represents
+ * the number of call-service selectors this finder expects to hear back from upon initiating
+ * lookups, see initiateLookup. Whenever all selectors respond before the timeout occurs, the
+ * complete set of available selectors is passed to the switchboard for further processing of
+ * outgoing calls etc. When the timeout occurs before all responds are received, the partial
+ * (potentially empty) set gets passed to the switchboard instead. Note that cached selectors
+ * do not require finding and hence are excluded from this set. Also note that selectors are
+ * removed from this set as they register.
+ */
+ private Set<ComponentName> mUnregisteredSelectors;
+
+ /**
+ * Persists the specified parameters.
+ *
+ * @param switchboard The switchboard for this finer to work against.
+ */
+ CallServiceSelectorFinder(Switchboard switchboard) {
+ mSwitchboard = switchboard;
+ }
+
+ /**
+ * Initiates a lookup cycle for call-service selectors. Must be called from the UI thread.
+ *
+ * @param context The relevant application context.
+ */
+ void initiateLookup(Context context) {
+ ThreadUtil.checkOnMainThread();
+ if (mIsLookupInProgress) {
+ // At most one active lookup is allowed at any given time, bail out.
+ return;
+ }
+
+ List<ComponentName> selectorNames = getSelectorNames(context);
+ if (selectorNames.isEmpty()) {
+ Log.i(TAG, "No ICallServiceSelector implementations found.");
+ updateSwitchboard();
+ return;
+ }
+
+ mIsLookupInProgress = true;
+ mUnregisteredSelectors = Sets.newHashSet();
+
+ int lookupId = mNextLookupId++;
+ for (ComponentName name : selectorNames) {
+ if (!mSelectorRegistry.contains(name)) {
+ // The selector is either not yet registered or has been unregistered
+ // due to unbinding etc.
+ mUnregisteredSelectors.add(name);
+ bindSelector(name, lookupId, context);
+ }
+ }
+
+ int selectorCount = selectorNames.size();
+ int unregisteredSelectorCount = mUnregisteredSelectors.size();
+
+ Log.i(TAG, "Found " + selectorCount + " implementations of ICallServiceSelector, "
+ + unregisteredSelectorCount + " of which are not currently registered.");
+
+ if (unregisteredSelectorCount == 0) {
+ // All known (selector) implementations are already registered, pass control
+ // back to the switchboard.
+ updateSwitchboard();
+ } else {
+ // Schedule a lookup terminator to run after LOOKUP_TIMEOUT_MS milliseconds.
+ mHandler.removeCallbacks(mLookupTerminator);
+ mHandler.postDelayed(mLookupTerminator, LOOKUP_TIMEOUT_MS);
+ }
+ }
+
+ /**
+ * Returns the all-inclusive list of call-service selector names.
+ *
+ * @param context The relevant/application context to query against.
+ * @return The list containing the (component) names of all known ICallServiceSelector
+ * implementations or the empty list upon no available selectors.
+ */
+ private List<ComponentName> getSelectorNames(Context context) {
+ // The list of selector names to return to the caller, may be populated below.
+ List<ComponentName> selectorNames = Lists.newArrayList();
+
+ PackageManager packageManager = context.getPackageManager();
+ Intent intent = new Intent(CALL_SERVICE_SELECTOR_CLASS_NAME);
+ for (ResolveInfo entry : packageManager.queryIntentServices(intent, 0)) {
+ ServiceInfo serviceInfo = entry.serviceInfo;
+ if (serviceInfo != null) {
+ // The entry resolves to a proper service, add it to the list of selector names.
+ selectorNames.add(
+ new ComponentName(serviceInfo.packageName, serviceInfo.name));
+ }
+ }
+
+ return selectorNames;
+ }
+
+ /**
+ * Attempts to bind the specified selector and have it register upon successful binding.
+ * Also performs the necessary wiring to unregister the selector upon un-binding.
+ *
+ * @param selectorName The component name of the relevant selector.
+ * @param lookupId The lookup-cycle ID.
+ * @param context The relevant application context.
+ */
+ private void bindSelector(
+ final ComponentName selectorName, final int lookupId, Context context) {
+
+ Preconditions.checkNotNull(selectorName);
+ Preconditions.checkNotNull(context);
+
+ Intent serviceIntent =
+ new Intent(CALL_SERVICE_SELECTOR_CLASS_NAME).setComponent(selectorName);
+ Log.i(TAG, "Binding to ICallServiceSelector through " + serviceIntent);
+
+ // Connection object for the service binding.
+ ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ ICallServiceSelector selector = ICallServiceSelector.Stub.asInterface(service);
+ registerSelector(lookupId, selectorName, selector);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ unregisterSelector(selectorName);
+ }
+ };
+
+ if (!context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
+ // TODO(gilad): Handle error.
+ }
+ }
+
+ /**
+ * Registers the specified selector.
+ *
+ * @param lookupId The lookup-cycle ID. Currently unused, consider removing.
+ * @param selectorName The component name of the relevant selector.
+ * @param selector The selector object to register.
+ */
+ private void registerSelector(
+ int lookupId, ComponentName selectorName, ICallServiceSelector selector) {
+
+ ThreadUtil.checkOnMainThread();
+
+ if (mUnregisteredSelectors.remove(selectorName)) {
+ mSelectorRegistry.add(selector);
+ if (mUnregisteredSelectors.size() < 1) {
+ terminateLookup(); // No other selectors to wait for.
+ }
+ }
+ }
+
+ /**
+ * Unregisters the specified selector.
+ *
+ * @param selectorName The component name of the relevant selector.
+ */
+ private void unregisterSelector(ComponentName selectorName) {
+ mSelectorRegistry.remove(selectorName);
+ }
+
+ /**
+ * Timeouts the current lookup cycle, see LookupTerminator.
+ */
+ private void terminateLookup() {
+ mHandler.removeCallbacks(mLookupTerminator);
+
+ updateSwitchboard();
+ mIsLookupInProgress = false;
+ }
+
+ /**
+ * Updates the switchboard passing the relevant call services selectors.
+ */
+ private void updateSwitchboard() {
+ ThreadUtil.checkOnMainThread();
+
+ // TODO(gilad): More here.
+ mSwitchboard.setSelectors(null);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index e9f9033..4abebce 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -22,11 +22,13 @@
import android.content.Context;
import android.os.RemoteException;
import android.telecomm.ICallService;
+import android.telecomm.ICallServiceSelector;
import android.util.Log;
import com.android.telecomm.exceptions.CallServiceUnavailableException;
import com.android.telecomm.exceptions.OutgoingCallException;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -42,7 +44,17 @@
private static final String TAG = Switchboard.class.getSimpleName();
- private CallServiceFinder callServiceFinder = new CallServiceFinder(this);
+ private CallServiceFinder mCallServiceFinder = new CallServiceFinder(this);
+
+ private CallServiceSelectorFinder mSelectorFinder = new CallServiceSelectorFinder(this);
+
+ /** TODO(gilad): Add comment, may also want to use a set instead. */
+ /** TODO(gilad): Null out once the active-call count goes to zero. */
+ private List<ICallService> mCallServices;
+
+ /** TODO(gilad): Add comment, may also want to use a set instead. */
+ /** TODO(gilad): Null out once the active-call count goes to zero. */
+ private List<ICallServiceSelector> mSelectors;
private Set<Call> mPendingOutgoingCalls = Sets.newHashSet();
@@ -50,6 +62,7 @@
* Places an outgoing call to the handle passed in. Method asynchronously collects
* {@link ICallService} implementations and passes them along with the handle and contactInfo
* to {@link #placeOutgoingCallInternal} to actually place the call.
+ * TODO(gilad): Update.
*
* @param handle The handle to dial.
* @param contactInfo Information about the entity being called.
@@ -57,41 +70,109 @@
*/
void placeOutgoingCall(String handle, ContactInfo contactInfo, Context context) {
ThreadUtil.checkOnMainThread();
- mPendingOutgoingCalls.add(new Call(handle, contactInfo));
- callServiceFinder.initiateLookup(context);
+
+ // TODO(gilad): Consider creating the call object even earlier, e.g. in CallsManager.
+ Call call = new Call(handle, contactInfo);
+ boolean bailout = false;
+ if (isNullOrEmpty(mCallServices)) {
+ mCallServiceFinder.initiateLookup(context);
+ bailout = true;
+ }
+ if (isNullOrEmpty(mSelectors)) {
+ mSelectorFinder.initiateLookup(context);
+ bailout = true;
+ }
+
+ if (bailout) {
+ // Unable to process the call without either call service, selectors, or both.
+ // Store the call for deferred processing and bail out.
+ mPendingOutgoingCalls.add(call);
+ return;
+ }
+
+ placeOutgoingCall(call);
}
/**
* Persists the specified list of call services and attempts to connect any pending outgoing
- * calls still waiting for a matching call-service to be initiated.
+ * calls still waiting for a matching call-service to be initiated. Intended to be called by
+ * {@link CallServiceFinder} exclusively.
*
- * @param callServices The potentially-partial list of call services the switchboard should
- * feel free to make use of. Partial since the lookup procedure is time-boxed such that
- * some providers/call-services may be too slow to respond and hence effectively omitted
- * from the specified list. If the switchboard has previous/reliable knowledge of other
- * call-services, it should be at liberty to use these just as well.
+ * @param callServices The potentially-partial list of call services. Partial since the
+ * lookup procedure is time-boxed, such that some providers/call-services may be slow
+ * to respond and hence effectively omitted from the specified list.
*/
void setCallServices(List<ICallService> callServices) {
ThreadUtil.checkOnMainThread();
- for (Call pendingCall : mPendingOutgoingCalls) {
- // TODO(gilad): Iterate through the prioritized list of switchboard policies passing
- // to each policy the call object as well as all known call services. Break out of
- // the inner/policy loop as soon as the first matching policy for the call is found.
- // Calls for which no matching policy can be found will be killed by cleanup/monitor
- // thread, see the "monitor" to-do at the top of the file.
- // Psuedo code (assuming connect to be a future switchboard method):
- //
- // FOR policy IN prioritizedPolicies:
- // IF policy.is_applicable_to(pendingCall, callServices):
- // TRY
- // connect(pendingCall, callServices, policy)
- // mPendingOutgoingCalls.remove(pendingCall)
- // BREAK
+ mCallServices = callServices;
+ processPendingOutgoingCalls();
+ }
+
+ /**
+ * Persists the specified list of selectors and attempts to connect any pending outgoing
+ * calls. Intended to be called by {@link CallServiceSelectorFinder} exclusively.
+ *
+ * @param selectors The potentially-partial list of selectors. Partial since the lookup
+ * procedure is time-boxed, such that some selectors may be slow to respond and hence
+ * effectively omitted from the specified list.
+ */
+ void setSelectors(List<ICallServiceSelector> selectors) {
+ ThreadUtil.checkOnMainThread();
+
+ mSelectors = selectors;
+ processPendingOutgoingCalls();
+ }
+
+ /**
+ * Attempts to process any pending outgoing calls that have not yet been expired.
+ */
+ void processPendingOutgoingCalls() {
+ for (Call call : mPendingOutgoingCalls) {
+ placeOutgoingCall(call);
}
}
/**
+ * Attempts to place the specified call.
+ *
+ * @param call The call to put through.
+ */
+ private void placeOutgoingCall(Call call) {
+ if (isNullOrEmpty(mCallServices) || isNullOrEmpty(mSelectors)) {
+ // At least one call service and one selector are required to process outgoing calls.
+ return;
+ }
+
+ // TODO(gilad): Iterate through the prioritized list of selectors passing to each selector
+ // (read-only versions of) the call object and all available call services. Break out once
+ // a matching selector is found. Calls with no matching selectors will eventually be killed
+ // by the cleanup/monitor thread, see the "monitor" to-do at the top of the file.
+
+ // Psuedo code (assuming connect to be a future switchboard method):
+ //
+ // FOR selector IN prioritizedSelectors:
+ // prioritizedCallServices = selector.select(mCallServices, call)
+ // IF notEmpty(prioritizedCallServices):
+ // FOR callService IN prioritizedCallServices:
+ // TRY
+ // connect(call, callService, selector)
+ // mPendingOutgoingCalls.remove(call)
+ // BREAK
+ }
+
+ /**
+ * Determines whether or not the specified collection is either null or empty.
+ *
+ * @param collection Either null or the collection object to be evaluated.
+ * @return True if the collection is null or empty.
+ */
+ @SuppressWarnings("rawtypes")
+ private boolean isNullOrEmpty(Collection collection) {
+ return collection == null || collection.isEmpty();
+ }
+
+ /**
* Places an outgoing call to the handle passed in. Given a list of {@link ICallServices},
* select one and place a call to the handle.
* TODO(santoscordon): How does the CallService selection process work?
@@ -156,16 +237,4 @@
// // RemoteExceptions also be converted to OutgoingCallExceptions thrown by call()?
// }
// }
-
- /**
- * Sorts a list of {@link ICallService} ordered by the preferred service for dialing the call.
- *
- * @param callServices The list to order.
- */
- private List<ICallService> sort(List<ICallService> callServices) {
- // TODO(android-contacts): Sort by reliability, cost, and ultimately
- // the desirability to issue a given call over each of the specified
- // call services.
- return callServices;
- }
}