blob: 0f77567eb553d8e5c2fd2580ce3f3cdfedccd2d6 [file] [log] [blame]
/*
* 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.os.Handler;
import android.os.Message;
import android.telecomm.CallServiceDescriptor;
import com.android.telecomm.BaseRepository.LookupCallback;
import com.google.android.collect.Sets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility class to place a call using the specified set of call-services and ordered selectors.
* Iterates through the selectors and gets a sorted list of supported call-service descriptors
* for each selector. Upon receiving each sorted list (one list per selector), each of the
* corresponding call services is then attempted until either the outgoing call is placed, the
* attempted call is aborted, or the list is exhausted -- whichever occurs first.
*
* Except for the abort case, all other scenarios should terminate with the call notified
* of the result.
*
* NOTE(gilad): Currently operating under the assumption that we'll have one timeout per (outgoing)
* call attempt. If we (also) like to timeout individual selectors and/or call services, the code
* here will need to be re-factored (quite a bit) to support that.
*/
final class OutgoingCallProcessor {
private final static int MSG_EXPIRE = 1;
/**
* The outgoing call this processor is tasked with placing.
*/
private final Call mCall;
/**
* The map of currently-available call-service implementations keyed by call-service ID.
*/
private final Map<String, CallServiceWrapper> mCallServicesById = Maps.newHashMap();
/**
* The set of attempted call services, used to ensure services are attempted at most once per
* outgoing-call attempt.
*/
private final Set<CallServiceWrapper> mAttemptedCallServices = Sets.newHashSet();
private final CallServiceRepository mCallServiceRepository;
private final CallServiceSelectorRepository mSelectorRepository;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_EXPIRE:
abort();
break;
}
}
};
/**
* The duplicate-free list of currently-available call-service descriptors.
*/
private List<CallServiceDescriptor> mCallServiceDescriptors;
/**
* The iterator over the currently-selected ordered list of call-service descriptors.
*/
private Iterator<CallServiceDescriptor> mCallServiceDescriptorIterator;
/**
* The list of currently-available call-service selector implementations.
*/
private Collection<CallServiceSelectorWrapper> mSelectors;
private Iterator<CallServiceSelectorWrapper> mSelectorIterator;
private AsyncResultCallback<Boolean> mResultCallback;
private boolean mIsAborted = false;
/**
* Persists the specified parameters and iterates through the prioritized list of selectors
* passing to each selector (read-only versions of) the call object and all available call-
* service descriptors. Stops once a matching selector is found. Calls with no matching
* selectors will eventually be killed by the cleanup/monitor switchboard handler, which will
* in turn call the abort method of this processor via {@link OutgoingCallsManager}.
*
* @param call The call to place.
* @param callServiceRepository
* @param selectorRepository
* @param resultCallback The callback on which to return the result.
*/
OutgoingCallProcessor(
Call call,
CallServiceRepository callServiceRepository,
CallServiceSelectorRepository selectorRepository,
AsyncResultCallback<Boolean> resultCallback) {
ThreadUtil.checkOnMainThread();
mCall = call;
mResultCallback = resultCallback;
mCallServiceRepository = callServiceRepository;
mSelectorRepository = selectorRepository;
}
/**
* Initiates the attempt to place the call. No-op beyond the first invocation.
*/
void process() {
Log.v(this, "process, mIsAborted: %b", mIsAborted);
if (!mIsAborted) {
// Start the expiration timeout.
mHandler.sendEmptyMessageDelayed(MSG_EXPIRE, Timeouts.getNewOutgoingCallMillis());
// Lookup call services
mCallServiceRepository.lookupServices(new LookupCallback<CallServiceWrapper>() {
@Override
public void onComplete(Collection<CallServiceWrapper> services) {
setCallServices(services);
}
});
if (mCall.getCallServiceSelector() == null) {
// Lookup selectors
mSelectorRepository.lookupServices(
new LookupCallback<CallServiceSelectorWrapper>() {
@Override
public void onComplete(
Collection<CallServiceSelectorWrapper> selectors) {
setSelectors(selectors);
}
});
} else {
setSelectors(ImmutableList.of(mCall.getCallServiceSelector()));
}
}
}
/**
* Aborts the attempt to place the relevant call. Intended to be invoked by
* switchboard through the outgoing-calls manager.
*/
void abort() {
Log.v(this, "abort");
ThreadUtil.checkOnMainThread();
if (!mIsAborted && mResultCallback != null) {
mIsAborted = true;
// On an abort, we need to check if we already told the call service to place the
// call. If so, we need to tell it to abort.
// TODO(santoscordon): The call service is saved with the call and so we have to query
// the call to get it, which is a bit backwards. Ideally, the call service would be
// saved inside this class until the whole thing is complete and then set on the call.
CallServiceWrapper callService = mCall.getCallService();
if (callService != null) {
callService.abort(mCall);
}
sendResult(false);
}
}
boolean isAborted() {
return mIsAborted;
}
/**
* Completes the outgoing call sequence by setting the call service on the call object. This is
* invoked when the call service adapter receives positive confirmation that the call service
* placed the call.
*/
void handleSuccessfulCallAttempt(CallServiceWrapper callService) {
Log.v(this, "handleSuccessfulCallAttempt");
ThreadUtil.checkOnMainThread();
if (mIsAborted) {
// TODO(gilad): Ask the call service to drop the call?
callService.abort(mCall);
return;
}
sendResult(true);
}
/**
* Attempts the next call service if the specified call service is the one currently being
* attempted.
*
* @param reason The call-service supplied reason for the failed call attempt.
*/
void handleFailedCallAttempt(String reason) {
Log.v(this, "handleFailedCallAttempt");
if (!mIsAborted) {
ThreadUtil.checkOnMainThread();
attemptNextCallService();
}
}
/**
* Persists the ordered-list of call-service descriptor as selected by the current selector and
* starts iterating through the corresponding call services continuing the attempt to place the
* call.
*
* @param descriptors The (ordered) list of call-service descriptor.
*/
void processSelectedCallServices(List<CallServiceDescriptor> descriptors) {
Log.v(this, "processSelectedCallServices");
if (descriptors == null || descriptors.isEmpty()) {
attemptNextSelector();
} else if (mCallServiceDescriptorIterator == null) {
mCallServiceDescriptorIterator = descriptors.iterator();
attemptNextCallService();
}
}
/**
* Sets the call services to attempt for this outgoing call.
*
* @param callServices The call services.
*/
private void setCallServices(Collection<CallServiceWrapper> callServices) {
mCallServiceDescriptors = new ArrayList<>();
// Populate the list and map of call-service descriptors. The list is needed since
// it's being passed down to selectors.
for (CallServiceWrapper callService : callServices) {
CallServiceDescriptor descriptor = callService.getDescriptor();
mCallServiceDescriptors.add(descriptor);
mCallServicesById.put(descriptor.getCallServiceId(), callService);
}
onLookupComplete();
}
/**
* Sets the selectors to attemnpt for this outgoing call.
*
* @param selectors The call-service selectors.
*/
private void setSelectors(Collection<CallServiceSelectorWrapper> selectors) {
mSelectors = adjustForEmergencyCalls(selectors);
onLookupComplete();
}
private void onLookupComplete() {
if (!mIsAborted && mSelectors != null && mCallServiceDescriptors != null) {
if (mSelectorIterator == null) {
mSelectorIterator = mSelectors.iterator();
attemptNextSelector();
}
}
}
/**
* Updates the specified collection of selectors to accomodate for emergency calls and any
* preferred selectors specified in the call object.
*
* @param selectors The selectors found through the selector repository.
*/
private Collection<CallServiceSelectorWrapper> adjustForEmergencyCalls(
Collection<CallServiceSelectorWrapper> selectors) {
boolean useEmergencySelector =
EmergencyCallServiceSelector.shouldUseSelector(mCall.getHandle());
Log.d(this, "processNewOutgoingCall, isEmergency=%b", useEmergencySelector);
if (useEmergencySelector) {
// This is potentially an emergency call so add the emergency selector before the
// other selectors.
ImmutableList.Builder<CallServiceSelectorWrapper> selectorsBuilder =
ImmutableList.builder();
ComponentName componentName = new ComponentName(
TelecommApp.getInstance(), EmergencyCallServiceSelector.class);
CallServiceSelectorWrapper emergencySelector =
new CallServiceSelectorWrapper(
componentName.flattenToShortString(),
componentName,
CallsManager.getInstance());
selectorsBuilder.add(emergencySelector);
selectorsBuilder.addAll(selectors);
selectors = selectorsBuilder.build();
}
return selectors;
}
/**
* Attempts to place the call using the next selector, no-op if no other selectors
* are available.
*/
private void attemptNextSelector() {
Log.v(this, "attemptNextSelector, mIsAborted: %b", mIsAborted);
if (mIsAborted) {
return;
}
if (mSelectorIterator.hasNext()) {
CallServiceSelectorWrapper selector = mSelectorIterator.next();
mCall.setCallServiceSelector(selector);
selector.select(mCall, mCallServiceDescriptors,
new AsyncResultCallback<List<CallServiceDescriptor>>() {
@Override
public void onResult(List<CallServiceDescriptor> descriptors) {
processSelectedCallServices(descriptors);
}
});
} else {
Log.v(this, "attemptNextSelector, no more selectors, failing");
mCall.clearCallServiceSelector();
sendResult(false);
}
}
/**
* Attempts to place the call using the call service specified by the next call-service
* descriptor of mCallServiceDescriptorIterator. If there are no more call services to
* attempt, the process continues to the next call-service selector via
* {@link #attemptNextSelector}.
*/
private void attemptNextCallService() {
Log.v(this, "attemptNextCallService, mIsAborted: %b", mIsAborted);
if (mIsAborted) {
return;
}
if (mCallServiceDescriptorIterator != null && mCallServiceDescriptorIterator.hasNext()) {
CallServiceDescriptor descriptor = mCallServiceDescriptorIterator.next();
final CallServiceWrapper callService =
mCallServicesById.get(descriptor.getCallServiceId());
if (callService == null || mAttemptedCallServices.contains(callService)) {
// The next call service is either null or has already been attempted, fast forward
// to the next.
attemptNextCallService();
} else {
mAttemptedCallServices.add(callService);
mCall.setCallService(callService);
// Increment the associated call count until we get a result. This prevents the call
// service from unbinding while we are using it.
callService.incrementAssociatedCallCount();
callService.call(mCall, new AsyncResultCallback<Boolean>() {
@Override
public void onResult(Boolean wasCallPlaced) {
if (wasCallPlaced) {
handleSuccessfulCallAttempt(callService);
} else {
handleFailedCallAttempt("call failed.");
}
// If successful, the call should not have it's own association to keep
// the call service bound.
callService.decrementAssociatedCallCount();
}
});
}
} else {
mCallServiceDescriptorIterator = null;
mCall.clearCallService();
attemptNextSelector();
}
}
private void sendResult(boolean wasCallPlaced) {
if (mResultCallback != null) {
mResultCallback.onResult(wasCallPlaced);
mResultCallback = null;
mHandler.removeMessages(MSG_EXPIRE);
} else {
Log.wtf(this, "Attempting to return outgoing result twice for call %s", mCall);
}
}
}