blob: 699a3e2cba4445e711dc94a41e882db9dcd6fdc2 [file] [log] [blame]
/*
* 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.telecomm;
import android.content.ContentUris;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract.Contacts;
import android.telecomm.CallInfo;
import android.telecomm.CallServiceDescriptor;
import android.telecomm.CallState;
import android.telecomm.GatewayInfo;
import android.telecomm.TelecommConstants;
import android.telephony.DisconnectCause;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.internal.telephony.CallerInfo;
import com.android.internal.telephony.CallerInfoAsyncQuery;
import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
import com.android.telecomm.ContactsAsyncHelper.OnImageLoadCompleteListener;
import com.google.android.collect.Sets;
import com.google.common.base.Preconditions;
import java.util.Locale;
import java.util.Set;
/**
* Encapsulates all aspects of a given phone call throughout its lifecycle, starting
* from the time the call intent was received by Telecomm (vs. the time the call was
* connected etc).
*/
final class Call {
/**
* Listener for events on the call.
*/
interface Listener {
void onSuccessfulOutgoingCall(Call call);
void onFailedOutgoingCall(Call call, boolean isAborted);
void onSuccessfulIncomingCall(Call call, CallInfo callInfo);
void onFailedIncomingCall(Call call);
}
private static final OnQueryCompleteListener sCallerInfoQueryListener =
new OnQueryCompleteListener() {
/** ${inheritDoc} */
@Override
public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
if (cookie != null) {
((Call) cookie).setCallerInfo(callerInfo, token);
}
}
};
private static final OnImageLoadCompleteListener sPhotoLoadListener =
new OnImageLoadCompleteListener() {
/** ${inheritDoc} */
@Override
public void onImageLoadComplete(
int token, Drawable photo, Bitmap photoIcon, Object cookie) {
if (cookie != null) {
((Call) cookie).setPhoto(photo, photoIcon, token);
}
}
};
/** True if this is an incoming call. */
private final boolean mIsIncoming;
/**
* The time this call was created, typically also the time this call was added to the set
* of pending outgoing calls (mPendingOutgoingCalls) that's maintained by the switchboard.
* Beyond logging and such, may also be used for bookkeeping and specifically for marking
* certain call attempts as failed attempts.
*/
private final long mCreationTimeMillis = System.currentTimeMillis();
/** The gateway information associated with this call. This stores the original call handle
* that the user is attempting to connect to via the gateway, the actual handle to dial in
* order to connect the call via the gateway, as well as the package name of the gateway
* service. */
private final GatewayInfo mGatewayInfo;
private long mConnectTimeMillis;
/** The state of the call. */
private CallState mState;
/** The handle with which to establish this call. */
private Uri mHandle;
/**
* The call service which is attempted or already connecting this call.
*/
private CallServiceWrapper mCallService;
/**
* The call-service selector for this call.
*/
private CallServiceSelectorWrapper mCallServiceSelector;
/**
* The set of call services that were attempted in the process of placing/switching this call
* but turned out unsuitable. Only used in the context of call switching.
*/
private Set<CallServiceWrapper> mIncompatibleCallServices;
private boolean mIsEmergencyCall;
/**
* Disconnect cause for the call. Only valid if the state of the call is DISCONNECTED.
* See {@link android.telephony.DisconnectCause}.
*/
private int mDisconnectCause = DisconnectCause.NOT_VALID;
/**
* Additional disconnect information provided by the call service.
*/
private String mDisconnectMessage;
/** Info used by the call services. */
private Bundle mExtras = Bundle.EMPTY;
/** The Uri to dial to perform the handoff. If this is null then handoff is not supported. */
private Uri mHandoffHandle;
/**
* References the call that is being handed off. This value is non-null for untracked calls
* that are being used to perform a handoff.
*/
private Call mOriginalCall;
/**
* The descriptor for the call service that this call is being switched to, null if handoff is
* not in progress.
*/
private CallServiceDescriptor mHandoffCallServiceDescriptor;
/** Set of listeners on this call. */
private Set<Listener> mListeners = Sets.newHashSet();
private OutgoingCallProcessor mOutgoingCallProcessor;
// TODO(santoscordon): The repositories should be changed into singleton types.
private CallServiceRepository mCallServiceRepository;
private CallServiceSelectorRepository mSelectorRepository;
/** Caller information retrieved from the latest contact query. */
private CallerInfo mCallerInfo;
/** The latest token used with a contact info query. */
private int mQueryToken = 0;
/**
* Creates an empty call object.
*
* @param isIncoming True if this is an incoming call.
*/
Call(boolean isIncoming) {
this(null, null, isIncoming);
}
/**
* Persists the specified parameters and initializes the new instance.
*
* @param handle The handle to dial.
* @param gatewayInfo Gateway information to use for the call.
* @param isIncoming True if this is an incoming call.
*/
Call(Uri handle, GatewayInfo gatewayInfo, boolean isIncoming) {
mState = CallState.NEW;
setHandle(handle);
mGatewayInfo = gatewayInfo;
mIsIncoming = isIncoming;
}
void addListener(Listener listener) {
mListeners.add(listener);
}
void removeListener(Listener listener) {
mListeners.remove(listener);
}
/** {@inheritDoc} */
@Override public String toString() {
String component = null;
if (mCallService != null && mCallService.getComponentName() != null) {
component = mCallService.getComponentName().flattenToShortString();
}
return String.format(Locale.US, "[%s, %s, %s]", mState, component, Log.piiHandle(mHandle));
}
CallState getState() {
return mState;
}
/**
* Sets the call state. Although there exists the notion of appropriate state transitions
* (see {@link CallState}), in practice those expectations break down when cellular systems
* misbehave and they do this very often. The result is that we do not enforce state transitions
* and instead keep the code resilient to unexpected state changes.
*/
void setState(CallState newState) {
Preconditions.checkState(newState != CallState.DISCONNECTED ||
mDisconnectCause != DisconnectCause.NOT_VALID);
if (mState != newState) {
Log.v(this, "setState %s -> %s", mState, newState);
mState = newState;
}
}
Uri getHandle() {
return mHandle;
}
void setHandle(Uri handle) {
if ((mHandle == null && handle != null) || (mHandle != null && !mHandle.equals(handle))) {
mHandle = handle;
mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
mHandle.getSchemeSpecificPart(), TelecommApp.getInstance());
startCallerInfoLookup();
}
}
String getName() {
return mCallerInfo == null ? null : mCallerInfo.name;
}
Bitmap getPhotoIcon() {
return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
}
Drawable getPhoto() {
return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
}
/**
* @param disconnectCause The reason for the disconnection, any of
* {@link android.telephony.DisconnectCause}.
* @param disconnectMessage Optional call-service-provided message about the disconnect.
*/
void setDisconnectCause(int disconnectCause, String disconnectMessage) {
// TODO: Consider combining this method with a setDisconnected() method that is totally
// separate from setState.
mDisconnectCause = disconnectCause;
mDisconnectMessage = disconnectMessage;
}
int getDisconnectCause() {
return mDisconnectCause;
}
String getDisconnectMessage() {
return mDisconnectMessage;
}
boolean isEmergencyCall() {
return mIsEmergencyCall;
}
/**
* @return The original handle this call is associated with. In-call services should use this
* handle when indicating in their UI the handle that is being called.
*/
public Uri getOriginalHandle() {
if (mGatewayInfo != null && !mGatewayInfo.isEmpty()) {
return mGatewayInfo.getOriginalHandle();
}
return getHandle();
}
GatewayInfo getGatewayInfo() {
return mGatewayInfo;
}
boolean isIncoming() {
return mIsIncoming;
}
/**
* @return The "age" of this call object in milliseconds, which typically also represents the
* period since this call was added to the set pending outgoing calls, see
* mCreationTimeMillis.
*/
long getAgeMillis() {
return System.currentTimeMillis() - mCreationTimeMillis;
}
/**
* @return The time when this call object was created and added to the set of pending outgoing
* calls.
*/
long getCreationTimeMillis() {
return mCreationTimeMillis;
}
long getConnectTimeMillis() {
return mConnectTimeMillis;
}
void setConnectTimeMillis(long connectTimeMillis) {
mConnectTimeMillis = connectTimeMillis;
}
CallServiceWrapper getCallService() {
return mCallService;
}
void setCallService(CallServiceWrapper callService) {
setCallService(callService, null);
}
/**
* Changes the call service this call is associated with. If callToReplace is non-null then this
* call takes its place within the call service.
*/
void setCallService(CallServiceWrapper callService, Call callToReplace) {
Preconditions.checkNotNull(callService);
clearCallService();
callService.incrementAssociatedCallCount();
mCallService = callService;
if (callToReplace == null) {
mCallService.addCall(this);
} else {
mCallService.replaceCall(this, callToReplace);
}
}
/**
* Clears the associated call service.
*/
void clearCallService() {
if (mCallService != null) {
CallServiceWrapper callServiceTemp = mCallService;
mCallService = null;
callServiceTemp.removeCall(this);
// Decrementing the count can cause the service to unbind, which itself can trigger the
// service-death code. Since the service death code tries to clean up any associated
// calls, we need to make sure to remove that information (e.g., removeCall()) before
// we decrement. Technically, invoking removeCall() prior to decrementing is all that is
// necessary, but cleaning up mCallService prior to triggering an unbind is good to do.
// If you change this, make sure to update {@link clearCallServiceSelector} as well.
decrementAssociatedCallCount(callServiceTemp);
}
}
CallServiceSelectorWrapper getCallServiceSelector() {
return mCallServiceSelector;
}
void setCallServiceSelector(CallServiceSelectorWrapper selector) {
Preconditions.checkNotNull(selector);
clearCallServiceSelector();
selector.incrementAssociatedCallCount();
mCallServiceSelector = selector;
mCallServiceSelector.addCall(this);
}
void clearCallServiceSelector() {
if (mCallServiceSelector != null) {
CallServiceSelectorWrapper selectorTemp = mCallServiceSelector;
mCallServiceSelector = null;
selectorTemp.removeCall(this);
// See comment on {@link #clearCallService}.
decrementAssociatedCallCount(selectorTemp);
}
}
/**
* Starts the incoming call flow through the switchboard. When switchboard completes, it will
* invoke handle[Un]SuccessfulIncomingCall.
*
* @param descriptor The relevant call-service descriptor.
* @param extras The optional extras passed via
* {@link TelecommConstants#EXTRA_INCOMING_CALL_EXTRAS}.
*/
void startIncoming(CallServiceDescriptor descriptor, Bundle extras) {
Switchboard.getInstance().retrieveIncomingCall(this, descriptor, extras);
}
void handleSuccessfulIncoming(CallInfo callInfo) {
Preconditions.checkState(callInfo.getState() == CallState.RINGING);
setHandle(callInfo.getHandle());
// TODO(santoscordon): Make this class (not CallsManager) responsible for changing the call
// state to RINGING.
// TODO(santoscordon): Replace this with state transitions related to "connecting".
for (Listener l : mListeners) {
l.onSuccessfulIncomingCall(this, callInfo);
}
}
void handleFailedIncoming() {
clearCallService();
// TODO: Needs more specific disconnect error for this case.
setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED, null);
setState(CallState.DISCONNECTED);
// TODO(santoscordon): Replace this with state transitions related to "connecting".
for (Listener l : mListeners) {
l.onFailedIncomingCall(this);
}
}
/**
* Starts the outgoing call sequence. Upon completion, there should exist an active connection
* through a call service (or the call will have failed).
*/
void startOutgoing() {
Preconditions.checkState(mOutgoingCallProcessor == null);
mOutgoingCallProcessor = new OutgoingCallProcessor(
this,
Switchboard.getInstance().getCallServiceRepository(),
Switchboard.getInstance().getSelectorRepository(),
new AsyncResultCallback<Boolean>() {
@Override
public void onResult(Boolean wasCallPlaced) {
if (wasCallPlaced) {
handleSuccessfulOutgoing();
} else {
handleFailedOutgoing(mOutgoingCallProcessor.isAborted());
}
mOutgoingCallProcessor = null;
}
});
mOutgoingCallProcessor.process();
}
void handleSuccessfulOutgoing() {
// TODO(santoscordon): Replace this with state transitions related to "connecting".
for (Listener l : mListeners) {
l.onSuccessfulOutgoingCall(this);
}
}
void handleFailedOutgoing(boolean isAborted) {
// TODO(santoscordon): Replace this with state transitions related to "connecting".
for (Listener l : mListeners) {
l.onFailedOutgoingCall(this, isAborted);
}
clearCallService();
clearCallServiceSelector();
}
/**
* Adds the specified call service to the list of incompatible services. The set is used when
* attempting to switch a phone call between call services such that incompatible services can
* be avoided.
*
* @param callService The incompatible call service.
*/
void addIncompatibleCallService(CallServiceWrapper callService) {
if (mIncompatibleCallServices == null) {
mIncompatibleCallServices = Sets.newHashSet();
}
mIncompatibleCallServices.add(callService);
}
/**
* Checks whether or not the specified callService was identified as incompatible in the
* context of this call.
*
* @param callService The call service to evaluate.
* @return True upon incompatible call services and false otherwise.
*/
boolean isIncompatibleCallService(CallServiceWrapper callService) {
return mIncompatibleCallServices != null &&
mIncompatibleCallServices.contains(callService);
}
/**
* Plays the specified DTMF tone.
*/
void playDtmfTone(char digit) {
if (mCallService == null) {
Log.w(this, "playDtmfTone() request on a call without a call service.");
} else {
Log.i(this, "Send playDtmfTone to call service for call %s", this);
mCallService.playDtmfTone(this, digit);
}
}
/**
* Stops playing any currently playing DTMF tone.
*/
void stopDtmfTone() {
if (mCallService == null) {
Log.w(this, "stopDtmfTone() request on a call without a call service.");
} else {
Log.i(this, "Send stopDtmfTone to call service for call %s", this);
mCallService.stopDtmfTone(this);
}
}
/**
* Attempts to disconnect the call through the call service.
*/
void disconnect() {
if (mState == CallState.NEW) {
Log.v(this, "Aborting call %s", this);
abort();
} else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
Preconditions.checkNotNull(mCallService);
Log.i(this, "Send disconnect to call service for call: %s", this);
// The call isn't officially disconnected until the call service confirms that the call
// was actually disconnected. Only then is the association between call and call service
// severed, see {@link CallsManager#markCallAsDisconnected}.
mCallService.disconnect(this);
}
}
void abort() {
if (mOutgoingCallProcessor != null) {
mOutgoingCallProcessor.abort();
}
}
/**
* Answers the call if it is ringing.
*/
void answer() {
Preconditions.checkNotNull(mCallService);
// Check to verify that the call is still in the ringing state. A call can change states
// between the time the user hits 'answer' and Telecomm receives the command.
if (isRinging("answer")) {
// At this point, we are asking the call service to answer but we don't assume that
// it will work. Instead, we wait until confirmation from the call service that the
// call is in a non-RINGING state before changing the UI. See
// {@link CallServiceAdapter#setActive} and other set* methods.
mCallService.answer(this);
}
}
/**
* Rejects the call if it is ringing.
*/
void reject() {
Preconditions.checkNotNull(mCallService);
// Check to verify that the call is still in the ringing state. A call can change states
// between the time the user hits 'reject' and Telecomm receives the command.
if (isRinging("reject")) {
mCallService.reject(this);
}
}
/**
* Puts the call on hold if it is currently active.
*/
void hold() {
Preconditions.checkNotNull(mCallService);
if (mState == CallState.ACTIVE) {
mCallService.hold(this);
}
}
/**
* Releases the call from hold if it is currently active.
*/
void unhold() {
Preconditions.checkNotNull(mCallService);
if (mState == CallState.ON_HOLD) {
mCallService.unhold(this);
}
}
/**
* @return An object containing read-only information about this call.
*/
CallInfo toCallInfo(String callId) {
CallServiceDescriptor descriptor = null;
if (mCallService != null) {
descriptor = mCallService.getDescriptor();
} else if (mOriginalCall != null && mOriginalCall.mCallService != null) {
descriptor = mOriginalCall.mCallService.getDescriptor();
}
return new CallInfo(callId, mState, mHandle, mGatewayInfo, mExtras, descriptor);
}
/** Checks if this is a live call or not. */
boolean isAlive() {
switch (mState) {
case NEW:
case RINGING:
case DISCONNECTED:
case ABORTED:
return false;
default:
return true;
}
}
boolean isActive() {
switch (mState) {
case ACTIVE:
case POST_DIAL:
case POST_DIAL_WAIT:
return true;
default:
return false;
}
}
Bundle getExtras() {
return mExtras;
}
void setExtras(Bundle extras) {
mExtras = extras;
}
Uri getHandoffHandle() {
return mHandoffHandle;
}
void setHandoffHandle(Uri handoffHandle) {
mHandoffHandle = handoffHandle;
}
Call getOriginalCall() {
return mOriginalCall;
}
void setOriginalCall(Call originalCall) {
mOriginalCall = originalCall;
}
CallServiceDescriptor getHandoffCallServiceDescriptor() {
return mHandoffCallServiceDescriptor;
}
void setHandoffCallServiceDescriptor(CallServiceDescriptor descriptor) {
mHandoffCallServiceDescriptor = descriptor;
}
/**
* @return True if the call is ringing, else logs the action name.
*/
private boolean isRinging(String actionName) {
if (mState == CallState.RINGING) {
return true;
}
Log.i(this, "Request to %s a non-ringing call %s", actionName, this);
return false;
}
@SuppressWarnings("rawtypes")
private void decrementAssociatedCallCount(ServiceBinder binder) {
if (binder != null) {
binder.decrementAssociatedCallCount();
}
}
/**
* Looks up contact information based on the current handle.
*/
private void startCallerInfoLookup() {
String number = mHandle == null ? null : mHandle.getSchemeSpecificPart();
mQueryToken++; // Updated so that previous queries can no longer set the information.
mCallerInfo = null;
if (!TextUtils.isEmpty(number)) {
CallerInfoAsyncQuery.startQuery(
mQueryToken,
TelecommApp.getInstance(),
number,
sCallerInfoQueryListener,
this);
}
}
/**
* Saves the specified caller info if the specified token matches that of the last query
* that was made.
*
* @param callerInfo The new caller information to set.
* @param token The token used with this query.
*/
private void setCallerInfo(CallerInfo callerInfo, int token) {
Preconditions.checkNotNull(callerInfo);
if (mQueryToken == token) {
mCallerInfo = callerInfo;
if (mCallerInfo.person_id != 0) {
Uri personUri =
ContentUris.withAppendedId(Contacts.CONTENT_URI, mCallerInfo.person_id);
Log.d(this, "Searching person uri %s for call %s", personUri, this);
ContactsAsyncHelper.startObtainPhotoAsync(
token,
TelecommApp.getInstance(),
personUri,
sPhotoLoadListener,
this);
}
}
}
/**
* Saves the specified photo information if the specified token matches that of the last query.
*
* @param photo The photo as a drawable.
* @param photoIcon The photo as a small icon.
* @param token The token used with this query.
*/
private void setPhoto(Drawable photo, Bitmap photoIcon, int token) {
if (mQueryToken == token) {
mCallerInfo.cachedPhoto = photo;
mCallerInfo.cachedPhotoIcon = photoIcon;
}
}
}