blob: c33b17e474c1edfdb25a8f7ec4bca1572d3e0765 [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.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.telecomm.CallServiceDescriptor;
import android.telephony.DisconnectCause;
import android.telephony.PhoneNumberUtils;
import com.android.telecomm.BaseRepository.LookupCallback;
import com.google.android.collect.Sets;
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. Each of the connection
* 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.
*/
final class OutgoingCallProcessor {
/**
* 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, ConnectionServiceWrapper> mConnectionServicesById = Maps.newHashMap();
/**
* The set of attempted connection services, used to ensure services are attempted at most once
* per outgoing-call attempt.
*/
private final Set<ConnectionServiceWrapper> mAttemptedConnectionServices = Sets.newHashSet();
private final CallServiceRepository mCallServiceRepository;
/**
* 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;
private OutgoingCallResponse mResultCallback;
private boolean mIsAborted = false;
private int mLastErrorCode = 0;
private String mLastErrorMsg = null;
/**
* Persists the specified parameters and iterates through the prioritized list of call
* services. Stops once a matching connection service is found. Calls with no matching
* connection service will eventually be killed by the cleanup/monitor switchboard handler.
*
* @param call The call to place.
* @param callServiceRepository
* @param resultCallback The callback on which to return the result.
*/
OutgoingCallProcessor(
Call call,
CallServiceRepository callServiceRepository,
OutgoingCallResponse resultCallback) {
ThreadUtil.checkOnMainThread();
mCall = call;
mResultCallback = resultCallback;
mCallServiceRepository = callServiceRepository;
}
/**
* Initiates the attempt to place the call. No-op beyond the first invocation.
*/
void process() {
Log.v(this, "process, mIsAborted: %b", mIsAborted);
if (!mIsAborted) {
// Lookup connection services
mCallServiceRepository.lookupServices(new LookupCallback<ConnectionServiceWrapper>() {
@Override
public void onComplete(Collection<ConnectionServiceWrapper> services) {
setConnectionServices(services);
}
});
}
}
/**
* 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 connection service to place the
// call. If so, we need to tell it to abort.
// TODO(santoscordon): The conneciton service is saved with the call and so we have to
// query the call to get it, which is a bit backwards. Ideally, the connection service
// would be saved inside this class until the whole thing is complete and then set on
// the call.
ConnectionServiceWrapper service = mCall.getConnectionService();
if (service != null) {
service.abort(mCall);
}
// We consider a deliberate abort to be a "normal" disconnect, not
// requiring special reporting.
sendResult(false, DisconnectCause.LOCAL, null);
}
}
boolean isAborted() {
return mIsAborted;
}
/**
* Completes the outgoing call sequence by setting the connection service on the call object.
* This is invoked when the connection service adapter receives positive confirmation that the
* connection service placed the call.
*/
void handleSuccessfulCallAttempt(ConnectionServiceWrapper service) {
Log.v(this, "handleSuccessfulCallAttempt");
ThreadUtil.checkOnMainThread();
if (mIsAborted) {
service.abort(mCall);
return;
}
sendResult(true, DisconnectCause.NOT_DISCONNECTED, null);
}
/**
* Attempts the next connection service if the specified connection service is the one currently
* being attempted.
*
* @param errorCode The reason for the failure, one of {@link DisconnectCause}.
* @param errorMsg Optional text reason for the failure.
*/
void handleFailedCallAttempt(int errorCode, String errorMsg) {
Log.v(this, "handleFailedCallAttempt %s %s", DisconnectCause.toString(errorCode), errorMsg);
// Store latest error code and message. If this is our last available attempt at placing
// a call, these error details will be considered "the" cause of the failure.
mLastErrorCode = errorCode;
mLastErrorMsg = errorMsg;
if (!mIsAborted) {
ThreadUtil.checkOnMainThread();
attemptNextConnectionService();
}
}
/**
* Sets the connection services to attempt for this outgoing call.
*
* @param services The connection services.
*/
private void setConnectionServices(Collection<ConnectionServiceWrapper> services) {
mCallServiceDescriptors = new ArrayList<>();
// Populate the list and map of call-service descriptors.
for (ConnectionServiceWrapper service : services) {
CallServiceDescriptor descriptor = service.getDescriptor();
// TODO(sail): Remove once there's a way to pick the service.
if (descriptor.getServiceComponent().getPackageName().equals(
"com.google.android.talk")) {
Log.i(this, "Moving connection service %s to top of list", descriptor);
mCallServiceDescriptors.add(0, descriptor);
} else {
mCallServiceDescriptors.add(descriptor);
}
mConnectionServicesById.put(descriptor.getConnectionServiceId(), service);
}
adjustCallServiceDescriptorsForEmergency();
mCallServiceDescriptorIterator = mCallServiceDescriptors.iterator();
attemptNextConnectionService();
}
/**
* Attempts to place the call using the connection service specified by the next call-service
* descriptor of mCallServiceDescriptorIterator.
*/
private void attemptNextConnectionService() {
Log.v(this, "attemptNextConnectionService, mIsAborted: %b", mIsAborted);
if (mIsAborted) {
return;
}
if (mCallServiceDescriptorIterator != null && mCallServiceDescriptorIterator.hasNext()) {
CallServiceDescriptor descriptor = mCallServiceDescriptorIterator.next();
final ConnectionServiceWrapper service =
mConnectionServicesById.get(descriptor.getConnectionServiceId());
if (service == null || mAttemptedConnectionServices.contains(service)) {
// The next connection service is either null or has already been attempted, fast
// forward to the next.
attemptNextConnectionService();
} else {
mAttemptedConnectionServices.add(service);
mCall.setConnectionService(service);
// Increment the associated call count until we get a result. This prevents the call
// service from unbinding while we are using it.
service.incrementAssociatedCallCount();
Log.i(this, "Attempting to call from %s", service.getDescriptor());
service.call(mCall, new OutgoingCallResponse() {
@Override
public void onOutgoingCallSuccess() {
handleSuccessfulCallAttempt(service);
service.decrementAssociatedCallCount();
}
@Override
public void onOutgoingCallFailure(int code, String msg) {
handleFailedCallAttempt(code, msg);
service.decrementAssociatedCallCount();
}
@Override
public void onOutgoingCallCancel() {
abort();
service.decrementAssociatedCallCount();
}
});
}
} else {
Log.v(this, "attemptNextConnectionService, no more service descriptors, failing");
mCallServiceDescriptorIterator = null;
mCall.clearConnectionService();
sendResult(false, mLastErrorCode, mLastErrorMsg);
}
}
private void sendResult(boolean wasCallPlaced, int errorCode, String errorMsg) {
if (mResultCallback != null) {
if (mIsAborted) {
mResultCallback.onOutgoingCallCancel();
} else if (wasCallPlaced) {
mResultCallback.onOutgoingCallSuccess();
} else {
mResultCallback.onOutgoingCallFailure(errorCode, errorMsg);
}
mResultCallback = null;
} else {
Log.wtf(this, "Attempting to return outgoing result twice for call %s", mCall);
}
}
// If we are possibly attempting to call a local emergency number, ensure that the
// plain PSTN connection service, if it exists, is attempted first.
private void adjustCallServiceDescriptorsForEmergency() {
for (int i = 0; i < mCallServiceDescriptors.size(); i++) {
if (shouldProcessAsEmergency(mCall.getHandle())) {
if (TelephonyUtil.isPstnConnectionService(mCallServiceDescriptors.get(i))) {
mCallServiceDescriptors.add(0, mCallServiceDescriptors.remove(i));
return;
}
} else {
if (mCallServiceDescriptors.get(i).getServiceComponent().getPackageName().equals(
"com.android.telecomm.tests")) {
mCallServiceDescriptors.add(0, mCallServiceDescriptors.remove(i));
}
}
}
}
private boolean shouldProcessAsEmergency(Uri handle) {
return PhoneNumberUtils.isPotentialLocalEmergencyNumber(
TelecommApp.getInstance(), handle.getSchemeSpecificPart());
}
}