| /* |
| * 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()); |
| } |
| } |