blob: e4e57227898916c107d48210376520405f505a8e [file] [log] [blame]
/*
* Copyright (C) 2013 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.server.telecom;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.pm.UserInfo;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.SystemVibrator;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BlockedNumberContract.SystemContract;
import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.telecom.CallAudioState;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.Logging.Runnable;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.AsyncEmergencyContactNotifier;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.TelephonyProperties;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.AsyncBlockCheckFilter;
import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
import com.android.server.telecom.callfiltering.CallFilterResultCallback;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.callfiltering.CallScreeningServiceFilter;
import com.android.server.telecom.callfiltering.DirectToVoicemailCallFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilter;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.settings.BlockedNumbersUtil;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
import com.android.server.telecom.ui.IncomingCallNotifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* Singleton.
*
* NOTE: by design most APIs are package private, use the relevant adapter/s to allow
* access from other packages specifically refraining from passing the CallsManager instance
* beyond the com.android.server.telecom package boundary.
*/
@VisibleForTesting
public class CallsManager extends Call.ListenerBase
implements VideoProviderProxy.Listener, CallFilterResultCallback, CurrentUserProxy {
// TODO: Consider renaming this CallsManagerPlugin.
@VisibleForTesting
public interface CallsManagerListener {
void onCallAdded(Call call);
void onCallRemoved(Call call);
void onCallStateChanged(Call call, int oldState, int newState);
void onConnectionServiceChanged(
Call call,
ConnectionServiceWrapper oldService,
ConnectionServiceWrapper newService);
void onIncomingCallAnswered(Call call);
void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
void onRingbackRequested(Call call, boolean ringback);
void onIsConferencedChanged(Call call);
void onIsVoipAudioModeChanged(Call call);
void onVideoStateChanged(Call call, int previousVideoState, int newVideoState);
void onCanAddCallChanged(boolean canAddCall);
void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
void onHoldToneRequested(Call call);
void onExternalCallChanged(Call call, boolean isExternalCall);
void onDisconnectedTonePlaying(boolean isTonePlaying);
}
/** Interface used to define the action which is executed delay under some condition. */
interface PendingAction {
void performAction();
}
private static final String TAG = "CallsManager";
/**
* Call filter specifier used with
* {@link #getNumCallsWithState(int, Call, PhoneAccountHandle, int...)} to indicate only
* self-managed calls should be included.
*/
private static final int CALL_FILTER_SELF_MANAGED = 1;
/**
* Call filter specifier used with
* {@link #getNumCallsWithState(int, Call, PhoneAccountHandle, int...)} to indicate only
* managed calls should be included.
*/
private static final int CALL_FILTER_MANAGED = 2;
/**
* Call filter specifier used with
* {@link #getNumCallsWithState(int, Call, PhoneAccountHandle, int...)} to indicate both managed
* and self-managed calls should be included.
*/
private static final int CALL_FILTER_ALL = 3;
private static final String PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION =
"android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION";
private static final int HANDLER_WAIT_TIMEOUT = 10000;
private static final int MAXIMUM_LIVE_CALLS = 1;
private static final int MAXIMUM_HOLD_CALLS = 1;
private static final int MAXIMUM_RINGING_CALLS = 1;
private static final int MAXIMUM_DIALING_CALLS = 1;
private static final int MAXIMUM_OUTGOING_CALLS = 1;
private static final int MAXIMUM_TOP_LEVEL_CALLS = 2;
private static final int MAXIMUM_SELF_MANAGED_CALLS = 10;
private static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.PULLING};
/**
* These states are used by {@link #makeRoomForOutgoingCall(Call, boolean)} to determine which
* call should be ended first to make room for a new outgoing call.
*/
private static final int[] LIVE_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.PULLING, CallState.ACTIVE};
/**
* These states determine which calls will cause {@link TelecomManager#isInCall()} or
* {@link TelecomManager#isInManagedCall()} to return true.
*
* See also {@link PhoneStateBroadcaster}, which considers a similar set of states as being
* off-hook.
*/
public static final int[] ONGOING_CALL_STATES =
{CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING, CallState.PULLING, CallState.ACTIVE,
CallState.ON_HOLD, CallState.RINGING};
private static final int[] ANY_CALL_STATE =
{CallState.NEW, CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.RINGING, CallState.ACTIVE, CallState.ON_HOLD, CallState.DISCONNECTED,
CallState.ABORTED, CallState.DISCONNECTING, CallState.PULLING};
public static final String TELECOM_CALL_ID_PREFIX = "TC@";
// Maps call technologies in PhoneConstants to those in Analytics.
private static final Map<Integer, Integer> sAnalyticsTechnologyMap;
static {
sAnalyticsTechnologyMap = new HashMap<>(5);
sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_CDMA, Analytics.CDMA_PHONE);
sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_GSM, Analytics.GSM_PHONE);
sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_IMS, Analytics.IMS_PHONE);
sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_SIP, Analytics.SIP_PHONE);
sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_THIRD_PARTY,
Analytics.THIRD_PARTY_PHONE);
}
/**
* The main call repository. Keeps an instance of all live calls. New incoming and outgoing
* calls are added to the map and removed when the calls move to the disconnected state.
*
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
* load factor before resizing, 1 means we only expect a single thread to
* access the map so make only a single shard
*/
private final Set<Call> mCalls = Collections.newSetFromMap(
new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
/**
* A pending call is one which requires user-intervention in order to be placed.
* Used by {@link #startCallConfirmation(Call)}.
*/
private Call mPendingCall;
/**
* The current telecom call ID. Used when creating new instances of {@link Call}. Should
* only be accessed using the {@link #getNextCallId()} method which synchronizes on the
* {@link #mLock} sync root.
*/
private int mCallId = 0;
private int mRttRequestId = 0;
/**
* Stores the current foreground user.
*/
private UserHandle mCurrentUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
private final ConnectionServiceRepository mConnectionServiceRepository;
private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
private final InCallController mInCallController;
private final CallAudioManager mCallAudioManager;
private final CallRecordingTonePlayer mCallRecordingTonePlayer;
private RespondViaSmsManager mRespondViaSmsManager;
private final Ringer mRinger;
private final InCallWakeLockController mInCallWakeLockController;
// For this set initial table size to 16 because we add 13 listeners in
// the CallsManager constructor.
private final Set<CallsManagerListener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
private final HeadsetMediaButton mHeadsetMediaButton;
private final WiredHeadsetManager mWiredHeadsetManager;
private final BluetoothRouteManager mBluetoothRouteManager;
private final DockManager mDockManager;
private final TtyManager mTtyManager;
private final ProximitySensorManager mProximitySensorManager;
private final PhoneStateBroadcaster mPhoneStateBroadcaster;
private final CallLogManager mCallLogManager;
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final ContactsAsyncHelper mContactsAsyncHelper;
private final CallerInfoAsyncQueryFactory mCallerInfoAsyncQueryFactory;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final MissedCallNotifier mMissedCallNotifier;
private IncomingCallNotifier mIncomingCallNotifier;
private final CallerInfoLookupHelper mCallerInfoLookupHelper;
private final DefaultDialerCache mDefaultDialerCache;
private final Timeouts.Adapter mTimeoutsAdapter;
private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
private final ClockProxy mClockProxy;
private final Set<Call> mLocallyDisconnectingCalls = new HashSet<>();
private final Set<Call> mPendingCallsToDisconnect = new HashSet<>();
private final ConnectionServiceFocusManager mConnectionSvrFocusMgr;
/* Handler tied to thread in which CallManager was initialized. */
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final EmergencyCallHelper mEmergencyCallHelper;
private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@Override
public void releaseConnectionService(
ConnectionServiceFocusManager.ConnectionServiceFocus connectionService) {
mCalls.stream()
.filter(c -> c.getConnectionServiceWrapper().equals(connectionService))
.forEach(c -> c.disconnect("release " +
connectionService.getComponentName().getPackageName()));
}
@Override
public void setCallsManagerListener(CallsManagerListener listener) {
mListeners.add(listener);
}
};
private boolean mCanAddCall = true;
private TelephonyManager.MultiSimVariants mRadioSimVariants = null;
private Runnable mStopTone;
/**
* Listener to PhoneAccountRegistrar events.
*/
private PhoneAccountRegistrar.Listener mPhoneAccountListener =
new PhoneAccountRegistrar.Listener() {
public void onPhoneAccountRegistered(PhoneAccountRegistrar registrar,
PhoneAccountHandle handle) {
broadcastRegisterIntent(handle);
}
public void onPhoneAccountUnRegistered(PhoneAccountRegistrar registrar,
PhoneAccountHandle handle) {
broadcastUnregisterIntent(handle);
}
};
/**
* Receiver for enhanced call blocking feature to update the emergency call notification
* in below cases:
* 1) Carrier config changed.
* 2) Blocking suppression state changed.
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
|| SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
BlockedNumbersUtil.updateEmergencyCallNotification(context,
SystemContract.shouldShowEmergencyCallNotification(context));
}
}
};
/**
* Initializes the required Telecom components.
*/
@VisibleForTesting
public CallsManager(
Context context,
TelecomSystem.SyncRoot lock,
ContactsAsyncHelper contactsAsyncHelper,
CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
MissedCallNotifier missedCallNotifier,
PhoneAccountRegistrar phoneAccountRegistrar,
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory
connectionServiceFocusManagerFactory,
CallAudioManager.AudioServiceFactory audioServiceFactory,
BluetoothRouteManager bluetoothManager,
WiredHeadsetManager wiredHeadsetManager,
SystemStateProvider systemStateProvider,
DefaultDialerCache defaultDialerCache,
Timeouts.Adapter timeoutsAdapter,
AsyncRingtonePlayer asyncRingtonePlayer,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
EmergencyCallHelper emergencyCallHelper,
InCallTonePlayer.ToneGeneratorFactory toneGeneratorFactory,
ClockProxy clockProxy,
BluetoothStateReceiver bluetoothStateReceiver,
InCallControllerFactory inCallControllerFactory) {
mContext = context;
mLock = lock;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
mContactsAsyncHelper = contactsAsyncHelper;
mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mPhoneAccountRegistrar.addListener(mPhoneAccountListener);
mMissedCallNotifier = missedCallNotifier;
StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
mWiredHeadsetManager = wiredHeadsetManager;
mDefaultDialerCache = defaultDialerCache;
mBluetoothRouteManager = bluetoothManager;
mDockManager = new DockManager(context);
mTimeoutsAdapter = timeoutsAdapter;
mEmergencyCallHelper = emergencyCallHelper;
mCallerInfoLookupHelper = new CallerInfoLookupHelper(context, mCallerInfoAsyncQueryFactory,
mContactsAsyncHelper, mLock);
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
context,
this,
bluetoothManager,
wiredHeadsetManager,
statusBarNotifier,
audioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT
);
callAudioRouteStateMachine.initialize();
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
callAudioRouteStateMachine,
bluetoothManager,
wiredHeadsetManager,
mDockManager);
InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(
callAudioRoutePeripheralAdapter, lock, toneGeneratorFactory);
SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
SystemVibrator systemVibrator = new SystemVibrator(context);
mInCallController = inCallControllerFactory.create(context, mLock, this,
systemStateProvider, defaultDialerCache, mTimeoutsAdapter,
emergencyCallHelper);
mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
ringtoneFactory, systemVibrator, mInCallController);
mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext,
(AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE), mLock);
mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
this,new CallAudioModeStateMachine((AudioManager)
mContext.getSystemService(Context.AUDIO_SERVICE)),
playerFactory, mRinger, new RingbackPlayer(playerFactory),
bluetoothStateReceiver, mDtmfLocalTonePlayer);
mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(
mRequester, Looper.getMainLooper());
mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
mTtyManager = new TtyManager(context, mWiredHeadsetManager);
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier);
mConnectionServiceRepository =
new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
mClockProxy = clockProxy;
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mInCallController);
mListeners.add(mCallAudioManager);
mListeners.add(mCallRecordingTonePlayer);
mListeners.add(missedCallNotifier);
mListeners.add(mHeadsetMediaButton);
mListeners.add(mProximitySensorManager);
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
final UserManager userManager = UserManager.get(mContext);
// Don't load missed call if it is run in split user model.
if (userManager.isPrimaryUser()) {
onUserSwitch(Process.myUserHandle());
}
// Register BroadcastReceiver to handle enhanced call blocking feature related event.
IntentFilter intentFilter = new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intentFilter.addAction(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter);
}
public void setIncomingCallNotifier(IncomingCallNotifier incomingCallNotifier) {
if (mIncomingCallNotifier != null) {
mListeners.remove(mIncomingCallNotifier);
}
mIncomingCallNotifier = incomingCallNotifier;
mListeners.add(mIncomingCallNotifier);
}
public void setRespondViaSmsManager(RespondViaSmsManager respondViaSmsManager) {
if (mRespondViaSmsManager != null) {
mListeners.remove(mRespondViaSmsManager);
}
mRespondViaSmsManager = respondViaSmsManager;
mListeners.add(respondViaSmsManager);
}
public RespondViaSmsManager getRespondViaSmsManager() {
return mRespondViaSmsManager;
}
public CallerInfoLookupHelper getCallerInfoLookupHelper() {
return mCallerInfoLookupHelper;
}
@Override
public void onSuccessfulOutgoingCall(Call call, int callState) {
Log.v(this, "onSuccessfulOutgoingCall, %s", call);
setCallState(call, callState, "successful outgoing call");
if (!mCalls.contains(call)) {
// Call was not added previously in startOutgoingCall due to it being a potential MMI
// code, so add it now.
addCall(call);
}
// The call's ConnectionService has been updated.
for (CallsManagerListener listener : mListeners) {
listener.onConnectionServiceChanged(call, null, call.getConnectionService());
}
markCallAsDialing(call);
}
@Override
public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {
Log.v(this, "onFailedOutgoingCall, call: %s", call);
markCallAsRemoved(call);
}
@Override
public void onSuccessfulIncomingCall(Call incomingCall) {
Log.d(this, "onSuccessfulIncomingCall");
if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
Log.i(this, "Skipping call filtering due to ECBM");
onCallFilteringComplete(incomingCall, new CallFilteringResult(true, false, true, true));
return;
}
List<IncomingCallFilter.CallFilter> filters = new ArrayList<>();
filters.add(new DirectToVoicemailCallFilter(mCallerInfoLookupHelper));
filters.add(new AsyncBlockCheckFilter(mContext, new BlockCheckerAdapter(),
mCallerInfoLookupHelper));
filters.add(new CallScreeningServiceFilter(mContext, this, mPhoneAccountRegistrar,
mDefaultDialerCache, new ParcelableCallUtils.Converter(), mLock));
new IncomingCallFilter(mContext, this, incomingCall, mLock,
mTimeoutsAdapter, filters).performFiltering();
}
@Override
public void onCallFilteringComplete(Call incomingCall, CallFilteringResult result) {
// Only set the incoming call as ringing if it isn't already disconnected. It is possible
// that the connection service disconnected the call before it was even added to Telecom, in
// which case it makes no sense to set it back to a ringing state.
if (incomingCall.getState() != CallState.DISCONNECTED &&
incomingCall.getState() != CallState.DISCONNECTING) {
setCallState(incomingCall, CallState.RINGING,
result.shouldAllowCall ? "successful incoming call" : "blocking call");
} else {
Log.i(this, "onCallFilteringCompleted: call already disconnected.");
return;
}
if (result.shouldAllowCall) {
if (hasMaximumManagedRingingCalls(incomingCall)) {
if (shouldSilenceInsteadOfReject(incomingCall)) {
incomingCall.silence();
} else {
Log.i(this, "onCallFilteringCompleted: Call rejected! " +
"Exceeds maximum number of ringing calls.");
rejectCallAndLog(incomingCall);
}
} else if (hasMaximumManagedDialingCalls(incomingCall)) {
Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
"dialing calls.");
rejectCallAndLog(incomingCall);
} else {
addCall(incomingCall);
}
} else {
if (result.shouldReject) {
Log.i(this, "onCallFilteringCompleted: blocked call, rejecting.");
incomingCall.reject(false, null);
}
if (result.shouldAddToCallLog) {
Log.i(this, "onCallScreeningCompleted: blocked call, adding to call log.");
if (result.shouldShowNotification) {
Log.w(this, "onCallScreeningCompleted: blocked call, showing notification.");
}
mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE,
result.shouldShowNotification);
} else if (result.shouldShowNotification) {
Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
mMissedCallNotifier.showMissedCallNotification(
new MissedCallNotifier.CallInfo(incomingCall));
}
}
}
/**
* Whether allow (silence rather than reject) the incoming call if it has a different source
* (connection service) from the existing ringing call when reaching maximum ringing calls.
*/
private boolean shouldSilenceInsteadOfReject(Call incomingCall) {
if (!mContext.getResources().getBoolean(
R.bool.silence_incoming_when_different_service_and_maximum_ringing)) {
return false;
}
Call ringingCall = null;
for (Call call : mCalls) {
// Only operate on top-level calls
if (call.getParentCall() != null) {
continue;
}
if (call.isExternalCall()) {
continue;
}
if (CallState.RINGING == call.getState() &&
call.getConnectionService() == incomingCall.getConnectionService()) {
return false;
}
}
return true;
}
@Override
public void onFailedIncomingCall(Call call) {
setCallState(call, CallState.DISCONNECTED, "failed incoming call");
call.removeListener(this);
}
@Override
public void onSuccessfulUnknownCall(Call call, int callState) {
setCallState(call, callState, "successful unknown call");
Log.i(this, "onSuccessfulUnknownCall for call %s", call);
addCall(call);
}
@Override
public void onFailedUnknownCall(Call call) {
Log.i(this, "onFailedUnknownCall for call %s", call);
setCallState(call, CallState.DISCONNECTED, "failed unknown call");
call.removeListener(this);
}
@Override
public void onRingbackRequested(Call call, boolean ringback) {
for (CallsManagerListener listener : mListeners) {
listener.onRingbackRequested(call, ringback);
}
}
@Override
public void onPostDialWait(Call call, String remaining) {
mInCallController.onPostDialWait(call, remaining);
}
@Override
public void onPostDialChar(final Call call, char nextChar) {
if (PhoneNumberUtils.is12Key(nextChar)) {
// Play tone if it is one of the dialpad digits, canceling out the previously queued
// up stopTone runnable since playing a new tone automatically stops the previous tone.
if (mStopTone != null) {
mHandler.removeCallbacks(mStopTone.getRunnableToCancel());
mStopTone.cancel();
}
mDtmfLocalTonePlayer.playTone(call, nextChar);
mStopTone = new Runnable("CM.oPDC", mLock) {
@Override
public void loggedRun() {
// Set a timeout to stop the tone in case there isn't another tone to
// follow.
mDtmfLocalTonePlayer.stopTone(call);
}
};
mHandler.postDelayed(mStopTone.prepare(),
Timeouts.getDelayBetweenDtmfTonesMillis(mContext.getContentResolver()));
} else if (nextChar == 0 || nextChar == TelecomManager.DTMF_CHARACTER_WAIT ||
nextChar == TelecomManager.DTMF_CHARACTER_PAUSE) {
// Stop the tone if a tone is playing, removing any other stopTone callbacks since
// the previous tone is being stopped anyway.
if (mStopTone != null) {
mHandler.removeCallbacks(mStopTone.getRunnableToCancel());
mStopTone.cancel();
}
mDtmfLocalTonePlayer.stopTone(call);
} else {
Log.w(this, "onPostDialChar: invalid value %d", nextChar);
}
}
@Override
public void onParentChanged(Call call) {
// parent-child relationship affects which call should be foreground, so do an update.
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
listener.onIsConferencedChanged(call);
}
}
@Override
public void onChildrenChanged(Call call) {
// parent-child relationship affects which call should be foreground, so do an update.
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
listener.onIsConferencedChanged(call);
}
}
@Override
public void onIsVoipAudioModeChanged(Call call) {
for (CallsManagerListener listener : mListeners) {
listener.onIsVoipAudioModeChanged(call);
}
}
@Override
public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
for (CallsManagerListener listener : mListeners) {
listener.onVideoStateChanged(call, previousVideoState, newVideoState);
}
}
@Override
public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call,
long disconnectionTimeout) {
mPendingCallsToDisconnect.add(call);
mHandler.postDelayed(new Runnable("CM.oCVNOCB", mLock) {
@Override
public void loggedRun() {
if (mPendingCallsToDisconnect.remove(call)) {
Log.i(this, "Delayed disconnection of call: %s", call);
call.disconnect();
}
}
}.prepare(), disconnectionTimeout);
return true;
}
/**
* Handles changes to the {@link Connection.VideoProvider} for a call. Adds the
* {@link CallsManager} as a listener for the {@link VideoProviderProxy} which is created
* in {@link Call#setVideoProvider(IVideoProvider)}. This allows the {@link CallsManager} to
* respond to callbacks from the {@link VideoProviderProxy}.
*
* @param call The call.
*/
@Override
public void onVideoCallProviderChanged(Call call) {
VideoProviderProxy videoProviderProxy = call.getVideoProviderProxy();
if (videoProviderProxy == null) {
return;
}
videoProviderProxy.addListener(this);
}
/**
* Handles session modification requests received via the {@link TelecomVideoCallCallback} for
* a call. Notifies listeners of the {@link CallsManager.CallsManagerListener} of the session
* modification request.
*
* @param call The call.
* @param videoProfile The {@link VideoProfile}.
*/
@Override
public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) {
int videoState = videoProfile != null ? videoProfile.getVideoState() :
VideoProfile.STATE_AUDIO_ONLY;
Log.v(TAG, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile
.videoStateToString(videoState));
for (CallsManagerListener listener : mListeners) {
listener.onSessionModifyRequestReceived(call, videoProfile);
}
}
public Collection<Call> getCalls() {
return Collections.unmodifiableCollection(mCalls);
}
/**
* Play or stop a call hold tone for a call. Triggered via
* {@link Connection#sendConnectionEvent(String)} when the
* {@link Connection#EVENT_ON_HOLD_TONE_START} event or
* {@link Connection#EVENT_ON_HOLD_TONE_STOP} event is passed through to the
*
* @param call The call which requested the hold tone.
*/
@Override
public void onHoldToneRequested(Call call) {
for (CallsManagerListener listener : mListeners) {
listener.onHoldToneRequested(call);
}
}
/**
* A {@link Call} managed by the {@link CallsManager} has requested a handover to another
* {@link PhoneAccount}.
* @param call The call.
* @param handoverTo The {@link PhoneAccountHandle} to handover the call to.
* @param videoState The desired video state of the call after handover.
* @param extras
*/
@Override
public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
Bundle extras, boolean isLegacy) {
if (isLegacy) {
requestHandoverViaEvents(call, handoverTo, videoState, extras);
} else {
requestHandover(call, handoverTo, videoState, extras);
}
}
@VisibleForTesting
public Call getForegroundCall() {
if (mCallAudioManager == null) {
// Happens when getForegroundCall is called before full initialization.
return null;
}
return mCallAudioManager.getForegroundCall();
}
@Override
public UserHandle getCurrentUserHandle() {
return mCurrentUserHandle;
}
public CallAudioManager getCallAudioManager() {
return mCallAudioManager;
}
InCallController getInCallController() {
return mInCallController;
}
EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
@VisibleForTesting
public boolean hasEmergencyCall() {
for (Call call : mCalls) {
if (call.isEmergencyCall()) {
return true;
}
}
return false;
}
public boolean hasEmergencyRttCall() {
for (Call call : mCalls) {
if (call.isEmergencyCall() && call.isRttCall()) {
return true;
}
}
return false;
}
@VisibleForTesting
public boolean hasOnlyDisconnectedCalls() {
if (mCalls.size() == 0) {
return false;
}
for (Call call : mCalls) {
if (!call.isDisconnected()) {
return false;
}
}
return true;
}
public boolean hasVideoCall() {
for (Call call : mCalls) {
if (VideoProfile.isVideo(call.getVideoState())) {
return true;
}
}
return false;
}
@VisibleForTesting
public CallAudioState getAudioState() {
return mCallAudioManager.getCallAudioState();
}
boolean isTtySupported() {
return mTtyManager.isTtySupported();
}
int getCurrentTtyMode() {
return mTtyManager.getCurrentTtyMode();
}
@VisibleForTesting
public void addListener(CallsManagerListener listener) {
mListeners.add(listener);
}
void removeListener(CallsManagerListener listener) {
mListeners.remove(listener);
}
/**
* Starts the process to attach the call to a connection service.
*
* @param phoneAccountHandle The phone account which contains the component name of the
* connection service to use for this call.
* @param extras The optional extras Bundle passed with the intent used for the incoming call.
*/
void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
Log.d(this, "processIncomingCallIntent");
boolean isHandover = extras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER);
Uri handle = extras.getParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
if (handle == null) {
// Required for backwards compatibility
handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
Call call = new Call(
getNextCallId(),
mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
handle,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
Call.CALL_DIRECTION_INCOMING /* callDirection */,
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy);
// Ensure new calls related to self-managed calls/connections are set as such. This will
// be overridden when the actual connection is returned in startCreateConnection, however
// doing this now ensures the logs and any other logic will treat this call as self-managed
// from the moment it is created.
PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
phoneAccountHandle);
if (phoneAccount != null) {
call.setIsSelfManaged(phoneAccount.isSelfManaged());
if (call.isSelfManaged()) {
// Self managed calls will always be voip audio mode.
call.setIsVoipAudioMode(true);
} else {
// Incoming call is managed, the active call is self-managed and can't be held.
// We need to set extras on it to indicate whether answering will cause a
// active self-managed call to drop.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
if (activeCall != null && !canHold(activeCall) && activeCall.isSelfManaged()) {
Bundle dropCallExtras = new Bundle();
dropCallExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
// Include the name of the app which will drop the call.
CharSequence droppedApp = activeCall.getTargetPhoneAccountLabel();
dropCallExtras.putCharSequence(
Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME, droppedApp);
Log.i(this, "Incoming managed call will drop %s call.", droppedApp);
call.putExtras(Call.SOURCE_CONNECTION_SERVICE, dropCallExtras);
}
}
if (extras.getBoolean(PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE)) {
Log.d(this, "processIncomingCallIntent: defaulting to voip mode for call %s",
call.getId());
call.setIsVoipAudioMode(true);
}
}
if (isRttSettingOn() ||
extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
Log.i(this, "Incoming call requesting RTT, rtt setting is %b", isRttSettingOn());
call.createRttStreams();
// Even if the phone account doesn't support RTT yet, the connection manager might
// change that. Set this to check it later.
call.setRequestedToStartWithRtt();
}
// If the extras specifies a video state, set it on the call if the PhoneAccount supports
// video.
int videoState = VideoProfile.STATE_AUDIO_ONLY;
if (extras.containsKey(TelecomManager.EXTRA_INCOMING_VIDEO_STATE) &&
phoneAccount != null && phoneAccount.hasCapabilities(
PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
videoState = extras.getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE);
call.setVideoState(videoState);
}
call.initAnalytics();
if (getForegroundCall() != null) {
getForegroundCall().getAnalytics().setCallIsInterrupted(true);
call.getAnalytics().setCallIsAdditional(true);
}
setIntentExtrasAndStartTime(call, extras);
// TODO: Move this to be a part of addCall()
call.addListener(this);
boolean isHandoverAllowed = true;
if (isHandover) {
if (!isHandoverInProgress() &&
isHandoverToPhoneAccountSupported(phoneAccountHandle)) {
final String handleScheme = handle.getSchemeSpecificPart();
Call fromCall = mCalls.stream()
.filter((c) -> mPhoneNumberUtilsAdapter.isSamePhoneNumber(
(c.getHandle() == null
? null : c.getHandle().getSchemeSpecificPart()),
handleScheme))
.findFirst()
.orElse(null);
if (fromCall != null) {
if (!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount())) {
Log.w(this, "processIncomingCallIntent: From account doesn't support " +
"handover.");
isHandoverAllowed = false;
}
} else {
Log.w(this, "processIncomingCallIntent: handover fail; can't find from call.");
isHandoverAllowed = false;
}
if (isHandoverAllowed) {
// Link the calls so we know we're handing over.
fromCall.setHandoverDestinationCall(call);
call.setHandoverSourceCall(fromCall);
call.setHandoverState(HandoverState.HANDOVER_TO_STARTED);
fromCall.setHandoverState(HandoverState.HANDOVER_FROM_STARTED);
Log.addEvent(fromCall, LogUtils.Events.START_HANDOVER,
"handOverFrom=%s, handOverTo=%s", fromCall.getId(), call.getId());
Log.addEvent(call, LogUtils.Events.START_HANDOVER,
"handOverFrom=%s, handOverTo=%s", fromCall.getId(), call.getId());
if (isSpeakerEnabledForVideoCalls() && VideoProfile.isVideo(videoState)) {
// Ensure when the call goes active that it will go to speakerphone if the
// handover to call is a video call.
call.setStartWithSpeakerphoneOn(true);
}
}
} else {
Log.w(this, "processIncomingCallIntent: To account doesn't support handover.");
}
}
if (!isHandoverAllowed || (call.isSelfManaged() && !isIncomingCallPermitted(call,
call.getTargetPhoneAccount()))) {
notifyCreateConnectionFailed(phoneAccountHandle, call);
} else {
call.startCreateConnection(mPhoneAccountRegistrar);
}
}
void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
Uri handle = extras.getParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE);
Log.i(this, "addNewUnknownCall with handle: %s", Log.pii(handle));
Call call = new Call(
getNextCallId(),
mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
handle,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
Call.CALL_DIRECTION_UNKNOWN /* callDirection */,
// Use onCreateIncomingConnection in TelephonyConnectionService, so that we attach
// to the existing connection instead of trying to create a new one.
true /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy);
call.initAnalytics();
setIntentExtrasAndStartTime(call, extras);
call.addListener(this);
call.startCreateConnection(mPhoneAccountRegistrar);
}
private boolean areHandlesEqual(Uri handle1, Uri handle2) {
if (handle1 == null || handle2 == null) {
return handle1 == handle2;
}
if (!TextUtils.equals(handle1.getScheme(), handle2.getScheme())) {
return false;
}
final String number1 = PhoneNumberUtils.normalizeNumber(handle1.getSchemeSpecificPart());
final String number2 = PhoneNumberUtils.normalizeNumber(handle2.getSchemeSpecificPart());
return TextUtils.equals(number1, number2);
}
private Call reuseOutgoingCall(Uri handle) {
// Check to see if we can reuse any of the calls that are waiting to disconnect.
// See {@link Call#abort} and {@link #onCanceledViaNewOutgoingCall} for more information.
Call reusedCall = null;
for (Iterator<Call> callIter = mPendingCallsToDisconnect.iterator(); callIter.hasNext();) {
Call pendingCall = callIter.next();
if (reusedCall == null && areHandlesEqual(pendingCall.getHandle(), handle)) {
callIter.remove();
Log.i(this, "Reusing disconnected call %s", pendingCall);
reusedCall = pendingCall;
} else {
Log.i(this, "Not reusing disconnected call %s", pendingCall);
callIter.remove();
pendingCall.disconnect();
}
}
return reusedCall;
}
/**
* Kicks off the first steps to creating an outgoing call.
*
* For managed connections, this is the first step to launching the Incall UI.
* For self-managed connections, we don't expect the Incall UI to launch, but this is still a
* first step in getting the self-managed ConnectionService to create the connection.
* @param handle Handle to connect the call with.
* @param phoneAccountHandle The phone account which contains the component name of the
* connection service to use for this call.
* @param extras The optional extras Bundle passed with the intent used for the incoming call.
* @param initiatingUser {@link UserHandle} of user that place the outgoing call.
* @param originalIntent
*/
@VisibleForTesting
public Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras,
UserHandle initiatingUser, Intent originalIntent) {
boolean isReusedCall = true;
Call call = reuseOutgoingCall(handle);
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
boolean isSelfManaged = account != null && account.isSelfManaged();
// Create a call with original handle. The handle may be changed when the call is attached
// to a connection service, but in most cases will remain the same.
if (call == null) {
call = new Call(getNextCallId(), mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
handle,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
null /* phoneAccountHandle */,
Call.CALL_DIRECTION_OUTGOING /* callDirection */,
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy);
call.initAnalytics();
// Ensure new calls related to self-managed calls/connections are set as such. This
// will be overridden when the actual connection is returned in startCreateConnection,
// however doing this now ensures the logs and any other logic will treat this call as
// self-managed from the moment it is created.
call.setIsSelfManaged(isSelfManaged);
if (isSelfManaged) {
// Self-managed calls will ALWAYS use voip audio mode.
call.setIsVoipAudioMode(true);
}
call.setInitiatingUser(initiatingUser);
isReusedCall = false;
}
int videoState = VideoProfile.STATE_AUDIO_ONLY;
if (extras != null) {
// Set the video state on the call early so that when it is added to the InCall UI the
// UI knows to configure itself as a video call immediately.
videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
VideoProfile.STATE_AUDIO_ONLY);
// If this is an emergency video call, we need to check if the phone account supports
// emergency video calling.
// Also, ensure we don't try to place an outgoing call with video if video is not
// supported.
if (VideoProfile.isVideo(videoState)) {
if (call.isEmergencyCall() && account != null &&
!account.hasCapabilities(PhoneAccount.CAPABILITY_EMERGENCY_VIDEO_CALLING)) {
// Phone account doesn't support emergency video calling, so fallback to
// audio-only now to prevent the InCall UI from setting up video surfaces
// needlessly.
Log.i(this, "startOutgoingCall - emergency video calls not supported; " +
"falling back to audio-only");
videoState = VideoProfile.STATE_AUDIO_ONLY;
} else if (account != null &&
!account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
// Phone account doesn't support video calling, so fallback to audio-only.
Log.i(this, "startOutgoingCall - video calls not supported; fallback to " +
"audio-only.");
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
}
call.setVideoState(videoState);
}
List<PhoneAccountHandle> potentialPhoneAccounts = findOutgoingCallPhoneAccount(
phoneAccountHandle, handle, VideoProfile.isVideo(videoState), initiatingUser);
if (potentialPhoneAccounts.size() == 1) {
phoneAccountHandle = potentialPhoneAccounts.get(0);
} else {
phoneAccountHandle = null;
}
call.setTargetPhoneAccount(phoneAccountHandle);
boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle) && !isSelfManaged;
// Do not support any more live calls. Our options are to move a call to hold, disconnect
// a call, or cancel this call altogether. If a call is being reused, then it has already
// passed the makeRoomForOutgoingCall check once and will fail the second time due to the
// call transitioning into the CONNECTING state.
if (!isPotentialInCallMMICode && (!isReusedCall
&& !makeRoomForOutgoingCall(call, call.isEmergencyCall()))) {
Call foregroundCall = getForegroundCall();
Log.d(this, "No more room for outgoing call %s ", call);
if (foregroundCall.isSelfManaged()) {
// If the ongoing call is a self-managed call, then prompt the user to ask if they'd
// like to disconnect their ongoing call and place the outgoing call.
call.setOriginalCallIntent(originalIntent);
startCallConfirmation(call);
} else {
// If the ongoing call is a managed call, we will prevent the outgoing call from
// dialing.
notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
}
return null;
}
// The outgoing call can be placed, go forward.
boolean needsAccountSelection =
phoneAccountHandle == null && potentialPhoneAccounts.size() > 1
&& !call.isEmergencyCall() && !isSelfManaged;
if (needsAccountSelection) {
// This is the state where the user is expected to select an account
call.setState(CallState.SELECT_PHONE_ACCOUNT, "needs account selection");
// Create our own instance to modify (since extras may be Bundle.EMPTY)
extras = new Bundle(extras);
extras.putParcelableList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS,
potentialPhoneAccounts);
} else {
PhoneAccount accountToUse =
mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
if (accountToUse != null && accountToUse.getExtras() != null) {
if (accountToUse.getExtras()
.getBoolean(PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE)) {
Log.d(this, "startOutgoingCall: defaulting to voip mode for call %s",
call.getId());
call.setIsVoipAudioMode(true);
}
}
call.setState(
CallState.CONNECTING,
phoneAccountHandle == null ? "no-handle" : phoneAccountHandle.toString());
if (isRttSettingOn() || (extras != null
&& extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false))) {
Log.d(this, "Outgoing call requesting RTT, rtt setting is %b", isRttSettingOn());
if (accountToUse != null
&& accountToUse.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
call.createRttStreams();
}
// Even if the phone account doesn't support RTT yet, the connection manager might
// change that. Set this to check it later.
call.setRequestedToStartWithRtt();
}
}
setIntentExtrasAndStartTime(call, extras);
if ((isPotentialMMICode(handle) || isPotentialInCallMMICode) && !needsAccountSelection) {
// Do not add the call if it is a potential MMI code.
call.addListener(this);
} else if (!mCalls.contains(call)) {
// We check if mCalls already contains the call because we could potentially be reusing
// a call which was previously added (See {@link #reuseOutgoingCall}).
addCall(call);
}
return call;
}
/**
* Finds the {@link PhoneAccountHandle}(s) which could potentially be used to place an outgoing
* call. Takes into account the following:
* 1. Any pre-chosen {@link PhoneAccountHandle} which was specified on the
* {@link Intent#ACTION_CALL} intent. If one was chosen it will be used if possible.
* 2. Whether the call is a video call. If the call being placed is a video call, an attempt is
* first made to consider video capable phone accounts. If no video capable phone accounts are
* found, the usual non-video capable phone accounts will be considered.
* 3. Whether there is a user-chosen default phone account; that one will be used if possible.
*
* @param targetPhoneAccountHandle The pre-chosen {@link PhoneAccountHandle} passed in when the
* call was placed. Will be {@code null} if the
* {@link Intent#ACTION_CALL} intent did not specify a target
* phone account.
* @param handle The handle of the outgoing call; used to determine the SIP scheme when matching
* phone accounts.
* @param isVideo {@code true} if the call is a video call, {@code false} otherwise.
* @param initiatingUser The {@link UserHandle} the call is placed on.
* @return
*/
@VisibleForTesting
public List<PhoneAccountHandle> findOutgoingCallPhoneAccount(
PhoneAccountHandle targetPhoneAccountHandle, Uri handle, boolean isVideo,
UserHandle initiatingUser) {
boolean isSelfManaged = isSelfManaged(targetPhoneAccountHandle, initiatingUser);
List<PhoneAccountHandle> accounts;
if (!isSelfManaged) {
// Try to find a potential phone account, taking into account whether this is a video
// call.
accounts = constructPossiblePhoneAccounts(handle, initiatingUser, isVideo);
if (isVideo && accounts.size() == 0) {
// Placing a video call but no video capable accounts were found, so consider any
// call capable accounts (we can fallback to audio).
accounts = constructPossiblePhoneAccounts(handle, initiatingUser,
false /* isVideo */);
}
Log.v(this, "findOutgoingCallPhoneAccount: accounts = " + accounts);
// Only dial with the requested phoneAccount if it is still valid. Otherwise treat this
// call as if a phoneAccount was not specified (does the default behavior instead).
// Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
if (targetPhoneAccountHandle != null) {
if (!accounts.contains(targetPhoneAccountHandle)) {
targetPhoneAccountHandle = null;
} else {
// The target phone account is valid and was found.
return Arrays.asList(targetPhoneAccountHandle);
}
}
if (targetPhoneAccountHandle == null && accounts.size() > 0) {
// No preset account, check if default exists that supports the URI scheme for the
// handle and verify it can be used.
if (accounts.size() > 1) {
PhoneAccountHandle defaultPhoneAccountHandle =
mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(
handle.getScheme(), initiatingUser);
if (defaultPhoneAccountHandle != null &&
accounts.contains(defaultPhoneAccountHandle)) {
accounts.clear();
accounts.add(defaultPhoneAccountHandle);
}
}
}
} else {
// Self-managed ConnectionServices can only have a single potential account.
accounts = Arrays.asList(targetPhoneAccountHandle);
}
return accounts;
}
/**
* Determines if a {@link PhoneAccountHandle} is for a self-managed ConnectionService.
* @param targetPhoneAccountHandle The phone account to check.
* @param initiatingUser The user associated with the account.
* @return {@code true} if the phone account is self-managed, {@code false} otherwise.
*/
public boolean isSelfManaged(PhoneAccountHandle targetPhoneAccountHandle,
UserHandle initiatingUser) {
PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccount(
targetPhoneAccountHandle, initiatingUser);
return targetPhoneAccount != null && targetPhoneAccount.isSelfManaged();
}
/**
* Attempts to issue/connect the specified call.
*
* @param handle Handle to connect the call with.
* @param gatewayInfo Optional gateway information that can be used to route the call to the
* actual dialed handle via a gateway provider. May be null.
* @param speakerphoneOn Whether or not to turn the speakerphone on once the call connects.
* @param videoState The desired video state for the outgoing call.
*/
@VisibleForTesting
public void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo,
boolean speakerphoneOn, int videoState) {
if (call == null) {
// don't do anything if the call no longer exists
Log.i(this, "Canceling unknown call.");
return;
}
final Uri uriHandle = (gatewayInfo == null) ? handle : gatewayInfo.getGatewayAddress();
if (gatewayInfo == null) {
Log.i(this, "Creating a new outgoing call with handle: %s", Log.piiHandle(uriHandle));
} else {
Log.i(this, "Creating a new outgoing call with gateway handle: %s, original handle: %s",
Log.pii(uriHandle), Log.pii(handle));
}
call.setHandle(uriHandle);
call.setGatewayInfo(gatewayInfo);
final boolean useSpeakerWhenDocked = mContext.getResources().getBoolean(
R.bool.use_speaker_when_docked);
final boolean useSpeakerForDock = isSpeakerphoneEnabledForDock();
final boolean useSpeakerForVideoCall = isSpeakerphoneAutoEnabledForVideoCalls(videoState);
// Auto-enable speakerphone if the originating intent specified to do so, if the call
// is a video call, of if using speaker when docked
call.setStartWithSpeakerphoneOn(speakerphoneOn || useSpeakerForVideoCall
|| (useSpeakerWhenDocked && useSpeakerForDock));
call.setVideoState(videoState);
if (speakerphoneOn) {
Log.i(this, "%s Starting with speakerphone as requested", call);
} else if (useSpeakerWhenDocked && useSpeakerForDock) {
Log.i(this, "%s Starting with speakerphone because car is docked.", call);
} else if (useSpeakerForVideoCall) {
Log.i(this, "%s Starting with speakerphone because its a video call.", call);
}
if (call.isEmergencyCall()) {
new AsyncEmergencyContactNotifier(mContext).execute();
}
final boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_requireCallCapableAccountForHandle);
final boolean isOutgoingCallPermitted = isOutgoingCallPermitted(call,
call.getTargetPhoneAccount());
final String callHandleScheme =
call.getHandle() == null ? null : call.getHandle().getScheme();
if (call.getTargetPhoneAccount() != null || call.isEmergencyCall()) {
// If the account has been set, proceed to place the outgoing call.
// Otherwise the connection will be initiated when the account is set by the user.
if (call.isSelfManaged() && !isOutgoingCallPermitted) {
notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
} else {
if (call.isEmergencyCall()) {
// Drop any ongoing self-managed calls to make way for an emergency call.
disconnectSelfManagedCalls("place emerg call" /* reason */);
}
call.startCreateConnection(mPhoneAccountRegistrar);
}
} else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
requireCallCapableAccountByHandle ? callHandleScheme : null, false,
call.getInitiatingUser()).isEmpty()) {
// If there are no call capable accounts, disconnect the call.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.CANCELED,
"No registered PhoneAccounts"));
markCallAsRemoved(call);
}
}
/**
* Attempts to start a conference call for the specified call.
*
* @param call The call to conference.
* @param otherCall The other call to conference with.
*/
@VisibleForTesting
public void conference(Call call, Call otherCall) {
call.conferenceWith(otherCall);
}
/**
* Instructs Telecom to answer the specified call. Intended to be invoked by the in-call
* app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
* the user opting to answer said call.
*
* @param call The call to answer.
* @param videoState The video state in which to answer the call.
*/
@VisibleForTesting
public void answerCall(Call call, int videoState) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to answer a non-existent call %s", call);
} else {
// Hold or disconnect the active call and request call focus for the incoming call.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
Log.d(this, "Incoming call = %s Ongoing call %s", call, activeCall);
holdActiveCallForNewCall(call);
mConnectionSvrFocusMgr.requestFocus(
call,
new RequestCallback(new ActionAnswerCall(call, videoState)));
}
}
/**
* Instructs Telecom to deflect the specified call. Intended to be invoked by the in-call
* app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
* the user opting to deflect said call.
*/
@VisibleForTesting
public void deflectCall(Call call, Uri address) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to deflect a non-existent call %s", call);
} else {
call.deflect(address);
}
}
/**
* Determines if the speakerphone should be automatically enabled for the call. Speakerphone
* should be enabled if the call is a video call and bluetooth or the wired headset are not in
* use.
*
* @param videoState The video state of the call.
* @return {@code true} if the speakerphone should be enabled.
*/
public boolean isSpeakerphoneAutoEnabledForVideoCalls(int videoState) {
return VideoProfile.isVideo(videoState) &&
!mWiredHeadsetManager.isPluggedIn() &&
!mBluetoothRouteManager.isBluetoothAvailable() &&
isSpeakerEnabledForVideoCalls();
}
/**
* Determines if the speakerphone should be enabled for when docked. Speakerphone
* should be enabled if the device is docked and bluetooth or the wired headset are
* not in use.
*
* @return {@code true} if the speakerphone should be enabled for the dock.
*/
private boolean isSpeakerphoneEnabledForDock() {
return mDockManager.isDocked() &&
!mWiredHeadsetManager.isPluggedIn() &&
!mBluetoothRouteManager.isBluetoothAvailable();
}
/**
* Determines if the speakerphone should be automatically enabled for video calls.
*
* @return {@code true} if the speakerphone should automatically be enabled.
*/
private static boolean isSpeakerEnabledForVideoCalls() {
return (SystemProperties.getInt(TelephonyProperties.PROPERTY_VIDEOCALL_AUDIO_OUTPUT,
PhoneConstants.AUDIO_OUTPUT_DEFAULT) ==
PhoneConstants.AUDIO_OUTPUT_ENABLE_SPEAKER);
}
/**
* Instructs Telecom to reject the specified call. Intended to be invoked by the in-call
* app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
* the user opting to reject said call.
*/
@VisibleForTesting
public void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to reject a non-existent call %s", call);
} else {
for (CallsManagerListener listener : mListeners) {
listener.onIncomingCallRejected(call, rejectWithMessage, textMessage);
}
call.reject(rejectWithMessage, textMessage);
}
}
/**
* Instructs Telecom to play the specified DTMF tone within the specified call.
*
* @param digit The DTMF digit to play.
*/
@VisibleForTesting
public void playDtmfTone(Call call, char digit) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to play DTMF in a non-existent call %s", call);
} else {
if (call.getState() != CallState.ON_HOLD) {
call.playDtmfTone(digit);
mDtmfLocalTonePlayer.playTone(call, digit);
} else {
Log.i(this, "Request to play DTMF tone for held call %s", call.getId());
}
}
}
/**
* Instructs Telecom to stop the currently playing DTMF tone, if any.
*/
@VisibleForTesting
public void stopDtmfTone(Call call) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to stop DTMF in a non-existent call %s", call);
} else {
call.stopDtmfTone();
mDtmfLocalTonePlayer.stopTone(call);
}
}
/**
* Instructs Telecom to continue (or not) the current post-dial DTMF string, if any.
*/
void postDialContinue(Call call, boolean proceed) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to continue post-dial string in a non-existent call %s", call);
} else {
call.postDialContinue(proceed);
}
}
/**
* Instructs Telecom to disconnect the specified call. Intended to be invoked by the
* in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
* the user hitting the end-call button.
*/
@VisibleForTesting
public void disconnectCall(Call call) {
Log.v(this, "disconnectCall %s", call);
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to disconnect", call);
} else {
mLocallyDisconnectingCalls.add(call);
call.disconnect();
}
}
/**
* Instructs Telecom to disconnect all calls.
*/
void disconnectAllCalls() {
Log.v(this, "disconnectAllCalls");
for (Call call : mCalls) {
disconnectCall(call);
}
}
/**
* Disconnects calls for any other {@link PhoneAccountHandle} but the one specified.
* Note: As a protective measure, will NEVER disconnect an emergency call. Although that
* situation should never arise, its a good safeguard.
* @param phoneAccountHandle Calls owned by {@link PhoneAccountHandle}s other than this one will
* be disconnected.
*/
private void disconnectOtherCalls(PhoneAccountHandle phoneAccountHandle) {
mCalls.stream()
.filter(c -> !c.isEmergencyCall() &&
!c.getTargetPhoneAccount().equals(phoneAccountHandle))
.forEach(c -> disconnectCall(c));
}
/**
* Instructs Telecom to put the specified call on hold. Intended to be invoked by the
* in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
* the user hitting the hold button during an active call.
*/
@VisibleForTesting
public void holdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be put on hold", call);
} else {
Log.d(this, "Putting call on hold: (%s)", call);
call.hold();
}
}
/**
* Instructs Telecom to release the specified call from hold. Intended to be invoked by
* the in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered
* by the user hitting the hold button during a held call.
*/
@VisibleForTesting
public void unholdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
} else {
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
String activeCallId = null;
if (activeCall != null) {
activeCallId = activeCall.getId();
if (canHold(activeCall)) {
activeCall.hold("Swap to " + call.getId());
Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
} else {
// This call does not support hold. If it is from a different connection
// service, then disconnect it, otherwise invoke call.hold() and allow the
// connection service to handle the situation.
if (activeCall.getConnectionService() != call.getConnectionService()) {
activeCall.disconnect("Swap to " + call.getId());
} else {
activeCall.hold("Swap to " + call.getId());
}
}
}
mConnectionSvrFocusMgr.requestFocus(
call,
new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
}
}
@Override
public void onExtrasRemoved(Call c, int source, List<String> keys) {
if (source != Call.SOURCE_CONNECTION_SERVICE) {
return;
}
updateCanAddCall();
}
@Override
public void onExtrasChanged(Call c, int source, Bundle extras) {
if (source != Call.SOURCE_CONNECTION_SERVICE) {
return;
}
handleCallTechnologyChange(c);
handleChildAddressChange(c);
updateCanAddCall();
}
// Construct the list of possible PhoneAccounts that the outgoing call can use based on the
// active calls in CallsManager. If any of the active calls are on a SIM based PhoneAccount,
// then include only that SIM based PhoneAccount and any non-SIM PhoneAccounts, such as SIP.
@VisibleForTesting
public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
boolean isVideo) {
if (handle == null) {
return Collections.emptyList();
}
// If we're specifically looking for video capable accounts, then include that capability,
// otherwise specify no additional capability constraints.
List<PhoneAccountHandle> allAccounts =
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false, user,
isVideo ? PhoneAccount.CAPABILITY_VIDEO_CALLING : 0 /* any */);
// First check the Radio SIM Technology
if(mRadioSimVariants == null) {
TelephonyManager tm = (TelephonyManager) mContext.getSystemService(
Context.TELEPHONY_SERVICE);
// Cache Sim Variants
mRadioSimVariants = tm.getMultiSimConfiguration();
}
// Only one SIM PhoneAccount can be active at one time for DSDS. Only that SIM PhoneAccount
// Should be available if a call is already active on the SIM account.
if(mRadioSimVariants != TelephonyManager.MultiSimVariants.DSDA) {
List<PhoneAccountHandle> simAccounts =
mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser();
PhoneAccountHandle ongoingCallAccount = null;
for (Call c : mCalls) {
if (!c.isDisconnected() && !c.isNew() && simAccounts.contains(
c.getTargetPhoneAccount())) {
ongoingCallAccount = c.getTargetPhoneAccount();
break;
}
}
if (ongoingCallAccount != null) {
// Remove all SIM accounts that are not the active SIM from the list.
simAccounts.remove(ongoingCallAccount);
allAccounts.removeAll(simAccounts);
}
}
return allAccounts;
}
/**
* Informs listeners (notably {@link CallAudioManager} of a change to the call's external
* property.
* .
* @param call The call whose external property changed.
* @param isExternalCall {@code True} if the call is now external, {@code false} otherwise.
*/
@Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {
Log.v(this, "onConnectionPropertiesChanged: %b", isExternalCall);
for (CallsManagerListener listener : mListeners) {
listener.onExternalCallChanged(call, isExternalCall);
}
}
private void handleCallTechnologyChange(Call call) {
if (call.getExtras() != null
&& call.getExtras().containsKey(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE)) {
Integer analyticsCallTechnology = sAnalyticsTechnologyMap.get(
call.getExtras().getInt(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE));
if (analyticsCallTechnology == null) {
analyticsCallTechnology = Analytics.THIRD_PARTY_PHONE;
}
call.getAnalytics().addCallTechnology(analyticsCallTechnology);
}
}
public void handleChildAddressChange(Call call) {
if (call.getExtras() != null
&& call.getExtras().containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
String viaNumber = call.getExtras().getString(Connection.EXTRA_CHILD_ADDRESS);
call.setViaNumber(viaNumber);
}
}
/** Called by the in-call UI to change the mute state. */
void mute(boolean shouldMute) {
if (hasEmergencyCall() && shouldMute) {
Log.i(this, "Refusing to turn on mute because we're in an emergency call");
shouldMute = false;
}
mCallAudioManager.mute(shouldMute);
}
/**
* Called by the in-call UI to change the audio route, for example to change from earpiece to
* speaker phone.
*/
void setAudioRoute(int route, String bluetoothAddress) {
if (hasEmergencyRttCall() && route != CallAudioState.ROUTE_SPEAKER) {
Log.i(this, "In an emergency RTT call. Forcing route to speaker.");
route = CallAudioState.ROUTE_SPEAKER;
bluetoothAddress = null;
}
mCallAudioManager.setAudioRoute(route, bluetoothAddress);
}
/** Called by the in-call UI to turn the proximity sensor on. */
void turnOnProximitySensor() {
mProximitySensorManager.turnOn();
}
/**
* Called by the in-call UI to turn the proximity sensor off.
* @param screenOnImmediately If true, the screen will be turned on immediately. Otherwise,
* the screen will be kept off until the proximity sensor goes negative.
*/
void turnOffProximitySensor(boolean screenOnImmediately) {
mProximitySensorManager.turnOff(screenOnImmediately);
}
private boolean isRttSettingOn() {
return Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.RTT_CALLING_MODE, 0) != 0;
}
void phoneAccountSelected(Call call, PhoneAccountHandle account, boolean setDefault) {
if (!mCalls.contains(call)) {
Log.i(this, "Attempted to add account to unknown call %s", call);
} else {
call.setTargetPhoneAccount(account);
PhoneAccount realPhoneAccount =
mPhoneAccountRegistrar.getPhoneAccountUnchecked(account);
if (realPhoneAccount != null && realPhoneAccount.getExtras() != null
&& realPhoneAccount.getExtras()
.getBoolean(PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE)) {
Log.d("phoneAccountSelected: default to voip mode for call %s", call.getId());
call.setIsVoipAudioMode(true);
}
if (isRttSettingOn() || call.getIntentExtras()
.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
Log.d(this, "Outgoing call after account selection requesting RTT," +
" rtt setting is %b", isRttSettingOn());
if (realPhoneAccount != null
&& realPhoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
call.createRttStreams();
}
// Even if the phone account doesn't support RTT yet, the connection manager might
// change that. Set this to check it later.
call.setRequestedToStartWithRtt();
}
if (!call.isNewOutgoingCallIntentBroadcastDone()) {
return;
}
// Note: emergency calls never go through account selection dialog so they never
// arrive here.
if (makeRoomForOutgoingCall(call, false /* isEmergencyCall */)) {
call.startCreateConnection(mPhoneAccountRegistrar);
} else {
call.disconnect("no room");
}
if (setDefault) {
mPhoneAccountRegistrar
.setUserSelectedOutgoingPhoneAccount(account, call.getInitiatingUser());
}
}
}
/** Called when the audio state changes. */
@VisibleForTesting
public void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState
newAudioState) {
Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
for (CallsManagerListener listener : mListeners) {
listener.onCallAudioStateChanged(oldAudioState, newAudioState);
}
}
/**
* Called when disconnect tone is started or stopped, including any InCallTone
* after disconnected call.
*
* @param isTonePlaying true if the disconnected tone is started, otherwise the disconnected
* tone is stopped.
*/
@VisibleForTesting
public void onDisconnectedTonePlaying(boolean isTonePlaying) {
Log.v(this, "onDisconnectedTonePlaying, %s", isTonePlaying ? "started" : "stopped");
for (CallsManagerListener listener : mListeners) {
listener.onDisconnectedTonePlaying(isTonePlaying);
}
}
void markCallAsRinging(Call call) {
setCallState(call, CallState.RINGING, "ringing set explicitly");
}
void markCallAsDialing(Call call) {
setCallState(call, CallState.DIALING, "dialing set explicitly");
maybeMoveToSpeakerPhone(call);
maybeTurnOffMute(call);
ensureCallAudible();
}
void markCallAsPulling(Call call) {
setCallState(call, CallState.PULLING, "pulling set explicitly");
maybeMoveToSpeakerPhone(call);
}
/**
* Returns true if the active call is held.
*/
boolean holdActiveCallForNewCall(Call call) {
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
if (activeCall != null && activeCall != call) {
if (canHold(activeCall)) {
activeCall.hold();
return true;
} else if (supportsHold(activeCall)
&& activeCall.getConnectionService() == call.getConnectionService()) {
// Handle the case where the active call and the new call are from the same CS, and
// the currently active call supports hold but cannot currently be held.
// In this case we'll look for the other held call for this connectionService and
// disconnect it prior to holding the active call.
// E.g.
// Call A - Held (Supports hold, can't hold)
// Call B - Active (Supports hold, can't hold)
// Call C - Incoming
// Here we need to disconnect A prior to holding B so that C can be answered.
// This case is driven by telephony requirements ultimately.
Call heldCall = getHeldCallByConnectionService(call.getConnectionService());
if (heldCall != null) {
heldCall.disconnect();
Log.i(this, "holdActiveCallForNewCall: Disconnect held call %s before "
+ "holding active call %s.",
heldCall.getId(), activeCall.getId());
}
Log.i(this, "holdActiveCallForNewCall: Holding active %s before making %s active.",
activeCall.getId(), call.getId());
activeCall.hold();
return true;
} else {
// This call does not support hold. If it is from a different connection
// service, then disconnect it, otherwise allow the connection service to
// figure out the right states.
if (activeCall.getConnectionService() != call.getConnectionService()) {
Log.i(this, "holdActiveCallForNewCall: disconnecting %s so that %s can be "
+ "made active.", activeCall.getId(), call.getId());
activeCall.disconnect();
}
}
}
return false;
}
@VisibleForTesting
public void markCallAsActive(Call call) {
if (call.isSelfManaged()) {
// backward compatibility, the self-managed connection service will set the call state
// to active directly. We should hold or disconnect the current active call based on the
// holdability, and request the call focus for the self-managed call before the state
// change.
holdActiveCallForNewCall(call);
mConnectionSvrFocusMgr.requestFocus(
call,
new RequestCallback(new ActionSetCallState(
call,
CallState.ACTIVE,
"active set explicitly for self-managed")));
} else {
setCallState(call, CallState.ACTIVE, "active set explicitly");
maybeMoveToSpeakerPhone(call);
ensureCallAudible();
}
}
@VisibleForTesting
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
}
/**
* Marks the specified call as STATE_DISCONNECTED and notifies the in-call app. If this was the
* last live call, then also disconnect from the in-call controller.
*
* @param disconnectCause The disconnect cause, see {@link android.telecom.DisconnectCause}.
*/
void markCallAsDisconnected(Call call, DisconnectCause disconnectCause) {
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
}
/**
* Removes an existing disconnected call, and notifies the in-call app.
*/
void markCallAsRemoved(Call call) {
call.maybeCleanupHandover();
removeCall(call);
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
if (mLocallyDisconnectingCalls.contains(call)) {
boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
Log.v(this, "markCallAsRemoved: isDisconnectingChildCall = "
+ isDisconnectingChildCall + "call -> %s", call);
mLocallyDisconnectingCalls.remove(call);
// Auto-unhold the foreground call due to a locally disconnected call, except if the
// call which was disconnected is a member of a conference (don't want to auto un-hold
// the conference if we remove a member of the conference).
if (!isDisconnectingChildCall && foregroundCall != null
&& foregroundCall.getState() == CallState.ON_HOLD) {
foregroundCall.unhold();
}
} else if (foregroundCall != null &&
!foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
foregroundCall.getState() == CallState.ON_HOLD) {
// The new foreground call is on hold, however the carrier does not display the hold
// button in the UI. Therefore, we need to auto unhold the held call since the user has
// no means of unholding it themselves.
Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)");
foregroundCall.unhold();
}
}
/**
* Given a call, marks the call as disconnected and removes it. Set the error message to
* indicate to the user that the call cannot me placed due to an ongoing call in another app.
*
* Used when there are ongoing self-managed calls and the user tries to make an outgoing managed
* call. Called by {@link #startCallConfirmation(Call)} when the user is already confirming an
* outgoing call. Realistically this should almost never be called since in practice the user
* won't make multiple outgoing calls at the same time.
*
* @param call The call to mark as disconnected.
*/
void markCallDisconnectedDueToSelfManagedCall(Call call) {
Call activeCall = getActiveCall();
CharSequence errorMessage;
if (activeCall == null) {
// Realistically this shouldn't happen, but best to handle gracefully
errorMessage = mContext.getText(R.string.cant_call_due_to_ongoing_unknown_call);
} else {
errorMessage = mContext.getString(R.string.cant_call_due_to_ongoing_call,
activeCall.getTargetPhoneAccountLabel());
}
// Call is managed and there are ongoing self-managed calls.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR,
errorMessage, errorMessage, "Ongoing call in another app."));
markCallAsRemoved(call);
}
/**
* Cleans up any calls currently associated with the specified connection service when the
* service binder disconnects unexpectedly.
*
* @param service The connection service that disconnected.
*/
void handleConnectionServiceDeath(ConnectionServiceWrapper service) {
if (service != null) {
Log.i(this, "handleConnectionServiceDeath: service %s died", service);
for (Call call : mCalls) {
if (call.getConnectionService() == service) {
if (call.getState() != CallState.DISCONNECTED) {
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR,
"CS_DEATH"));
}
markCallAsRemoved(call);
}
}
}
}
/**
* Determines if the {@link CallsManager} has any non-external calls.
*
* @return {@code True} if there are any non-external calls, {@code false} otherwise.
*/
boolean hasAnyCalls() {
if (mCalls.isEmpty()) {
return false;
}
for (Call call : mCalls) {
if (!call.isExternalCall()) {
return true;
}
}
return false;
}
boolean hasActiveOrHoldingCall() {
return getFirstCallWithState(CallState.ACTIVE, CallState.ON_HOLD) != null;
}
boolean hasRingingCall() {
return getFirstCallWithState(CallState.RINGING) != null;
}
boolean onMediaButton(int type) {
if (hasAnyCalls()) {
Call ringingCall = getFirstCallWithState(CallState.RINGING);
if (HeadsetMediaButton.SHORT_PRESS == type) {
if (ringingCall == null) {
Call callToHangup = getFirstCallWithState(CallState.RINGING, CallState.DIALING,
CallState.PULLING, CallState.ACTIVE, CallState.ON_HOLD);
Log.addEvent(callToHangup, LogUtils.Events.INFO,
"media btn short press - end call.");
if (callToHangup != null) {
disconnectCall(callToHangup);
return true;
}
} else {
ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
return true;
}
} else if (HeadsetMediaButton.LONG_PRESS == type) {
if (ringingCall != null) {
Log.addEvent(getForegroundCall(),
LogUtils.Events.INFO, "media btn long press - reject");
ringingCall.reject(false, null);
} else {
Log.addEvent(getForegroundCall(), LogUtils.Events.INFO,
"media btn long press - mute");
mCallAudioManager.toggleMute();
}
return true;
}
}
return false;
}
/**
* Returns true if telecom supports adding another top-level call.
*/
@VisibleForTesting
public boolean canAddCall() {
boolean isDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
if (!isDeviceProvisioned) {
Log.d(TAG, "Device not provisioned, canAddCall is false.");
return false;
}
if (getFirstCallWithState(OUTGOING_CALL_STATES) != null) {
return false;
}
int count = 0;
for (Call call : mCalls) {
if (call.isEmergencyCall()) {
// We never support add call if one of the calls is an emergency call.
return false;
} else if (call.isExternalCall()) {
// External calls don't count.
continue;
} else if (call.getParentCall() == null) {
count++;
}
Bundle extras = call.getExtras();
if (extras != null) {
if (extras.getBoolean(Connection.EXTRA_DISABLE_ADD_CALL, false)) {
return false;
}
}
// We do not check states for canAddCall. We treat disconnected calls the same
// and wait until they are removed instead. If we didn't count disconnected calls,
// we could put InCallServices into a state where they are showing two calls but
// also support add-call. Technically it's right, but overall looks better (UI-wise)
// and acts better if we wait until the call is removed.
if (count >= MAXIMUM_TOP_LEVEL_CALLS) {
return false;
}
}
return true;
}
@VisibleForTesting
public Call getRingingCall() {
return getFirstCallWithState(CallState.RINGING);
}
public Call getActiveCall() {
return getFirstCallWithState(CallState.ACTIVE);
}
Call getDialingCall() {
return getFirstCallWithState(CallState.DIALING);
}
@VisibleForTesting
public Call getHeldCall() {
return getFirstCallWithState(CallState.ON_HOLD);
}
public Call getHeldCallByConnectionService(ConnectionServiceWrapper connSvr) {
Optional<Call> heldCall = mCalls.stream()
.filter(call -> call.getConnectionService() == connSvr
&& call.getState() == CallState.ON_HOLD)
.findFirst();
return heldCall.isPresent() ? heldCall.get() : null;
}
@VisibleForTesting
public int getNumHeldCalls() {
int count = 0;
for (Call call : mCalls) {
if (call.getParentCall() == null && call.getState() == CallState.ON_HOLD) {
count++;
}
}
return count;
}
@VisibleForTesting
public Call getOutgoingCall() {
return getFirstCallWithState(OUTGOING_CALL_STATES);
}
@VisibleForTesting
public Call getFirstCallWithState(int... states) {
return getFirstCallWithState(null, states);
}
@VisibleForTesting
public PhoneNumberUtilsAdapter getPhoneNumberUtilsAdapter() {
return mPhoneNumberUtilsAdapter;
}
/**
* Returns the first call that it finds with the given states. The states are treated as having
* priority order so that any call with the first state will be returned before any call with
* states listed later in the parameter list.
*
* @param callToSkip Call that this method should skip while searching
*/
Call getFirstCallWithState(Call callToSkip, int... states) {
for (int currentState : states) {
// check the foreground first
Call foregroundCall = getForegroundCall();
if (foregroundCall != null && foregroundCall.getState() == currentState) {
return foregroundCall;
}
for (Call call : mCalls) {
if (Objects.equals(callToSkip, call)) {
continue;
}
// Only operate on top-level calls
if (call.getParentCall() != null) {
continue;
}
if (call.isExternalCall()) {
continue;
}
if (currentState == call.getState()) {
return call;
}
}
}
return null;
}
Call createConferenceCall(
String callId,
PhoneAccountHandle phoneAccount,
ParcelableConference parcelableConference) {
// If the parceled conference specifies a connect time, use it; otherwise default to 0,
// which is the default value for new Calls.
long connectTime =
parcelableConference.getConnectTimeMillis() ==
Conference.CONNECT_TIME_NOT_SPECIFIED ? 0 :
parcelableConference.getConnectTimeMillis();
long connectElapsedTime =
parcelableConference.getConnectElapsedTimeMillis() ==
Conference.CONNECT_TIME_NOT_SPECIFIED ? 0 :
parcelableConference.getConnectElapsedTimeMillis();
Call call = new Call(
callId,
mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
null /* handle */,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccount,
Call.CALL_DIRECTION_UNDEFINED /* callDirection */,
false /* forceAttachToExistingConnection */,
true /* isConference */,
connectTime,
connectElapsedTime,
mClockProxy);
setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()),
"new conference call");
call.setConnectionCapabilities(parcelableConference.getConnectionCapabilities());
call.setConnectionProperties(parcelableConference.getConnectionProperties());
call.setVideoState(parcelableConference.getVideoState());
call.setVideoProvider(parcelableConference.getVideoProvider());
call.setStatusHints(parcelableConference.getStatusHints());
call.putExtras(Call.SOURCE_CONNECTION_SERVICE, parcelableConference.getExtras());
// In case this Conference was added via a ConnectionManager, keep track of the original
// Connection ID as created by the originating ConnectionService.
Bundle extras = parcelableConference.getExtras();
if (extras != null && extras.containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
call.setOriginalConnectionId(extras.getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID));
}
// TODO: Move this to be a part of addCall()
call.addListener(this);
addCall(call);
return call;
}
/**
* @return the call state currently tracked by {@link PhoneStateBroadcaster}
*/
int getCallState() {
return mPhoneStateBroadcaster.getCallState();
}
/**
* Retrieves the {@link PhoneAccountRegistrar}.
*
* @return The {@link PhoneAccountRegistrar}.
*/
PhoneAccountRegistrar getPhoneAccountRegistrar() {
return mPhoneAccountRegistrar;
}
/**
* Retrieves the {@link MissedCallNotifier}
* @return The {@link MissedCallNotifier}.
*/
MissedCallNotifier getMissedCallNotifier() {
return mMissedCallNotifier;
}
/**
* Retrieves the {@link IncomingCallNotifier}.
* @return The {@link IncomingCallNotifier}.
*/
IncomingCallNotifier getIncomingCallNotifier() {
return mIncomingCallNotifier;
}
/**
* Reject an incoming call and manually add it to the Call Log.
* @param incomingCall Incoming call that has been rejected
*/
private void rejectCallAndLog(Call incomingCall) {
if (incomingCall.getConnectionService() != null) {
// Only reject the call if it has not already been destroyed. If a call ends while
// incoming call filtering is taking place, it is possible that the call has already
// been destroyed, and as such it will be impossible to send the reject to the
// associated ConnectionService.
incomingCall.reject(false, null);
} else {
Log.i(this, "rejectCallAndLog - call already destroyed.");
}
// Since the call was not added to the list of calls, we have to call the missed
// call notifier and the call logger manually.
// Do we need missed call notification for direct to Voicemail calls?
mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE,
true /*showNotificationForMissedCall*/);
}
/**
* Adds the specified call to the main list of live calls.
*
* @param call The call to add.
*/
@VisibleForTesting
public void addCall(Call call) {
Trace.beginSection("addCall");
Log.v(this, "addCall(%s)", call);
call.addListener(this);
mCalls.add(call);
// Specifies the time telecom finished routing the call. This is used by the dialer for
// analytics.
Bundle extras = call.getIntentExtras();
extras.putLong(TelecomManager.EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS,
SystemClock.elapsedRealtime());
updateCanAddCall();
// onCallAdded for calls which immediately take the foreground (like the first call).
for (CallsManagerListener listener : mListeners) {
if (LogUtils.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " addCall");
}
listener.onCallAdded(call);
if (LogUtils.SYSTRACE_DEBUG) {
Trace.endSection();
}
}
Trace.endSection();
}
private void removeCall(Call call) {
Trace.beginSection("removeCall");
Log.v(this, "removeCall(%s)", call);
call.setParentAndChildCall(null); // clean up parent relationship before destroying.
call.removeListener(this);
call.clearConnectionService();
// TODO: clean up RTT pipes
boolean shouldNotify = false;
if (mCalls.contains(call)) {
mCalls.remove(call);
shouldNotify = true;
}
call.destroy();
// Only broadcast changes for calls that are being tracked.
if (shouldNotify) {
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
if (LogUtils.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " onCallRemoved");
}
listener.onCallRemoved(call);
if (LogUtils.SYSTRACE_DEBUG) {
Trace.endSection();
}
}
}
Trace.endSection();
}
/**
* Sets the specified state on the specified call.
*
* @param call The call.
* @param newState The new state of the call.
*/
private void setCallState(Call call, int newState, String tag) {
if (call == null) {
return;
}
int oldState = call.getState();
Log.i(this, "setCallState %s -> %s, call: %s", CallState.toString(oldState),
CallState.toString(newState), call);
if (newState != oldState) {
// If the call switches to held state while a DTMF tone is playing, stop the tone to
// ensure that the tone generator stops playing the tone.
if (newState == CallState.ON_HOLD && call.isDtmfTonePlaying()) {
stopDtmfTone(call);
}
// Unfortunately, in the telephony world the radio is king. So if the call notifies
// us that the call is in a particular state, we allow it even if it doesn't make
// sense (e.g., STATE_ACTIVE -> STATE_RINGING).
// TODO: Consider putting a stop to the above and turning CallState
// into a well-defined state machine.
// TODO: Define expected state transitions here, and log when an
// unexpected transition occurs.
call.setState(newState, tag);
maybeShowErrorDialogOnDisconnect(call);
Trace.beginSection("onCallStateChanged");
maybeHandleHandover(call, newState);
// Only broadcast state change for calls that are being tracked.
if (mCalls.contains(call)) {
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
if (LogUtils.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " onCallStateChanged");
}
listener.onCallStateChanged(call, oldState, newState);
if (LogUtils.SYSTRACE_DEBUG) {
Trace.endSection();
}
}
}
Trace.endSection();
}
}
/**
* Identifies call state transitions for a call which trigger handover events.
* - If this call has a handover to it which just started and this call goes active, treat
* this as if the user accepted the handover.
* - If this call has a handover to it which just started and this call is disconnected, treat
* this as if the user rejected the handover.
* - If this call has a handover from it which just started and this call is disconnected, do
* nothing as the call prematurely disconnected before the user accepted the handover.
* - If this call has a handover from it which was already accepted by the user and this call is
* disconnected, mark the handover as complete.
*
* @param call A call whose state is changing.
* @param newState The new state of the call.
*/
private void maybeHandleHandover(Call call, int newState) {
if (call.getHandoverSourceCall() != null) {
// We are handing over another call to this one.
if (call.getHandoverState() == HandoverState.HANDOVER_TO_STARTED) {
// A handover to this call has just been initiated.
if (newState == CallState.ACTIVE) {
// This call went active, so the user has accepted the handover.
Log.i(this, "setCallState: handover to accepted");
acceptHandoverTo(call);
} else if (newState == CallState.DISCONNECTED) {
// The call was disconnected, so the user has rejected the handover.
Log.i(this, "setCallState: handover to rejected");
rejectHandoverTo(call);
}
}
// If this call was disconnected because it was handed over TO another call, report the
// handover as complete.
} else if (call.getHandoverDestinationCall() != null
&& newState == CallState.DISCONNECTED) {
int handoverState = call.getHandoverState();
if (handoverState == HandoverState.HANDOVER_FROM_STARTED) {
// Disconnect before handover was accepted.
Log.i(this, "setCallState: disconnect before handover accepted");
// Let the handover destination know that the source has disconnected prior to
// completion of the handover.
call.getHandoverDestinationCall().sendCallEvent(
android.telecom.Call.EVENT_HANDOVER_SOURCE_DISCONNECTED, null);
} else if (handoverState == HandoverState.HANDOVER_ACCEPTED) {
Log.i(this, "setCallState: handover from complete");
completeHandoverFrom(call);
}
}
}
private void completeHandoverFrom(Call call) {
Call handoverTo = call.getHandoverDestinationCall();
Log.addEvent(handoverTo, LogUtils.Events.HANDOVER_COMPLETE, "from=%s, to=%s",
call.getId(), handoverTo.getId());
Log.addEvent(call, LogUtils.Events.HANDOVER_COMPLETE, "from=%s, to=%s",
call.getId(), handoverTo.getId());
// Inform the "from" Call (ie the source call) that the handover from it has
// completed; this allows the InCallService to be notified that a handover it
// initiated completed.
call.onConnectionEvent(Connection.EVENT_HANDOVER_COMPLETE, null);
call.onHandoverComplete();
// Inform the "to" ConnectionService that handover to it has completed.
handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_COMPLETE, null);
handoverTo.onHandoverComplete();
answerCall(handoverTo, handoverTo.getVideoState());
call.markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
// If the call we handed over to is self-managed, we need to disconnect the calls for other
// ConnectionServices.
if (handoverTo.isSelfManaged()) {
disconnectOtherCalls(handoverTo.getTargetPhoneAccount());
}
}
private void rejectHandoverTo(Call handoverTo) {
Call handoverFrom = handoverTo.getHandoverSourceCall();
Log.i(this, "rejectHandoverTo: from=%s, to=%s", handoverFrom.getId(), handoverTo.getId());
Log.addEvent(handoverFrom, LogUtils.Events.HANDOVER_FAILED, "from=%s, to=%s, rejected",
handoverTo.getId(), handoverFrom.getId());
Log.addEvent(handoverTo, LogUtils.Events.HANDOVER_FAILED, "from=%s, to=%s, rejected",
handoverTo.getId(), handoverFrom.getId());
// Inform the "from" Call (ie the source call) that the handover from it has
// failed; this allows the InCallService to be notified that a handover it
// initiated failed.
handoverFrom.onConnectionEvent(Connection.EVENT_HANDOVER_FAILED, null);
handoverFrom.onHandoverFailed(android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
// Inform the "to" ConnectionService that handover to it has failed. This
// allows the ConnectionService the call was being handed over
if (handoverTo.getConnectionService() != null) {
// Only attempt if the call has a bound ConnectionService if handover failed
// early on in the handover process, the CS will be unbound and we won't be
// able to send the call event.
handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
handoverTo.getConnectionService().handoverFailed(handoverTo,
android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
}
handoverTo.markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_FAILED);
}
private void acceptHandoverTo(Call handoverTo) {
Call handoverFrom = handoverTo.getHandoverSourceCall();
Log.i(this, "acceptHandoverTo: from=%s, to=%s", handoverFrom.getId(), handoverTo.getId());
handoverTo.setHandoverState(HandoverState.HANDOVER_ACCEPTED);
handoverTo.onHandoverComplete();
handoverFrom.setHandoverState(HandoverState.HANDOVER_ACCEPTED);
handoverFrom.onHandoverComplete();
Log.addEvent(handoverTo, LogUtils.Events.ACCEPT_HANDOVER, "from=%s, to=%s",
handoverFrom.getId(), handoverTo.getId());
Log.addEvent(handoverFrom, LogUtils.Events.ACCEPT_HANDOVER, "from=%s, to=%s",
handoverFrom.getId(), handoverTo.getId());
// Disconnect the call we handed over from.
disconnectCall(handoverFrom);
// If we handed over to a self-managed ConnectionService, we need to disconnect calls for
// other ConnectionServices.
if (handoverTo.isSelfManaged()) {
disconnectOtherCalls(handoverTo.getTargetPhoneAccount());
}
}
private void updateCanAddCall() {
boolean newCanAddCall = canAddCall();
if (newCanAddCall != mCanAddCall) {
mCanAddCall = newCanAddCall;
for (CallsManagerListener listener : mListeners) {
if (LogUtils.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " updateCanAddCall");
}
listener.onCanAddCallChanged(mCanAddCall);
if (LogUtils.SYSTRACE_DEBUG) {
Trace.endSection();
}
}
}
}
private boolean isPotentialMMICode(Uri handle) {
return (handle != null && handle.getSchemeSpecificPart() != null
&& handle.getSchemeSpecificPart().contains("#"));
}
/**
* Determines if a dialed number is potentially an In-Call MMI code. In-Call MMI codes are
* MMI codes which can be dialed when one or more calls are in progress.
* <P>
* Checks for numbers formatted similar to the MMI codes defined in:
* {@link com.android.internal.telephony.Phone#handleInCallMmiCommands(String)}
*
* @param handle The URI to call.
* @return {@code True} if the URI represents a number which could be an in-call MMI code.
*/
private boolean isPotentialInCallMMICode(Uri handle) {
if (handle != null && handle.getSchemeSpecificPart() != null &&
handle.getScheme() != null &&
handle.getScheme().equals(PhoneAccount.SCHEME_TEL)) {
String dialedNumber = handle.getSchemeSpecificPart();
return (dialedNumber.equals("0") ||
(dialedNumber.startsWith("1") && dialedNumber.length() <= 2) ||
(dialedNumber.startsWith("2") && dialedNumber.length() <= 2) ||
dialedNumber.equals("3") ||
dialedNumber.equals("4") ||
dialedNumber.equals("5"));
}
return false;
}
@VisibleForTesting
public int getNumCallsWithState(final boolean isSelfManaged, Call excludeCall,
PhoneAccountHandle phoneAccountHandle, int... states) {
return getNumCallsWithState(isSelfManaged ? CALL_FILTER_SELF_MANAGED : CALL_FILTER_MANAGED,
excludeCall, phoneAccountHandle, states);
}
/**
* Determines the number of calls matching the specified criteria.
* @param callFilter indicates whether to include just managed calls
* ({@link #CALL_FILTER_MANAGED}), self-managed calls
* ({@link #CALL_FILTER_SELF_MANAGED}), or all calls
* ({@link #CALL_FILTER_ALL}).
* @param excludeCall Where {@code non-null}, this call is excluded from the count.
* @param phoneAccountHandle Where {@code non-null}, calls for this {@link PhoneAccountHandle}
* are excluded from the count.
* @param states The list of {@link CallState}s to include in the count.
* @return Count of calls matching criteria.
*/
@VisibleForTesting
public int getNumCallsWithState(final int callFilter, Call excludeCall,
PhoneAccountHandle phoneAccountHandle, int... states) {
Set<Integer> desiredStates = IntStream.of(states).boxed().collect(Collectors.toSet());
Stream<Call> callsStream = mCalls.stream()
.filter(call -> desiredStates.contains(call.getState()) &&
call.getParentCall() == null && !call.isExternalCall());
if (callFilter == CALL_FILTER_MANAGED) {
callsStream = callsStream.filter(call -> !call.isSelfManaged());
} else if (callFilter == CALL_FILTER_SELF_MANAGED) {
callsStream = callsStream.filter(call -> call.isSelfManaged());
}
// If a call to exclude was specified, filter it out.
if (excludeCall != null) {
callsStream = callsStream.filter(call -> call != excludeCall);
}
// If a phone account handle was specified, only consider calls for that phone account.
if (phoneAccountHandle != null) {
callsStream = callsStream.filter(
call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
}
return (int) callsStream.count();
}
private boolean hasMaximumLiveCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
exceptCall, null /* phoneAccountHandle*/, LIVE_CALL_STATES);
}
private boolean hasMaximumManagedLiveCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(false /* isSelfManaged */,
exceptCall, null /* phoneAccountHandle */, LIVE_CALL_STATES);
}
private boolean hasMaximumSelfManagedCalls(Call exceptCall,
PhoneAccountHandle phoneAccountHandle) {
return MAXIMUM_SELF_MANAGED_CALLS <= getNumCallsWithState(true /* isSelfManaged */,
exceptCall, phoneAccountHandle, ANY_CALL_STATE);
}
private boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
null /* phoneAccountHandle */, CallState.ON_HOLD);
}
private boolean hasMaximumManagedRingingCalls(Call exceptCall) {
return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
null /* phoneAccountHandle */, CallState.RINGING);
}
private boolean hasMaximumSelfManagedRingingCalls(Call exceptCall,
PhoneAccountHandle phoneAccountHandle) {
return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(true /* isSelfManaged */, exceptCall,
phoneAccountHandle, CallState.RINGING);
}
private boolean hasMaximumOutgoingCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
exceptCall, null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
}
private boolean hasMaximumManagedOutgoingCalls(Call exceptCall) {
return MAXIMUM_OUTGOING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
}
private boolean hasMaximumManagedDialingCalls(Call exceptCall) {
return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
null /* phoneAccountHandle */, CallState.DIALING, CallState.PULLING);
}
/**
* Given a {@link PhoneAccountHandle} determines if there are calls owned by any other
* {@link PhoneAccountHandle}.
* @param phoneAccountHandle The {@link PhoneAccountHandle} to check.
* @return {@code true} if there are other calls, {@code false} otherwise.
*/
public boolean hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
return getNumCallsForOtherPhoneAccount(phoneAccountHandle) > 0;
}
/**
* Determines the number of calls present for PhoneAccounts other than the one specified.
* @param phoneAccountHandle The handle of the PhoneAccount.
* @return Number of calls owned by other PhoneAccounts.
*/
public int getNumCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
return (int) mCalls.stream().filter(call ->
!phoneAccountHandle.equals(call.getTargetPhoneAccount()) &&
call.getParentCall() == null &&
!call.isExternalCall()).count();
}
/**
* Determines if there are any managed calls.
* @return {@code true} if there are managed calls, {@code false} otherwise.
*/
public boolean hasManagedCalls() {
return mCalls.stream().filter(call -> !call.isSelfManaged() &&
!call.isExternalCall()).count() > 0;
}
/**
* Determines if there are any self-managed calls.
* @return {@code true} if there are self-managed calls, {@code false} otherwise.
*/
public boolean hasSelfManagedCalls() {
return mCalls.stream().filter(call -> call.isSelfManaged()).count() > 0;
}
/**
* Determines if there are any ongoing managed or self-managed calls.
* Note: The {@link #ONGOING_CALL_STATES} are
* @return {@code true} if there are ongoing managed or self-managed calls, {@code false}
* otherwise.
*/
public boolean hasOngoingCalls() {
return getNumCallsWithState(
CALL_FILTER_ALL, null /* excludeCall */,
null /* phoneAccountHandle */,
ONGOING_CALL_STATES) > 0;
}
/**
* Determines if there are any ongoing managed calls.
* @return {@code true} if there are ongoing managed calls, {@code false} otherwise.
*/
public boolean hasOngoingManagedCalls() {
return getNumCallsWithState(
CALL_FILTER_MANAGED, null /* excludeCall */,
null /* phoneAccountHandle */,
ONGOING_CALL_STATES) > 0;
}
/**
* Determines if the system incoming call UI should be shown.
* The system incoming call UI will be shown if the new incoming call is self-managed, and there
* are ongoing calls for another PhoneAccount.
* @param incomingCall The incoming call.
* @return {@code true} if the system incoming call UI should be shown, {@code false} otherwise.
*/
public boolean shouldShowSystemIncomingCallUi(Call incomingCall) {
return incomingCall.isIncoming() && incomingCall.isSelfManaged() &&
hasCallsForOtherPhoneAccount(incomingCall.getTargetPhoneAccount()) &&
incomingCall.getHandoverSourceCall() == null;
}
private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
if (hasMaximumLiveCalls(call)) {
// NOTE: If the amount of live calls changes beyond 1, this logic will probably
// have to change.
Call liveCall = getFirstCallWithState(LIVE_CALL_STATES);
Log.i(this, "makeRoomForOutgoingCall call = " + call + " livecall = " +
liveCall);
if (call == liveCall) {
// If the call is already the foreground call, then we are golden.
// This can happen after the user selects an account in the SELECT_PHONE_ACCOUNT
// state since the call was already populated into the list.
return true;
}
if (hasMaximumOutgoingCalls(call)) {
Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
if (isEmergency && !outgoingCall.isEmergencyCall()) {
// Disconnect the current outgoing call if it's not an emergency call. If the
// user tries to make two outgoing calls to different emergency call numbers,
// we will try to connect the first outgoing call.
call.getAnalytics().setCallIsAdditional(true);
outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect();
return true;
}
if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
// If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
// state, just disconnect it since the user has explicitly started a new call.
call.getAnalytics().setCallIsAdditional(true);
outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect();
return true;
}
return false;
}
// If we have the max number of held managed calls and we're placing an emergency call,
// we'll disconnect the ongoing call if it cannot be held.
if (hasMaximumManagedHoldingCalls(call) && isEmergency && !canHold(liveCall)) {
call.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.disconnect("disconnecting to make room for emergency call "
+ call.getId());
return true;
}
// TODO: Remove once b/23035408 has been corrected.
// If the live call is a conference, it will not have a target phone account set. This
// means the check to see if the live call has the same target phone account as the new
// call will not cause us to bail early. As a result, we'll end up holding the
// ongoing conference call. However, the ConnectionService is already doing that. This
// has caused problems with some carriers. As a workaround until b/23035408 is
// corrected, we will try and get the target phone account for one of the conference's
// children and use that instead.
PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
if (liveCallPhoneAccount == null && liveCall.isConference() &&
!liveCall.getChildCalls().isEmpty()) {
liveCallPhoneAccount = getFirstChildPhoneAccount(liveCall);
Log.i(this, "makeRoomForOutgoingCall: using child call PhoneAccount = " +
liveCallPhoneAccount);
}
// First thing, if we are trying to make a call with the same phone account as the live
// call, then allow it so that the connection service can make its own decision about
// how to handle the new call relative to the current one.
if (Objects.equals(liveCallPhoneAccount, call.getTargetPhoneAccount())) {
Log.i(this, "makeRoomForOutgoingCall: phoneAccount matches.");
call.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
return true;
} else if (call.getTargetPhoneAccount() == null) {
// Without a phone account, we can't say reliably that the call will fail.
// If the user chooses the same phone account as the live call, then it's
// still possible that the call can be made (like with CDMA calls not supporting
// hold but they still support adding a call by going immediately into conference
// mode). Return true here and we'll run this code again after user chooses an
// account.
return true;
}
// Try to hold the live call before attempting the new outgoing call.
if (canHold(liveCall)) {
Log.i(this, "makeRoomForOutgoingCall: holding live call.");
call.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.hold("calling " + call.getId());
return true;
}
// The live call cannot be held so we're out of luck here. There's no room.
return false;
}
return true;
}
/**
* Given a call, find the first non-null phone account handle of its children.
*
* @param parentCall The parent call.
* @return The first non-null phone account handle of the children, or {@code null} if none.
*/
private PhoneAccountHandle getFirstChildPhoneAccount(Call parentCall) {
for (Call childCall : parentCall.getChildCalls()) {
PhoneAccountHandle childPhoneAccount = childCall.getTargetPhoneAccount();
if (childPhoneAccount != null) {
return childPhoneAccount;
}
}
return null;
}
/**
* Checks to see if the call should be on speakerphone and if so, set it.
*/
private void maybeMoveToSpeakerPhone(Call call) {
if (call.isHandoverInProgress() && call.getState() == CallState.DIALING) {
// When a new outgoing call is initiated for the purpose of handing over, do not engage
// speaker automatically until the call goes active.
return;
}
if (call.getStartWithSpeakerphoneOn()) {
setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
call.setStartWithSpeakerphoneOn(false);
}
}
/**
* Checks to see if the call is an emergency call and if so, turn off mute.
*/
private void maybeTurnOffMute(Call call) {
if (call.isEmergencyCall()) {
mute(false);
}
}
private void ensureCallAudible() {
AudioManager am = mContext.getSystemService(AudioManager.class);
if (am == null) {
Log.w(this, "ensureCallAudible: audio manager is null");
return;
}
if (am.getStreamVolume(AudioManager.STREAM_VOICE_CALL) == 0) {
Log.i(this, "ensureCallAudible: voice call stream has volume 0. Adjusting to default.");
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
AudioSystem.getDefaultStreamVolume(AudioManager.STREAM_VOICE_CALL), 0);
}
}
/**
* Creates a new call for an existing connection.
*
* @param callId The id of the new call.
* @param connection The connection information.
* @return The new call.
*/
Call createCallForExistingConnection(String callId, ParcelableConnection connection) {
boolean isDowngradedConference = (connection.getConnectionProperties()
& Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0;
Call call = new Call(
callId,
mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
connection.getHandle() /* handle */,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
connection.getPhoneAccount(), /* targetPhoneAccountHandle */
Call.CALL_DIRECTION_UNDEFINED /* callDirection */,
false /* forceAttachToExistingConnection */,
isDowngradedConference /* isConference */,
connection.getConnectTimeMillis() /* connectTimeMillis */,
connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */
mClockProxy);
call.initAnalytics();
call.getAnalytics().setCreatedFromExistingConnection(true);
setCallState(call, Call.getStateFromConnectionState(connection.getState()),
"existing connection");
call.setConnectionCapabilities(connection.getConnectionCapabilities());
call.setConnectionProperties(connection.getConnectionProperties());
call.setHandle(connection.getHandle(), connection.getHandlePresentation());
call.setCallerDisplayName(connection.getCallerDisplayName(),
connection.getCallerDisplayNamePresentation());
call.addListener(this);
// In case this connection was added via a ConnectionManager, keep track of the original
// Connection ID as created by the originating ConnectionService.
Bundle extras = connection.getExtras();
if (extras != null && extras.containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
call.setOriginalConnectionId(extras.getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID));
}
Log.i(this, "createCallForExistingConnection: %s", connection);
Call parentCall = null;
if (!TextUtils.isEmpty(connection.getParentCallId())) {
String parentId = connection.getParentCallId();
parentCall = mCalls
.stream()
.filter(c -> c.getId().equals(parentId))
.findFirst()
.orElse(null);
if (parentCall != null) {
Log.i(this, "createCallForExistingConnection: %s added as child of %s.",
call.getId(),
parentCall.getId());
// Set JUST the parent property, which won't send an update to the Incall UI.
call.setParentCall(parentCall);
}
}
addCall(call);
if (parentCall != null) {
// Now, set the call as a child of the parent since it has been added to Telecom. This
// is where we will inform InCall.
call.setChildOf(parentCall);
call.notifyParentChanged(parentCall);
}
return call;
}
/**
* Determines whether Telecom already knows about a Connection added via the
* {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
* Connection)} API via a ConnectionManager.
*
* See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID}.
* @param originalConnectionId The new connection ID to check.
* @return {@code true} if this connection is already known by Telecom.
*/
Call getAlreadyAddedConnection(String originalConnectionId) {
Optional<Call> existingCall = mCalls.stream()
.filter(call -> originalConnectionId.equals(call.getOriginalConnectionId()) ||
originalConnectionId.equals(call.getId()))
.findFirst();
if (existingCall.isPresent()) {
Log.i(this, "isExistingConnectionAlreadyAdded - call %s already added with id %s",
originalConnectionId, existingCall.get().getId());
return existingCall.get();
}
return null;
}
/**
* @return A new unique telecom call Id.
*/
private String getNextCallId() {
synchronized(mLock) {
return TELECOM_CALL_ID_PREFIX + (++mCallId);
}
}
public int getNextRttRequestId() {
synchronized (mLock) {
return (++mRttRequestId);
}
}
/**
* Callback when foreground user is switched. We will reload missed call in all profiles
* including the user itself. There may be chances that profiles are not started yet.
*/
@VisibleForTesting
public void onUserSwitch(UserHandle userHandle) {
mCurrentUserHandle = userHandle;
mMissedCallNotifier.setCurrentUserHandle(userHandle);
final UserManager userManager = UserManager.get(mContext);
List<UserInfo> profiles = userManager.getEnabledProfiles(userHandle.getIdentifier());
for (UserInfo profile : profiles) {
reloadMissedCallsOfUser(profile.getUserHandle());
}
}
/**
* Because there may be chances that profiles are not started yet though its parent user is
* switched, we reload missed calls of profile that are just started here.
*/
void onUserStarting(UserHandle userHandle) {
if (UserUtil.isProfile(mContext, userHandle)) {
reloadMissedCallsOfUser(userHandle);
}
}
public TelecomSystem.SyncRoot getLock() {
return mLock;
}
private void reloadMissedCallsOfUser(UserHandle userHandle) {
mMissedCallNotifier.reloadFromDatabase(mCallerInfoLookupHelper,
new MissedCallNotifier.CallInfoFactory(), userHandle);
}
public void onBootCompleted() {
mMissedCallNotifier.reloadAfterBootComplete(mCallerInfoLookupHelper,
new MissedCallNotifier.CallInfoFactory());
}
public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
return isIncomingCallPermitted(null /* excludeCall */, phoneAccountHandle);
}
public boolean isIncomingCallPermitted(Call excludeCall,
PhoneAccountHandle phoneAccountHandle) {
if (phoneAccountHandle == null) {
return false;
}
PhoneAccount phoneAccount =
mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
if (phoneAccount == null) {
return false;
}
if (!phoneAccount.isSelfManaged()) {
return !hasMaximumManagedRingingCalls(excludeCall) &&
!hasMaximumManagedHoldingCalls(excludeCall);
} else {
return !hasEmergencyCall() &&
!hasMaximumSelfManagedRingingCalls(excludeCall, phoneAccountHandle) &&
!hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle);
}
}
public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
return isOutgoingCallPermitted(null /* excludeCall */, phoneAccountHandle);
}
public boolean isOutgoingCallPermitted(Call excludeCall,
PhoneAccountHandle phoneAccountHandle) {
if (phoneAccountHandle == null) {
return false;
}
PhoneAccount phoneAccount =
mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
if (phoneAccount == null) {
return false;
}
if (!phoneAccount.isSelfManaged()) {
return !hasMaximumManagedOutgoingCalls(excludeCall) &&
!hasMaximumManagedDialingCalls(excludeCall) &&
!hasMaximumManagedLiveCalls(excludeCall) &&
!hasMaximumManagedHoldingCalls(excludeCall);
} else {
// Only permit self-managed outgoing calls if
// 1. there is no emergency ongoing call
// 2. The outgoing call is an handover call or it not hit the self-managed call limit
// and the current active call can be held.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
return !hasEmergencyCall() &&
((excludeCall != null && excludeCall.getHandoverSourceCall() != null) ||
(!hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
(activeCall == null || canHold(activeCall))));
}
}
public boolean isReplyWithSmsAllowed(int uid) {
UserHandle callingUser = UserHandle.of(UserHandle.getUserId(uid));
UserManager userManager = mContext.getSystemService(UserManager.class);
KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
boolean isUserRestricted = userManager != null
&& userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
boolean isLockscreenRestricted = keyguardManager != null
&& keyguardManager.isDeviceLocked();
Log.d(this, "isReplyWithSmsAllowed: isUserRestricted: %s, isLockscreenRestricted: %s",
isUserRestricted, isLockscreenRestricted);
// TODO(hallliu): actually check the lockscreen once b/77731473 is fixed
return !isUserRestricted;
}
/**
* Blocks execution until all Telecom handlers have completed their current work.
*/
public void waitOnHandlers() {
CountDownLatch mainHandlerLatch = new CountDownLatch(3);
mHandler.post(() -> {
mainHandlerLatch.countDown();
});
mCallAudioManager.getCallAudioModeStateMachine().getHandler().post(() -> {
mainHandlerLatch.countDown();
});
mCallAudioManager.getCallAudioRouteStateMachine().getHandler().post(() -> {
mainHandlerLatch.countDown();
});
try {
mainHandlerLatch.await(HANDLER_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.w(this, "waitOnHandlers: interrupted %s", e);
}
}
/**
* Used to confirm creation of an outgoing call which was marked as pending confirmation in
* {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)}.
* Called via {@link TelecomBroadcastIntentProcessor} for a call which was confirmed via
* {@link ConfirmCallDialogActivity}.
* @param callId The call ID of the call to confirm.
*/
public void confirmPendingCall(String callId) {
Log.i(this, "confirmPendingCall: callId=%s", callId);
if (mPendingCall != null && mPendingCall.getId().equals(callId)) {
Log.addEvent(mPendingCall, LogUtils.Events.USER_CONFIRMED);
addCall(mPendingCall);
// We are going to place the new outgoing call, so disconnect any ongoing self-managed
// calls which are ongoing at this time.
disconnectSelfManagedCalls("outgoing call " + callId);
// Kick of the new outgoing call intent from where it left off prior to confirming the
// call.
CallIntentProcessor.sendNewOutgoingCallIntent(mContext, mPendingCall, this,
mPendingCall.getOriginalCallIntent());
mPendingCall = null;
}
}
/**
* Used to cancel an outgoing call which was marked as pending confirmation in
* {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)}.
* Called via {@link TelecomBroadcastIntentProcessor} for a call which was confirmed via
* {@link ConfirmCallDialogActivity}.
* @param callId The call ID of the call to cancel.
*/
public void cancelPendingCall(String callId) {
Log.i(this, "cancelPendingCall: callId=%s", callId);
if (mPendingCall != null && mPendingCall.getId().equals(callId)) {
Log.addEvent(mPendingCall, LogUtils.Events.USER_CANCELLED);
markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED));
markCallAsRemoved(mPendingCall);
mPendingCall = null;
}
}
/**
* Called from {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent)} when
* a managed call is added while there are ongoing self-managed calls. Starts
* {@link ConfirmCallDialogActivity} to prompt the user to see if they wish to place the
* outgoing call or not.
* @param call The call to confirm.
*/
private void startCallConfirmation(Call call) {
if (mPendingCall != null) {
Log.i(this, "startCallConfirmation: call %s is already pending; disconnecting %s",
mPendingCall.getId(), call.getId());
markCallDisconnectedDueToSelfManagedCall(call);
return;
}
Log.addEvent(call, LogUtils.Events.USER_CONFIRMATION);
mPendingCall = call;
// Figure out the name of the app in charge of the self-managed call(s).
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
if (activeCall != null) {
CharSequence ongoingAppName = activeCall.getTargetPhoneAccountLabel();
Log.i(this, "startCallConfirmation: callId=%s, ongoingApp=%s", call.getId(),
ongoingAppName);
Intent confirmIntent = new Intent(mContext, ConfirmCallDialogActivity.class);
confirmIntent.putExtra(ConfirmCallDialogActivity.EXTRA_OUTGOING_CALL_ID, call.getId());
confirmIntent.putExtra(ConfirmCallDialogActivity.EXTRA_ONGOING_APP_NAME, ongoingAppName);
confirmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(confirmIntent, UserHandle.CURRENT);
}
}
/**
* Disconnects all self-managed calls.
*/
private void disconnectSelfManagedCalls(String reason) {
// Disconnect all self-managed calls to make priority for emergency call.
// Use Call.disconnect() to command the ConnectionService to disconnect the calls.
// CallsManager.markCallAsDisconnected doesn't actually tell the ConnectionService to
// disconnect.
mCalls.stream()
.filter(c -> c.isSelfManaged())
.forEach(c -> c.disconnect(reason));
// When disconnecting all self-managed calls, switch audio routing back to the baseline
// route. This ensures if, for example, the self-managed ConnectionService was routed to
// speakerphone that we'll switch back to earpiece for the managed call which necessitated
// disconnecting the self-managed calls.
mCallAudioManager.switchBaseline();
}
/**
* Dumps the state of the {@link CallsManager}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
*/
public void dump(IndentingPrintWriter pw) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
if (mCalls != null) {
pw.println("mCalls: ");
pw.increaseIndent();
for (Call call : mCalls) {
pw.println(call);
}
pw.decreaseIndent();
}
if (mPendingCall != null) {
pw.print("mPendingCall:");
pw.println(mPendingCall.getId());
}
if (mCallAudioManager != null) {
pw.println("mCallAudioManager:");
pw.increaseIndent();
mCallAudioManager.dump(pw);
pw.decreaseIndent();
}
if (mTtyManager != null) {
pw.println("mTtyManager:");
pw.increaseIndent();
mTtyManager.dump(pw);
pw.decreaseIndent();
}
if (mInCallController != null) {
pw.println("mInCallController:");
pw.increaseIndent();
mInCallController.dump(pw);
pw.decreaseIndent();
}
if (mDefaultDialerCache != null) {
pw.println("mDefaultDialerCache:");
pw.increaseIndent();
mDefaultDialerCache.dumpCache(pw);
pw.decreaseIndent();
}
if (mConnectionServiceRepository != null) {
pw.println("mConnectionServiceRepository:");
pw.increaseIndent();
mConnectionServiceRepository.dump(pw);
pw.decreaseIndent();
}
}
/**
* For some disconnected causes, we show a dialog when it's a mmi code or potential mmi code.
*
* @param call The call.
*/
private void maybeShowErrorDialogOnDisconnect(Call call) {
if (call.getState() == CallState.DISCONNECTED && (isPotentialMMICode(call.getHandle())
|| isPotentialInCallMMICode(call.getHandle())) && !mCalls.contains(call)) {
DisconnectCause disconnectCause = call.getDisconnectCause();
if (!TextUtils.isEmpty(disconnectCause.getDescription()) && (disconnectCause.getCode()
== DisconnectCause.ERROR)) {
Intent errorIntent = new Intent(mContext, ErrorDialogActivity.class);
errorIntent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_STRING_EXTRA,
disconnectCause.getDescription());
errorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(errorIntent, UserHandle.CURRENT);
}
}
}
private void setIntentExtrasAndStartTime(Call call, Bundle extras) {
if (extras != null) {
// Create our own instance to modify (since extras may be Bundle.EMPTY)
extras = new Bundle(extras);
} else {
extras = new Bundle();
}
// Specifies the time telecom began routing the call. This is used by the dialer for
// analytics.
extras.putLong(TelecomManager.EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS,
SystemClock.elapsedRealtime());
call.setIntentExtras(extras);
}
/**
* Notifies the {@link android.telecom.ConnectionService} associated with a
* {@link PhoneAccountHandle} that the attempt to create a new connection has failed.
*
* @param phoneAccountHandle The {@link PhoneAccountHandle}.
* @param call The {@link Call} which could not be added.
*/
private void notifyCreateConnectionFailed(PhoneAccountHandle phoneAccountHandle, Call call) {
if (phoneAccountHandle == null) {
return;
}
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
if (service == null) {
Log.i(this, "Found no connection service.");
return;
} else {
call.setConnectionService(service);
service.createConnectionFailed(call);
}
}
/**
* Notifies the {@link android.telecom.ConnectionService} associated with a
* {@link PhoneAccountHandle} that the attempt to handover a call has failed.
*
* @param call The handover call
* @param reason The error reason code for handover failure
*/
private void notifyHandoverFailed(Call call, int reason) {
ConnectionServiceWrapper service = call.getConnectionService();
service.handoverFailed(call, reason);
call.setDisconnectCause(new DisconnectCause(DisconnectCause.CANCELED));
call.disconnect("handover failed");
}
/**
* Called in response to a {@link Call} receiving a {@link Call#sendCallEvent(String, Bundle)}
* of type {@link android.telecom.Call#EVENT_REQUEST_HANDOVER} indicating the
* {@link android.telecom.InCallService} has requested a handover to another
* {@link android.telecom.ConnectionService}.
*
* We will explicitly disallow a handover when there is an emergency call present.
*
* @param handoverFromCall The {@link Call} to be handed over.
* @param handoverToHandle The {@link PhoneAccountHandle} to hand over the call to.
* @param videoState The desired video state of {@link Call} after handover.
* @param initiatingExtras Extras associated with the handover, to be passed to the handover
* {@link android.telecom.ConnectionService}.
*/
private void requestHandoverViaEvents(Call handoverFromCall,
PhoneAccountHandle handoverToHandle,
int videoState, Bundle initiatingExtras) {
boolean isHandoverFromSupported = isHandoverFromPhoneAccountSupported(
handoverFromCall.getTargetPhoneAccount());
boolean isHandoverToSupported = isHandoverToPhoneAccountSupported(handoverToHandle);
if (!isHandoverFromSupported || !isHandoverToSupported || hasEmergencyCall()) {
handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
return;
}
Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, handoverToHandle);
Bundle extras = new Bundle();
extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER, true);
extras.putParcelable(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT,
handoverFromCall.getTargetPhoneAccount());
extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
if (initiatingExtras != null) {
extras.putAll(initiatingExtras);
}
extras.putParcelable(TelecomManager.EXTRA_CALL_AUDIO_STATE,
mCallAudioManager.getCallAudioState());
Call handoverToCall = startOutgoingCall(handoverFromCall.getHandle(), handoverToHandle,
extras, getCurrentUserHandle(), null /* originalIntent */);
Log.addEvent(handoverFromCall, LogUtils.Events.START_HANDOVER,
"handOverFrom=%s, handOverTo=%s", handoverFromCall.getId(), handoverToCall.getId());
handoverFromCall.setHandoverDestinationCall(handoverToCall);
handoverFromCall.setHandoverState(HandoverState.HANDOVER_FROM_STARTED);
handoverToCall.setHandoverState(HandoverState.HANDOVER_TO_STARTED);
handoverToCall.setHandoverSourceCall(handoverFromCall);
handoverToCall.setNewOutgoingCallIntentBroadcastIsDone();
placeOutgoingCall(handoverToCall, handoverToCall.getHandle(), null /* gatewayInfo */,
false /* startwithSpeaker */,
videoState);
}
/**
* Called in response to a {@link Call} receiving a {@link Call#handoverTo(PhoneAccountHandle,
* int, Bundle)} indicating the {@link android.telecom.InCallService} has requested a
* handover to another {@link android.telecom.ConnectionService}.
*
* We will explicitly disallow a handover when there is an emergency call present.
*
* @param handoverFromCall The {@link Call} to be handed over.
* @param handoverToHandle The {@link PhoneAccountHandle} to hand over the call to.
* @param videoState The desired video state of {@link Call} after handover.
* @param extras Extras associated with the handover, to be passed to the handover
* {@link android.telecom.ConnectionService}.
*/
private void requestHandover(Call handoverFromCall, PhoneAccountHandle handoverToHandle,
int videoState, Bundle extras) {
// Send an error back if there are any ongoing emergency calls.
if (hasEmergencyCall()) {
handoverFromCall.onHandoverFailed(
android.telecom.Call.Callback.HANDOVER_FAILURE_ONGOING_EMERGENCY_CALL);
return;
}
// If source and destination phone accounts don't support handover, send an error back.
boolean isHandoverFromSupported = isHandoverFromPhoneAccountSupported(
handoverFromCall.getTargetPhoneAccount());
boolean isHandoverToSupported = isHandoverToPhoneAccountSupported(handoverToHandle);
if (!isHandoverFromSupported || !isHandoverToSupported) {
handoverFromCall.onHandoverFailed(
android.telecom.Call.Callback.HANDOVER_FAILURE_NOT_SUPPORTED);
return;
}
Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, handoverToHandle);
// Create a new instance of Call
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(handoverToHandle, getCurrentUserHandle());
boolean isSelfManaged = account != null && account.isSelfManaged();
Call call = new Call(getNextCallId(), mContext,
this, mLock, mConnectionServiceRepository,
mContactsAsyncHelper, mCallerInfoAsyncQueryFactory, mPhoneNumberUtilsAdapter,
handoverFromCall.getHandle(), null,
null, null,
Call.CALL_DIRECTION_OUTGOING, false,
false, mClockProxy);
call.initAnalytics();
// Set self-managed and voipAudioMode if destination is self-managed CS
call.setIsSelfManaged(isSelfManaged);
if (isSelfManaged) {
call.setIsVoipAudioMode(true);
}
call.setInitiatingUser(getCurrentUserHandle());
// Ensure we don't try to place an outgoing call with video if video is not
// supported.
if (VideoProfile.isVideo(videoState) && account != null &&
!account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
} else {
call.setVideoState(videoState);
}
// Set target phone account to destAcct.
call.setTargetPhoneAccount(handoverToHandle);
if (account != null && account.getExtras() != null && account.getExtras()
.getBoolean(PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE)) {
Log.d(this, "requestHandover: defaulting to voip mode for call %s",
call.getId());
call.setIsVoipAudioMode(true);
}
// Set call state to connecting
call.setState(
CallState.CONNECTING,
handoverToHandle == null ? "no-handle" : handoverToHandle.toString());
// Mark as handover so that the ConnectionService knows this is a handover request.
if (extras == null) {
extras = new Bundle();
}
extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER_CONNECTION, true);
extras.putParcelable(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT,
handoverFromCall.getTargetPhoneAccount());
setIntentExtrasAndStartTime(call, extras);
// Add call to call tracker
if (!mCalls.contains(call)) {
addCall(call);
}
Log.addEvent(handoverFromCall, LogUtils.Events.START_HANDOVER,
"handOverFrom=%s, handOverTo=%s", handoverFromCall.getId(), call.getId());
handoverFromCall.setHandoverDestinationCall(call);
handoverFromCall.setHandoverState(HandoverState.HANDOVER_FROM_STARTED);
call.setHandoverState(HandoverState.HANDOVER_TO_STARTED);
call.setHandoverSourceCall(handoverFromCall);
call.setNewOutgoingCallIntentBroadcastIsDone();
// Auto-enable speakerphone if the originating intent specified to do so, if the call
// is a video call, of if using speaker when docked
final boolean useSpeakerWhenDocked = mContext.getResources().getBoolean(
R.bool.use_speaker_when_docked);
final boolean useSpeakerForDock = isSpeakerphoneEnabledForDock();
final boolean useSpeakerForVideoCall = isSpeakerphoneAutoEnabledForVideoCalls(videoState);
call.setStartWithSpeakerphoneOn(false || useSpeakerForVideoCall
|| (useSpeakerWhenDocked && useSpeakerForDock));
call.setVideoState(videoState);
final boolean isOutgoingCallPermitted = isOutgoingCallPermitted(call,
call.getTargetPhoneAccount());
// If the account has been set, proceed to place the outgoing call.
if (call.isSelfManaged() && !isOutgoingCallPermitted) {
notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
} else if (!call.isSelfManaged() && hasSelfManagedCalls() && !call.isEmergencyCall()) {
markCallDisconnectedDueToSelfManagedCall(call);
} else {
if (call.isEmergencyCall()) {
// Disconnect all self-managed calls to make priority for emergency call.
disconnectSelfManagedCalls("emergency call");
}
call.startCreateConnection(mPhoneAccountRegistrar);
}
}
/**
* Determines if handover from the specified {@link PhoneAccountHandle} is supported.
*
* @param from The {@link PhoneAccountHandle} the handover originates from.
* @return {@code true} if handover is currently allowed, {@code false} otherwise.
*/
private boolean isHandoverFromPhoneAccountSupported(PhoneAccountHandle from) {
return getBooleanPhoneAccountExtra(from, PhoneAccount.EXTRA_SUPPORTS_HANDOVER_FROM);
}
/**
* Determines if handover to the specified {@link PhoneAccountHandle} is supported.
*
* @param to The {@link PhoneAccountHandle} the handover it to.
* @return {@code true} if handover is currently allowed, {@code false} otherwise.
*/
private boolean isHandoverToPhoneAccountSupported(PhoneAccountHandle to) {
return getBooleanPhoneAccountExtra(to, PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO);
}
/**
* Retrieves a boolean phone account extra.
* @param handle the {@link PhoneAccountHandle} to retrieve the extra for.
* @param key The extras key.
* @return {@code true} if the extra {@link PhoneAccount} extra is true, {@code false}
* otherwise.
*/
private boolean getBooleanPhoneAccountExtra(PhoneAccountHandle handle, String key) {
PhoneAccount phoneAccount = getPhoneAccountRegistrar().getPhoneAccountUnchecked(handle);
if (phoneAccount == null) {
return false;
}
Bundle fromExtras = phoneAccount.getExtras();
if (fromExtras == null) {
return false;
}
return fromExtras.getBoolean(key);
}
/**
* Determines if there is an existing handover in process.
* @return {@code true} if a call in the process of handover exists, {@code false} otherwise.
*/
private boolean isHandoverInProgress() {
return mCalls.stream().filter(c -> c.getHandoverSourceCall() != null ||
c.getHandoverDestinationCall() != null).count() > 0;
}
private void broadcastUnregisterIntent(PhoneAccountHandle accountHandle) {
Intent intent =
new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
intent.putExtra(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
Log.i(this, "Sending phone-account %s unregistered intent as user", accountHandle);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION);
String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
getCurrentUserHandle().getIdentifier());
if (!TextUtils.isEmpty(dialerPackage)) {
Intent directedIntent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED)
.setPackage(dialerPackage);
directedIntent.putExtra(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
Log.i(this, "Sending phone-account unregistered intent to default dialer");
mContext.sendBroadcastAsUser(directedIntent, UserHandle.ALL, null);
}
return ;
}
private void broadcastRegisterIntent(PhoneAccountHandle accountHandle) {
Intent intent = new Intent(
TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
accountHandle);
Log.i(this, "Sending phone-account %s registered intent as user", accountHandle);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION);
String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
getCurrentUserHandle().getIdentifier());
if (!TextUtils.isEmpty(dialerPackage)) {
Intent directedIntent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED)
.setPackage(dialerPackage);
directedIntent.putExtra(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
Log.i(this, "Sending phone-account registered intent to default dialer");
mContext.sendBroadcastAsUser(directedIntent, UserHandle.ALL, null);
}
return ;
}
public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct) {
final String handleScheme = srcAddr.getSchemeSpecificPart();
Call fromCall = mCalls.stream()
.filter((c) -> mPhoneNumberUtilsAdapter.isSamePhoneNumber(
(c.getHandle() == null ? null : c.getHandle().getSchemeSpecificPart()),
handleScheme))
.findFirst()
.orElse(null);
Call call = new Call(
getNextCallId(),
mContext,
this,
mLock,
mConnectionServiceRepository,
mContactsAsyncHelper,
mCallerInfoAsyncQueryFactory,
mPhoneNumberUtilsAdapter,
srcAddr,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
destAcct,
Call.CALL_DIRECTION_INCOMING /* callDirection */,
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy);
if (fromCall == null || isHandoverInProgress() ||
!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) ||
!isHandoverToPhoneAccountSupported(destAcct) ||
hasEmergencyCall()) {
Log.w(this, "acceptHandover: Handover not supported");
notifyHandoverFailed(call,
android.telecom.Call.Callback.HANDOVER_FAILURE_NOT_SUPPORTED);
return;
}
PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(destAcct);
if (phoneAccount == null) {
Log.w(this, "acceptHandover: Handover not supported. phoneAccount = null");
notifyHandoverFailed(call,
android.telecom.Call.Callback.HANDOVER_FAILURE_NOT_SUPPORTED);
return;
}
call.setIsSelfManaged(phoneAccount.isSelfManaged());
if (call.isSelfManaged() || (phoneAccount.getExtras() != null &&
phoneAccount.getExtras().getBoolean(
PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE))) {
call.setIsVoipAudioMode(true);
}
if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
} else {
call.setVideoState(videoState);
}
call.initAnalytics();
call.addListener(this);
fromCall.setHandoverDestinationCall(call);
call.setHandoverSourceCall(fromCall);
call.setHandoverState(HandoverState.HANDOVER_TO_STARTED);
fromCall.setHandoverState(HandoverState.HANDOVER_FROM_STARTED);
if (isSpeakerEnabledForVideoCalls() && VideoProfile.isVideo(videoState)) {
// Ensure when the call goes active that it will go to speakerphone if the
// handover to call is a video call.
call.setStartWithSpeakerphoneOn(true);
}
Bundle extras = call.getIntentExtras();
if (extras == null) {
extras = new Bundle();
}
extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER_CONNECTION, true);
extras.putParcelable(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT,
fromCall.getTargetPhoneAccount());
call.startCreateConnection(mPhoneAccountRegistrar);
}
ConnectionServiceFocusManager getConnectionServiceFocusManager() {
return mConnectionSvrFocusMgr;
}
private boolean canHold(Call call) {
return call.can(Connection.CAPABILITY_HOLD);
}
private boolean supportsHold(Call call) {
return call.can(Connection.CAPABILITY_SUPPORT_HOLD);
}
private final class ActionSetCallState implements PendingAction {
private final Call mCall;
private final int mState;
private final String mTag;
ActionSetCallState(Call call, int state, String tag) {
mCall = call;
mState = state;
mTag = tag;
}
@Override
public void performAction() {
Log.d(this, "perform set call state for %s, state = %s", mCall, mState);
setCallState(mCall, mState, mTag);
}
}
private final class ActionUnHoldCall implements PendingAction {
private final Call mCall;
private final String mPreviouslyHeldCallId;
ActionUnHoldCall(Call call, String previouslyHeldCallId) {
mCall = call;
mPreviouslyHeldCallId = previouslyHeldCallId;
}
@Override
public void performAction() {
Log.d(this, "perform unhold call for %s", mCall);
mCall.unhold("held " + mPreviouslyHeldCallId);
}
}
private final class ActionAnswerCall implements PendingAction {
private final Call mCall;
private final int mVideoState;
ActionAnswerCall(Call call, int videoState) {
mCall = call;
mVideoState = videoState;
}
@Override
public void performAction() {
Log.d(this, "perform answer call for %s, videoState = %d", mCall, mVideoState);
for (CallsManagerListener listener : mListeners) {
listener.onIncomingCallAnswered(mCall);
}
// We do not update the UI until we get confirmation of the answer() through
// {@link #markCallAsActive}.
mCall.answer(mVideoState);
if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
mCall.setStartWithSpeakerphoneOn(true);
}
}
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static final class RequestCallback implements
ConnectionServiceFocusManager.RequestFocusCallback {
private PendingAction mPendingAction;
RequestCallback(PendingAction pendingAction) {
mPendingAction = pendingAction;
}
@Override
public void onRequestFocusDone(ConnectionServiceFocusManager.CallFocus call) {
if (mPendingAction != null) {
mPendingAction.performAction();
}
}
}
}