blob: ea45abbb14a31a605d438bd7d9bc9206d6d82398 [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 static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
import static android.telecom.TelecomManager.ACTION_POST_CALL;
import static android.telecom.TelecomManager.DURATION_LONG;
import static android.telecom.TelecomManager.DURATION_MEDIUM;
import static android.telecom.TelecomManager.DURATION_SHORT;
import static android.telecom.TelecomManager.DURATION_VERY_SHORT;
import static android.telecom.TelecomManager.EXTRA_CALL_DURATION;
import static android.telecom.TelecomManager.EXTRA_DISCONNECT_CAUSE;
import static android.telecom.TelecomManager.EXTRA_HANDLE;
import static android.telecom.TelecomManager.MEDIUM_CALL_TIME_MS;
import static android.telecom.TelecomManager.SHORT_CALL_TIME_MS;
import static android.telecom.TelecomManager.VERY_SHORT_CALL_TIME_MS;
import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
import android.Manifest;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.media.ToneGenerator;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.SystemClock;
import android.os.SystemVibrator;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.provider.BlockedNumberContract.SystemContract;
import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.sysprop.TelephonyProperties;
import android.telecom.CallAudioState;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.PhoneAccountSuggestion;
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 android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
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.BlockCheckerAdapter;
import com.android.server.telecom.callfiltering.BlockCheckerFilter;
import com.android.server.telecom.callfiltering.CallFilterResultCallback;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.callfiltering.CallFilteringResult.Builder;
import com.android.server.telecom.callfiltering.CallScreeningServiceFilter;
import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
import com.android.server.telecom.callredirection.CallRedirectionProcessor;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import com.android.server.telecom.settings.BlockedNumbersUtil;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
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.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
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);
void onConnectionTimeChanged(Call call);
void onConferenceStateChanged(Call call, boolean isConference);
void onCdmaConferenceSwap(Call call);
void onSetCamera(Call call, String cameraId);
}
/** 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, CallState.AUDIO_PROCESSING};
/**
* 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, CallState.SIMULATED_RINGING,
CallState.ANSWERED, CallState.AUDIO_PROCESSING};
private static final int[] ANY_CALL_STATE =
{CallState.NEW, CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.RINGING, CallState.SIMULATED_RINGING, CallState.ACTIVE,
CallState.ON_HOLD, CallState.DISCONNECTED, CallState.ABORTED,
CallState.DISCONNECTING, CallState.PULLING, CallState.ANSWERED,
CallState.AUDIO_PROCESSING};
public static final String TELECOM_CALL_ID_PREFIX = "TC@";
// Maps call technologies in TelephonyManager to those in Analytics.
private static final Map<Integer, Integer> sAnalyticsTechnologyMap;
static {
sAnalyticsTechnologyMap = new HashMap<>(5);
sAnalyticsTechnologyMap.put(TelephonyManager.PHONE_TYPE_CDMA, Analytics.CDMA_PHONE);
sAnalyticsTechnologyMap.put(TelephonyManager.PHONE_TYPE_GSM, Analytics.GSM_PHONE);
sAnalyticsTechnologyMap.put(TelephonyManager.PHONE_TYPE_IMS, Analytics.IMS_PHONE);
sAnalyticsTechnologyMap.put(TelephonyManager.PHONE_TYPE_SIP, Analytics.SIP_PHONE);
sAnalyticsTechnologyMap.put(TelephonyManager.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}.
*/
private Call mPendingCall;
/**
* Cached latest pending redirected call which requires user-intervention in order to be placed.
* Used by {@link #onCallRedirectionComplete}.
*/
private Call mPendingRedirectedOutgoingCall;
/**
* Cached call that's been answered but will be added to mCalls pending confirmation of active
* status from the connection service.
*/
private Call mPendingAudioProcessingCall;
/**
* Cached latest pending redirected call information which require user-intervention in order
* to be placed. Used by {@link #onCallRedirectionComplete}.
*/
private final Map<String, Runnable> mPendingRedirectedOutgoingCallInfo =
new ConcurrentHashMap<>();
/**
* Cached latest pending Unredirected call information which require user-intervention in order
* to be placed. Used by {@link #onCallRedirectionComplete}.
*/
private final Map<String, Runnable> mPendingUnredirectedOutgoingCallInfo =
new ConcurrentHashMap<>();
private CompletableFuture<Call> mPendingCallConfirm;
private CompletableFuture<Pair<Call, PhoneAccountHandle>> mPendingAccountSelection;
// Instance variables for testing -- we keep the latest copy of the outgoing call futures
// here so that we can wait on them in tests
private CompletableFuture<Call> mLatestPostSelectionProcessingFuture;
private CompletableFuture<Pair<Call, List<PhoneAccountSuggestion>>>
mLatestPreAccountSelectionFuture;
/**
* 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 CallDiagnosticServiceController mCallDiagnosticServiceController;
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 SystemStateHelper mSystemStateHelper;
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 PhoneAccountRegistrar mPhoneAccountRegistrar;
private final MissedCallNotifier mMissedCallNotifier;
private final DisconnectedCallNotifier mDisconnectedCallNotifier;
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 ToastFactory mToastFactory;
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 RoleManagerAdapter mRoleManagerAdapter;
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 int mMaxNumberOfSimultaneouslyActiveSims = -1;
private Runnable mStopTone;
private LinkedList<HandlerThread> mGraphHandlerThreads;
private boolean mHasActiveRttCall = false;
/**
* 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);
}
@Override
public void onPhoneAccountChanged(PhoneAccountRegistrar registrar,
PhoneAccount phoneAccount) {
handlePhoneAccountChanged(registrar, phoneAccount);
}
};
/**
* 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) {
Log.startSession("CM.CCCR");
String action = intent.getAction();
if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
|| SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
new UpdateEmergencyCallNotificationTask().doInBackground(
Pair.create(context, Log.createSubsession()));
}
}
};
private static class UpdateEmergencyCallNotificationTask
extends AsyncTask<Pair<Context, Session>, Void, Void> {
@SafeVarargs
@Override
protected final Void doInBackground(Pair<Context, Session>... args) {
if (args == null || args.length != 1 || args[0] == null) {
Log.e(this, new IllegalArgumentException(), "Incorrect invocation");
return null;
}
Log.continueSession(args[0].second, "CM.UECNT");
Context context = args[0].first;
BlockedNumbersUtil.updateEmergencyCallNotification(context,
SystemContract.shouldShowEmergencyCallNotification(context));
Log.endSession();
return null;
}
}
/**
* Initializes the required Telecom components.
*/
@VisibleForTesting
public CallsManager(
Context context,
TelecomSystem.SyncRoot lock,
CallerInfoLookupHelper callerInfoLookupHelper,
MissedCallNotifier missedCallNotifier,
DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory,
PhoneAccountRegistrar phoneAccountRegistrar,
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory
connectionServiceFocusManagerFactory,
CallAudioManager.AudioServiceFactory audioServiceFactory,
BluetoothRouteManager bluetoothManager,
WiredHeadsetManager wiredHeadsetManager,
SystemStateHelper systemStateHelper,
DefaultDialerCache defaultDialerCache,
Timeouts.Adapter timeoutsAdapter,
AsyncRingtonePlayer asyncRingtonePlayer,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
EmergencyCallHelper emergencyCallHelper,
InCallTonePlayer.ToneGeneratorFactory toneGeneratorFactory,
ClockProxy clockProxy,
AudioProcessingNotification audioProcessingNotification,
BluetoothStateReceiver bluetoothStateReceiver,
CallAudioRouteStateMachine.Factory callAudioRouteStateMachineFactory,
CallAudioModeStateMachine.Factory callAudioModeStateMachineFactory,
InCallControllerFactory inCallControllerFactory,
CallDiagnosticServiceController callDiagnosticServiceController,
RoleManagerAdapter roleManagerAdapter,
ToastFactory toastFactory) {
mContext = context;
mLock = lock;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mPhoneAccountRegistrar.addListener(mPhoneAccountListener);
mMissedCallNotifier = missedCallNotifier;
mDisconnectedCallNotifier = disconnectedCallNotifierFactory.create(mContext, this);
StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
mWiredHeadsetManager = wiredHeadsetManager;
mSystemStateHelper = systemStateHelper;
mDefaultDialerCache = defaultDialerCache;
mBluetoothRouteManager = bluetoothManager;
mDockManager = new DockManager(context);
mTimeoutsAdapter = timeoutsAdapter;
mEmergencyCallHelper = emergencyCallHelper;
mCallerInfoLookupHelper = callerInfoLookupHelper;
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
CallAudioRouteStateMachine callAudioRouteStateMachine =
callAudioRouteStateMachineFactory.create(
context,
this,
bluetoothManager,
wiredHeadsetManager,
statusBarNotifier,
audioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT
);
callAudioRouteStateMachine.initialize();
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
callAudioRouteStateMachine,
bluetoothManager,
wiredHeadsetManager,
mDockManager);
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
(resourceId, attributes) ->
new InCallTonePlayer.MediaPlayerAdapterImpl(
MediaPlayer.create(mContext, resourceId, attributes,
audioManager.generateAudioSessionId()));
InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(
callAudioRoutePeripheralAdapter, lock, toneGeneratorFactory, mediaPlayerFactory,
() -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0);
SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
SystemVibrator systemVibrator = new SystemVibrator(context);
mInCallController = inCallControllerFactory.create(context, mLock, this,
systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
emergencyCallHelper);
mCallDiagnosticServiceController = callDiagnosticServiceController;
mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory);
mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
ringtoneFactory, systemVibrator,
new Ringer.VibrationEffectProxy(), mInCallController);
mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
mTimeoutsAdapter, mLock);
mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
this, callAudioModeStateMachineFactory.create(systemStateHelper,
(AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
playerFactory, mRinger, new RingbackPlayer(playerFactory),
bluetoothStateReceiver, mDtmfLocalTonePlayer);
mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(mRequester);
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;
mToastFactory = toastFactory;
mRoleManagerAdapter = roleManagerAdapter;
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mInCallController);
mListeners.add(mCallDiagnosticServiceController);
mListeners.add(mCallAudioManager);
mListeners.add(mCallRecordingTonePlayer);
mListeners.add(missedCallNotifier);
mListeners.add(mDisconnectedCallNotifier);
mListeners.add(mHeadsetMediaButton);
mListeners.add(mProximitySensorManager);
mListeners.add(audioProcessingNotification);
// 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);
mGraphHandlerThreads = new LinkedList<>();
}
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;
}
public RoleManagerAdapter getRoleManagerAdapter() {
return mRoleManagerAdapter;
}
public CallDiagnosticServiceController getCallDiagnosticServiceController() {
return mCallDiagnosticServiceController;
}
@Override
public void onSuccessfulOutgoingCall(Call call, int callState) {
Log.v(this, "onSuccessfulOutgoingCall, %s", call);
call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp());
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");
PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
incomingCall.getTargetPhoneAccount());
Bundle extras =
phoneAccount == null || phoneAccount.getExtras() == null
? new Bundle()
: phoneAccount.getExtras();
if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
incomingCall.isSelfManaged() ||
extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
Log.i(this, "Skipping call filtering for %s (ecm=%b, selfMgd=%b, skipExtra=%b)",
incomingCall.getId(),
incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE),
incomingCall.isSelfManaged(),
extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING));
onCallFilteringComplete(incomingCall, new Builder()
.setShouldAllowCall(true)
.setShouldReject(false)
.setShouldAddToCallLog(true)
.setShouldShowNotification(true)
.build(), false);
incomingCall.setIsUsingCallFiltering(false);
return;
}
IncomingCallFilterGraph graph = setUpCallFilterGraph(incomingCall);
graph.performFiltering();
}
private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
incomingCall.setIsUsingCallFiltering(true);
String carrierPackageName = getCarrierPackageName();
String defaultDialerPackageName = TelecomManager.from(mContext).getDefaultDialerPackage();
String userChosenPackageName = getRoleManagerAdapter().getDefaultCallScreeningApp();
AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
mContext.getPackageManager(), packageName);
ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
IncomingCallFilterGraph graph = new IncomingCallFilterGraph(incomingCall,
this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
mCallerInfoLookupHelper);
BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
mCallerInfoLookupHelper, new BlockCheckerAdapter());
CallScreeningServiceFilter carrierCallScreeningServiceFilter =
new CallScreeningServiceFilter(incomingCall, carrierPackageName,
CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, this,
appLabelProxy, converter);
CallScreeningServiceFilter callScreeningServiceFilter;
if ((userChosenPackageName != null)
&& (!userChosenPackageName.equals(defaultDialerPackageName))) {
callScreeningServiceFilter = new CallScreeningServiceFilter(incomingCall,
userChosenPackageName, CallScreeningServiceFilter.PACKAGE_TYPE_USER_CHOSEN,
mContext, this, appLabelProxy, converter);
} else {
callScreeningServiceFilter = new CallScreeningServiceFilter(incomingCall,
defaultDialerPackageName,
CallScreeningServiceFilter.PACKAGE_TYPE_DEFAULT_DIALER,
mContext, this, appLabelProxy, converter);
}
graph.addFilter(voicemailFilter);
graph.addFilter(blockCheckerFilter);
graph.addFilter(carrierCallScreeningServiceFilter);
graph.addFilter(callScreeningServiceFilter);
IncomingCallFilterGraph.addEdge(voicemailFilter, carrierCallScreeningServiceFilter);
IncomingCallFilterGraph.addEdge(blockCheckerFilter, carrierCallScreeningServiceFilter);
IncomingCallFilterGraph.addEdge(carrierCallScreeningServiceFilter,
callScreeningServiceFilter);
mGraphHandlerThreads.add(graph.getHandlerThread());
return graph;
}
private String getCarrierPackageName() {
ComponentName componentName = null;
CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService
(Context.CARRIER_CONFIG_SERVICE);
PersistableBundle configBundle = configManager.getConfig();
if (configBundle != null) {
componentName = ComponentName.unflattenFromString(configBundle.getString
(CarrierConfigManager.KEY_CARRIER_CALL_SCREENING_APP_STRING, ""));
}
return componentName != null ? componentName.getPackageName() : null;
}
@Override
public void onCallFilteringComplete(Call incomingCall, CallFilteringResult result,
boolean timeout) {
// 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.
Log.i(this, "onCallFilteringComplete");
mGraphHandlerThreads.clear();
if (timeout) {
Log.i(this, "onCallFilteringCompleted: Call filters timeout!");
incomingCall.setUserMissed(USER_MISSED_CALL_FILTERS_TIMEOUT);
}
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;
}
// Inform our connection service that call filtering is done (if it was performed at all).
if (incomingCall.isUsingCallFiltering()) {
boolean isInContacts = incomingCall.getCallerInfo() != null
&& incomingCall.getCallerInfo().contactExists;
Connection.CallFilteringCompletionInfo completionInfo =
new Connection.CallFilteringCompletionInfo(!result.shouldAllowCall,
isInContacts,
result.mCallScreeningResponse == null
? null : result.mCallScreeningResponse.toCallResponse(),
result.mCallScreeningComponentName == null ? null
: ComponentName.unflattenFromString(
result.mCallScreeningComponentName));
incomingCall.getConnectionService().onCallFilteringCompleted(incomingCall,
completionInfo);
}
// Get rid of the call composer attachments that aren't wanted
if (result.mIsResponseFromSystemDialer && result.mCallScreeningResponse != null
&& result.mCallScreeningResponse.getCallComposerAttachmentsToShow() >= 0) {
int attachmentMask = result.mCallScreeningResponse.getCallComposerAttachmentsToShow();
if ((attachmentMask
& CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION) == 0) {
incomingCall.getIntentExtras().remove(TelecomManager.EXTRA_LOCATION);
}
if ((attachmentMask
& CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT) == 0) {
incomingCall.getIntentExtras().remove(TelecomManager.EXTRA_CALL_SUBJECT);
}
if ((attachmentMask
& CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY) == 0) {
incomingCall.getIntentExtras().remove(TelecomManager.EXTRA_PRIORITY);
}
}
if (result.shouldAllowCall) {
incomingCall.setPostCallPackageName(
getRoleManagerAdapter().getDefaultCallScreeningApp());
Log.i(this, "onCallFilteringComplete: allow call.");
if (hasMaximumManagedRingingCalls(incomingCall)) {
if (shouldSilenceInsteadOfReject(incomingCall)) {
incomingCall.silence();
} else {
Log.i(this, "onCallFilteringCompleted: Call rejected! " +
"Exceeds maximum number of ringing calls.");
incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
autoMissCallAndLog(incomingCall, result);
return;
}
} else if (hasMaximumManagedDialingCalls(incomingCall)) {
if (shouldSilenceInsteadOfReject(incomingCall)) {
incomingCall.silence();
} else {
Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
"dialing calls.");
incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_DIALING);
autoMissCallAndLog(incomingCall, result);
return;
}
} else if (result.shouldScreenViaAudio) {
Log.i(this, "onCallFilteringCompleted: starting background audio processing");
answerCallForAudioProcessing(incomingCall);
incomingCall.setAudioProcessingRequestingApp(result.mCallScreeningAppName);
} else if (result.shouldSilence) {
Log.i(this, "onCallFilteringCompleted: setting the call to silent ringing state");
incomingCall.setSilentRingingRequested(true);
incomingCall.setUserMissed(USER_MISSED_CALL_SCREENING_SERVICE_SILENCED);
incomingCall.setCallScreeningAppName(result.mCallScreeningAppName);
incomingCall.setCallScreeningComponentName(result.mCallScreeningComponentName);
addCall(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.BLOCKED_TYPE,
result.shouldShowNotification, result);
} else if (result.shouldShowNotification) {
Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
mMissedCallNotifier.showMissedCallNotification(
new MissedCallNotifier.CallInfo(incomingCall));
}
}
}
/**
* In the event that the maximum supported calls of a given type is reached, the
* default behavior is to reject any additional calls of that type. This checks
* if the device is configured to silence instead of reject the call, provided
* that the incoming call is from a different source (connection service).
*/
private boolean shouldSilenceInsteadOfReject(Call incomingCall) {
if (!mContext.getResources().getBoolean(
R.bool.silence_incoming_when_different_service_and_maximum_ringing)) {
return false;
}
for (Call call : mCalls) {
// Only operate on top-level calls
if (call.getParentCall() != null) {
continue;
}
if (call.isExternalCall()) {
continue;
}
if (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 onConnectionPropertiesChanged(Call call, boolean didRttChange) {
if (didRttChange) {
updateHasActiveRttCall();
}
}
@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 onConferenceStateChanged(Call call, boolean isConference) {
// Conference changed whether it is treated as a conference or not.
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
listener.onConferenceStateChanged(call, isConference);
}
}
@Override
public void onCdmaConferenceSwap(Call call) {
// SWAP was executed on a CDMA conference
for (CallsManagerListener listener : mListeners) {
listener.onCdmaConferenceSwap(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);
}
}
/**
* Handles a change to the currently active camera for a call by notifying listeners.
* @param call The call.
* @param cameraId The ID of the camera in use, or {@code null} if no camera is in use.
*/
@Override
public void onSetCamera(Call call, String cameraId) {
for (CallsManagerListener listener : mListeners) {
listener.onSetCamera(call, cameraId);
}
}
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 void onCallHoldFailed(Call call) {
markAllAnsweredCallAsRinging(call, "hold");
}
@Override
public void onCallSwitchFailed(Call call) {
markAllAnsweredCallAsRinging(call, "switch");
}
private void markAllAnsweredCallAsRinging(Call call, String actionName) {
// Normally, we don't care whether a call hold or switch has failed.
// However, if a call was held or switched in order to answer an incoming call, that
// incoming call needs to be brought out of the ANSWERED state so that the user can
// try the operation again.
for (Call call1 : mCalls) {
if (call1 != call && call1.getState() == CallState.ANSWERED) {
setCallState(call1, CallState.RINGING, actionName + " failed on other call");
}
}
}
@Override
public UserHandle getCurrentUserHandle() {
return mCurrentUserHandle;
}
public CallAudioManager getCallAudioManager() {
return mCallAudioManager;
}
InCallController getInCallController() {
return mInCallController;
}
EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
public DefaultDialerCache getDefaultDialerCache() {
return mDefaultDialerCache;
}
@VisibleForTesting
public PhoneAccountRegistrar.Listener getPhoneAccountListener() {
return mPhoneAccountListener;
}
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);
}
@VisibleForTesting
public void removeListener(CallsManagerListener listener) {
mListeners.remove(listener);
}
void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
Log.d(this, "processIncomingCallConference");
processIncomingCallIntent(phoneAccountHandle, extras, true);
}
/**
* 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) {
processIncomingCallIntent(phoneAccountHandle, extras, false);
}
void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras,
boolean isConference) {
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,
mPhoneNumberUtilsAdapter,
handle,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
Call.CALL_DIRECTION_INCOMING /* callDirection */,
false /* forceAttachToExistingConnection */,
isConference, /* isConference */
mClockProxy,
mToastFactory);
// 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) {
Bundle phoneAccountExtras = phoneAccount.getExtras();
call.setIsSelfManaged(phoneAccount.isSelfManaged());
if (call.isSelfManaged()) {
// Self managed calls will always be voip audio mode.
call.setIsVoipAudioMode(true);
call.setVisibleToInCallService(phoneAccountExtras == null
|| phoneAccountExtras.getBoolean(
PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, 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 (phoneAccountExtras != null
&& phoneAccountExtras.getBoolean(
PhoneAccount.EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE)) {
Log.d(this, "processIncomingCallIntent: defaulting to voip mode for call %s",
call.getId());
call.setIsVoipAudioMode(true);
}
}
boolean isRttSettingOn = isRttSettingOn(phoneAccountHandle);
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);
if (extras.containsKey(TelecomManager.EXTRA_CALL_DISCONNECT_MESSAGE)) {
String disconnectMessage = extras.getString(TelecomManager.EXTRA_CALL_DISCONNECT_MESSAGE);
Log.i(this, "processIncomingCallIntent Disconnect message " + disconnectMessage);
}
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()))) {
if (isConference) {
notifyCreateConferenceFailed(phoneAccountHandle, call);
} else {
if (hasMaximumManagedRingingCalls(call)) {
call.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
mCallLogManager.logCall(call, Calls.MISSED_TYPE,
true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
}
notifyCreateConnectionFailed(phoneAccountHandle, call);
}
} else if (isInEmergencyCall()) {
// The incoming call is implicitly being rejected so the user does not get any incoming
// call UI during an emergency call. In this case, log the call as missed instead of
// rejected since the user did not explicitly reject.
call.setMissedReason(AUTO_MISSED_EMERGENCY_CALL);
call.getAnalytics().setMissedReason(call.getMissedReason());
mCallLogManager.logCall(call, Calls.MISSED_TYPE,
true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
if (isConference) {
notifyCreateConferenceFailed(phoneAccountHandle, call);
} else {
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,
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,
mToastFactory);
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 requestedAccountHandle 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
* @param callingPackage the package name of the app which initiated the outgoing call.
*/
@VisibleForTesting
public @NonNull
CompletableFuture<Call> startOutgoingCall(Uri handle,
PhoneAccountHandle requestedAccountHandle,
Bundle extras, UserHandle initiatingUser, Intent originalIntent,
String callingPackage) {
final List<Uri> callee = new ArrayList<>();
callee.add(handle);
return startOutgoingCall(callee, requestedAccountHandle, extras, initiatingUser,
originalIntent, callingPackage, false);
}
private CompletableFuture<Call> startOutgoingCall(List<Uri> participants,
PhoneAccountHandle requestedAccountHandle,
Bundle extras, UserHandle initiatingUser, Intent originalIntent,
String callingPackage, boolean isConference) {
boolean isReusedCall;
Uri handle = isConference ? Uri.parse("tel:conf-factory") : participants.get(0);
Call call = reuseOutgoingCall(handle);
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(requestedAccountHandle, initiatingUser);
Bundle phoneAccountExtra = account != null ? account.getExtras() : null;
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,
mPhoneNumberUtilsAdapter,
handle,
isConference ? participants : null,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
null /* requestedAccountHandle */,
Call.CALL_DIRECTION_OUTGOING /* callDirection */,
false /* forceAttachToExistingConnection */,
isConference, /* isConference */
mClockProxy,
mToastFactory);
call.initAnalytics(callingPackage);
// 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.setVisibleToInCallService(phoneAccountExtra == null
|| phoneAccountExtra.getBoolean(
PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true));
}
call.setInitiatingUser(initiatingUser);
isReusedCall = false;
} else {
isReusedCall = true;
}
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);
}
final int finalVideoState = videoState;
final Call finalCall = call;
Handler outgoingCallHandler = new Handler(Looper.getMainLooper());
// Create a empty CompletableFuture and compose it with findOutgoingPhoneAccount to get
// a first guess at the list of suitable outgoing PhoneAccounts.
// findOutgoingPhoneAccount returns a CompletableFuture which is either already complete
// (in the case where we don't need to do the per-contact lookup) or a CompletableFuture
// that completes once the contact lookup via CallerInfoLookupHelper is complete.
CompletableFuture<List<PhoneAccountHandle>> accountsForCall =
CompletableFuture.completedFuture((Void) null).thenComposeAsync((x) ->
findOutgoingCallPhoneAccount(requestedAccountHandle, handle,
VideoProfile.isVideo(finalVideoState),
finalCall.isEmergencyCall(), initiatingUser,
isConference),
new LoggedHandlerExecutor(outgoingCallHandler, "CM.fOCP", mLock));
// This is a block of code that executes after the list of potential phone accts has been
// retrieved.
CompletableFuture<List<PhoneAccountHandle>> setAccountHandle =
accountsForCall.whenCompleteAsync((potentialPhoneAccounts, exception) -> {
Log.i(CallsManager.this, "set outgoing call phone acct stage");
PhoneAccountHandle phoneAccountHandle;
if (potentialPhoneAccounts.size() == 1) {
phoneAccountHandle = potentialPhoneAccounts.get(0);
} else {
phoneAccountHandle = null;
}
finalCall.setTargetPhoneAccount(phoneAccountHandle);
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.sOCPA", mLock));
// This composes the future containing the potential phone accounts with code that queries
// the suggestion service if necessary (i.e. if the list is longer than 1).
// If the suggestion service is queried, the inner lambda will return a future that
// completes when the suggestion service calls the callback.
CompletableFuture<List<PhoneAccountSuggestion>> suggestionFuture = accountsForCall.
thenComposeAsync(potentialPhoneAccounts -> {
Log.i(CallsManager.this, "call outgoing call suggestion service stage");
if (potentialPhoneAccounts.size() == 1) {
PhoneAccountSuggestion suggestion =
new PhoneAccountSuggestion(potentialPhoneAccounts.get(0),
PhoneAccountSuggestion.REASON_NONE, true);
return CompletableFuture.completedFuture(
Collections.singletonList(suggestion));
}
return PhoneAccountSuggestionHelper.bindAndGetSuggestions(mContext,
finalCall.getHandle(), potentialPhoneAccounts);
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.cOCSS", mLock));
// This future checks the status of existing calls and attempts to make room for the
// outgoing call. The future returned by the inner method will usually be pre-completed --
// we only pause here if user interaction is required to disconnect a self-managed call.
// It runs after the account handle is set, independently of the phone account suggestion
// future.
CompletableFuture<Call> makeRoomForCall = setAccountHandle.thenComposeAsync(
potentialPhoneAccounts -> {
Log.i(CallsManager.this, "make room for outgoing call stage");
if (isPotentialInCallMMICode(handle) && !isSelfManaged) {
return CompletableFuture.completedFuture(finalCall);
}
// 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 (isReusedCall) {
return CompletableFuture.completedFuture(finalCall);
} else {
Call reusableCall = reuseOutgoingCall(handle);
if (reusableCall != null) {
Log.i(CallsManager.this,
"reusable call %s came in later; disconnect it.",
reusableCall.getId());
mPendingCallsToDisconnect.remove(reusableCall);
reusableCall.disconnect();
markCallAsDisconnected(reusableCall,
new DisconnectCause(DisconnectCause.CANCELED));
}
}
if (!finalCall.isEmergencyCall() && isInEmergencyCall()) {
Log.i(CallsManager.this, "Aborting call since there's an"
+ " ongoing emergency call");
// If the ongoing call is a managed call, we will prevent the outgoing
// call from dialing.
if (isConference) {
notifyCreateConferenceFailed(finalCall.getTargetPhoneAccount(),
finalCall);
} else {
notifyCreateConnectionFailed(
finalCall.getTargetPhoneAccount(), finalCall);
}
return CompletableFuture.completedFuture(null);
}
// If we can not supportany more active calls, our options are to move a call
// to hold, disconnect a call, or cancel this call altogether.
boolean isRoomForCall = finalCall.isEmergencyCall() ?
makeRoomForOutgoingEmergencyCall(finalCall) :
makeRoomForOutgoingCall(finalCall);
if (!isRoomForCall) {
Call foregroundCall = getForegroundCall();
Log.d(CallsManager.this, "No more room for outgoing call %s ", finalCall);
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.
Log.i(CallsManager.this, "Prompting user to disconnect "
+ "self-managed call");
finalCall.setOriginalCallIntent(originalIntent);
CompletableFuture<Call> completionFuture = new CompletableFuture<>();
startCallConfirmation(finalCall, completionFuture);
return completionFuture;
} else {
// If the ongoing call is a managed call, we will prevent the outgoing
// call from dialing.
if (isConference) {
notifyCreateConferenceFailed(finalCall.getTargetPhoneAccount(),
finalCall);
} else {
notifyCreateConnectionFailed(
finalCall.getTargetPhoneAccount(), finalCall);
}
}
Log.i(CallsManager.this, "Aborting call since there's no room");
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.completedFuture(finalCall);
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSMCP", mLock));
// The outgoing call can be placed, go forward. This future glues together the results of
// the account suggestion stage and the make room for call stage.
CompletableFuture<Pair<Call, List<PhoneAccountSuggestion>>> preSelectStage =
makeRoomForCall.thenCombine(suggestionFuture, Pair::create);
mLatestPreAccountSelectionFuture = preSelectStage;
// This future takes the list of suggested accounts and the call and determines if more
// user interaction in the form of a phone account selection screen is needed. If so, it
// will set the call to SELECT_PHONE_ACCOUNT, add it to our internal list/send it to dialer,
// and then execution will pause pending the dialer calling phoneAccountSelected.
CompletableFuture<Pair<Call, PhoneAccountHandle>> dialerSelectPhoneAccountFuture =
preSelectStage.thenComposeAsync(
(args) -> {
Log.i(CallsManager.this, "dialer phone acct select stage");
Call callToPlace = args.first;
List<PhoneAccountSuggestion> accountSuggestions = args.second;
if (callToPlace == null) {
return CompletableFuture.completedFuture(null);
}
if (accountSuggestions == null || accountSuggestions.isEmpty()) {
Log.i(CallsManager.this, "Aborting call since there are no"
+ " available accounts.");
showErrorMessage(R.string.cant_call_due_to_no_supported_service);
return CompletableFuture.completedFuture(null);
}
boolean needsAccountSelection = accountSuggestions.size() > 1
&& !callToPlace.isEmergencyCall() && !isSelfManaged;
if (!needsAccountSelection) {
return CompletableFuture.completedFuture(Pair.create(callToPlace,
accountSuggestions.get(0).getPhoneAccountHandle()));
}
// This is the state where the user is expected to select an account
callToPlace.setState(CallState.SELECT_PHONE_ACCOUNT,
"needs account selection");
// Create our own instance to modify (since extras may be Bundle.EMPTY)
Bundle newExtras = new Bundle(extras);
List<PhoneAccountHandle> accountsFromSuggestions = accountSuggestions
.stream()
.map(PhoneAccountSuggestion::getPhoneAccountHandle)
.collect(Collectors.toList());
newExtras.putParcelableList(
android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS,
accountsFromSuggestions);
newExtras.putParcelableList(
android.telecom.Call.EXTRA_SUGGESTED_PHONE_ACCOUNTS,
accountSuggestions);
// Set a future in place so that we can proceed once the dialer replies.
mPendingAccountSelection = new CompletableFuture<>();
callToPlace.setIntentExtras(newExtras);
addCall(callToPlace);
return mPendingAccountSelection;
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSPA", mLock));
// Potentially perform call identification for dialed TEL scheme numbers.
if (PhoneAccount.SCHEME_TEL.equals(handle.getScheme())) {
// Perform an asynchronous contacts lookup in this stage; ensure post-dial digits are
// not included.
CompletableFuture<Pair<Uri, CallerInfo>> contactLookupFuture =
mCallerInfoLookupHelper.startLookup(Uri.fromParts(handle.getScheme(),
PhoneNumberUtils.extractNetworkPortion(handle.getSchemeSpecificPart()),
null));
// Once the phone account selection stage has completed, we can handle the results from
// that with the contacts lookup in order to determine if we should lookup bind to the
// CallScreeningService in order for it to potentially provide caller ID.
dialerSelectPhoneAccountFuture.thenAcceptBothAsync(contactLookupFuture,
(callPhoneAccountHandlePair, uriCallerInfoPair) -> {
Call theCall = callPhoneAccountHandlePair.first;
boolean isInContacts = uriCallerInfoPair.second != null
&& uriCallerInfoPair.second.contactExists;
Log.d(CallsManager.this, "outgoingCallIdStage: isInContacts=%s",
isInContacts);
// We only want to provide a CallScreeningService with a call if its not in
// contacts or the package has READ_CONTACT permission.
PackageManager packageManager = mContext.getPackageManager();
int permission = packageManager.checkPermission(
Manifest.permission.READ_CONTACTS,
mRoleManagerAdapter.getDefaultCallScreeningApp());
Log.d(CallsManager.this,
"default call screening service package %s has permissions=%s",
mRoleManagerAdapter.getDefaultCallScreeningApp(),
permission == PackageManager.PERMISSION_GRANTED);
if ((!isInContacts) || (permission == PackageManager.PERMISSION_GRANTED)) {
bindForOutgoingCallerId(theCall);
}
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.pCSB", mLock));
}
// Finally, after all user interaction is complete, we execute this code to finish setting
// up the outgoing call. The inner method always returns a completed future containing the
// call that we've finished setting up.
mLatestPostSelectionProcessingFuture = dialerSelectPhoneAccountFuture
.thenComposeAsync(args -> {
if (args == null) {
return CompletableFuture.completedFuture(null);
}
Log.i(CallsManager.this, "post acct selection stage");
Call callToUse = args.first;
PhoneAccountHandle phoneAccountHandle = args.second;
PhoneAccount accountToUse = mPhoneAccountRegistrar
.getPhoneAccount(phoneAccountHandle, initiatingUser);
callToUse.setTargetPhoneAccount(phoneAccountHandle);
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",
callToUse.getId());
callToUse.setIsVoipAudioMode(true);
}
}
callToUse.setState(
CallState.CONNECTING,
phoneAccountHandle == null ? "no-handle"
: phoneAccountHandle.toString());
boolean isVoicemail = isVoicemail(callToUse.getHandle(), accountToUse);
boolean isRttSettingOn = isRttSettingOn(phoneAccountHandle);
if (!isVoicemail && (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 (callToUse.isEmergencyCall() || (accountToUse != null
&& accountToUse.hasCapabilities(PhoneAccount.CAPABILITY_RTT))) {
// If the call requested RTT and it's an emergency call, ignore the
// capability and hope that the modem will deal with it somehow.
callToUse.createRttStreams();
}
// Even if the phone account doesn't support RTT yet,
// the connection manager might change that. Set this to check it later.
callToUse.setRequestedToStartWithRtt();
}
setIntentExtrasAndStartTime(callToUse, extras);
setCallSourceToAnalytics(callToUse, originalIntent);
if (isPotentialMMICode(handle) && !isSelfManaged) {
// Do not add the call if it is a potential MMI code.
callToUse.addListener(this);
} else if (!mCalls.contains(callToUse)) {
// We check if mCalls already contains the call because we could
// potentially be reusing
// a call which was previously added (See {@link #reuseOutgoingCall}).
addCall(callToUse);
}
return CompletableFuture.completedFuture(callToUse);
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.pASP", mLock));
return mLatestPostSelectionProcessingFuture;
}
public void startConference(List<Uri> participants, Bundle clientExtras, String callingPackage,
UserHandle initiatingUser) {
if (clientExtras == null) {
clientExtras = new Bundle();
}
PhoneAccountHandle phoneAccountHandle = clientExtras.getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
clientExtras, initiatingUser, null/* originalIntent */, callingPackage,
true/* isconference*/);
final boolean speakerphoneOn = clientExtras.getBoolean(
TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE);
final int videoState = clientExtras.getInt(
TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
final Session logSubsession = Log.createSubsession();
callFuture.thenAccept((call) -> {
if (call != null) {
Log.continueSession(logSubsession, "CM.pOGC");
try {
placeOutgoingCall(call, call.getHandle(), null/* gatewayInfo */,
speakerphoneOn, videoState);
} finally {
Log.endSession();
}
}
});
}
/**
* Performs call identification for an outgoing phone call.
* @param theCall The outgoing call to perform identification.
*/
private void bindForOutgoingCallerId(Call theCall) {
// Find the user chosen call screening app.
String callScreeningApp =
mRoleManagerAdapter.getDefaultCallScreeningApp();
CompletableFuture future =
new CallScreeningServiceHelper(mContext,
mLock,
callScreeningApp,
new ParcelableCallUtils.Converter(),
mCurrentUserHandle,
theCall,
new AppLabelProxy() {
@Override
public CharSequence getAppLabel(String packageName) {
return Util.getAppLabel(mContext.getPackageManager(), packageName);
}
}).process();
future.thenApply( v -> {
Log.i(this, "Outgoing caller ID complete");
return null;
});
}
/**
* 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 isEmergency {@code true} if the call is an emergency call.
* @param initiatingUser The {@link UserHandle} the call is placed on.
* @return
*/
@VisibleForTesting
public CompletableFuture<List<PhoneAccountHandle>> findOutgoingCallPhoneAccount(
PhoneAccountHandle targetPhoneAccountHandle, Uri handle, boolean isVideo,
boolean isEmergency, UserHandle initiatingUser) {
return findOutgoingCallPhoneAccount(targetPhoneAccountHandle, handle, isVideo,
isEmergency, initiatingUser, false/* isConference */);
}
public CompletableFuture<List<PhoneAccountHandle>> findOutgoingCallPhoneAccount(
PhoneAccountHandle targetPhoneAccountHandle, Uri handle, boolean isVideo,
boolean isEmergency, UserHandle initiatingUser, boolean isConference) {
if (isSelfManaged(targetPhoneAccountHandle, initiatingUser)) {
return CompletableFuture.completedFuture(Arrays.asList(targetPhoneAccountHandle));
}
List<PhoneAccountHandle> accounts;
// Try to find a potential phone account, taking into account whether this is a video
// call.
accounts = constructPossiblePhoneAccounts(handle, initiatingUser, isVideo, isEmergency,
isConference);
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 */, isEmergency /* isEmergency */, isConference);
}
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)) {
// The target phone account is valid and was found.
return CompletableFuture.completedFuture(Arrays.asList(targetPhoneAccountHandle));
}
}
if (accounts.isEmpty() || accounts.size() == 1) {
return CompletableFuture.completedFuture(accounts);
}
// Do the query for whether there's a preferred contact
final CompletableFuture<PhoneAccountHandle> userPreferredAccountForContact =
new CompletableFuture<>();
final List<PhoneAccountHandle> possibleAccounts = accounts;
mCallerInfoLookupHelper.startLookup(handle,
new CallerInfoLookupHelper.OnQueryCompleteListener() {
@Override
public void onCallerInfoQueryComplete(Uri handle, CallerInfo info) {
if (info != null &&
info.preferredPhoneAccountComponent != null &&
info.preferredPhoneAccountId != null &&
!info.preferredPhoneAccountId.isEmpty()) {
PhoneAccountHandle contactDefaultHandle = new PhoneAccountHandle(
info.preferredPhoneAccountComponent,
info.preferredPhoneAccountId,
initiatingUser);
userPreferredAccountForContact.complete(contactDefaultHandle);
} else {
userPreferredAccountForContact.complete(null);
}
}
@Override
public void onContactPhotoQueryComplete(Uri handle, CallerInfo info) {
// ignore this
}
});
return userPreferredAccountForContact.thenApply(phoneAccountHandle -> {
if (phoneAccountHandle != null) {
return Collections.singletonList(phoneAccountHandle);
}
// No preset account, check if default exists that supports the URI scheme for the
// handle and verify it can be used.
PhoneAccountHandle defaultPhoneAccountHandle =
mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(
handle.getScheme(), initiatingUser);
if (defaultPhoneAccountHandle != null &&
possibleAccounts.contains(defaultPhoneAccountHandle)) {
return Collections.singletonList(defaultPhoneAccountHandle);
}
return possibleAccounts;
});
}
/**
* 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();
}
public void onCallRedirectionComplete(Call call, Uri handle,
PhoneAccountHandle phoneAccountHandle,
GatewayInfo gatewayInfo, boolean speakerphoneOn,
int videoState, boolean shouldCancelCall,
String uiAction) {
Log.i(this, "onCallRedirectionComplete for Call %s with handle %s" +
" and phoneAccountHandle %s", call, handle, phoneAccountHandle);
boolean endEarly = false;
String disconnectReason = "";
String callRedirectionApp = mRoleManagerAdapter.getDefaultCallRedirectionApp();
boolean isPotentialEmergencyNumber;
try {
isPotentialEmergencyNumber =
handle != null && getTelephonyManager().isPotentialEmergencyNumber(
handle.getSchemeSpecificPart());
} catch (IllegalStateException ise) {
isPotentialEmergencyNumber = false;
} catch (RuntimeException r) {
isPotentialEmergencyNumber = false;
}
if (shouldCancelCall) {
Log.w(this, "onCallRedirectionComplete: call is canceled");
endEarly = true;
disconnectReason = "Canceled from Call Redirection Service";
// Show UX when user-defined call redirection service does not response; the UX
// is not needed to show if the call is disconnected (e.g. by the user)
if (uiAction.equals(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT)
&& !call.isDisconnected()) {
Intent timeoutIntent = new Intent(mContext,
CallRedirectionTimeoutDialogActivity.class);
timeoutIntent.putExtra(
CallRedirectionTimeoutDialogActivity.EXTRA_REDIRECTION_APP_NAME,
mRoleManagerAdapter.getApplicationLabelForPackageName(callRedirectionApp));
timeoutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(timeoutIntent, UserHandle.CURRENT);
}
} else if (handle == null) {
Log.w(this, "onCallRedirectionComplete: handle is null");
endEarly = true;
disconnectReason = "Null handle from Call Redirection Service";
} else if (phoneAccountHandle == null) {
Log.w(this, "onCallRedirectionComplete: phoneAccountHandle is null");
endEarly = true;
disconnectReason = "Null phoneAccountHandle from Call Redirection Service";
} else if (isPotentialEmergencyNumber) {
Log.w(this, "onCallRedirectionComplete: emergency number %s is redirected from Call"
+ " Redirection Service", handle.getSchemeSpecificPart());
endEarly = true;
disconnectReason = "Emergency number is redirected from Call Redirection Service";
}
if (endEarly) {
if (call != null) {
call.disconnect(disconnectReason);
}
return;
}
// If this call is already disconnected then we have nothing more to do.
if (call.isDisconnected()) {
Log.w(this, "onCallRedirectionComplete: Call has already been disconnected,"
+ " ignore the call redirection %s", call);
return;
}
if (uiAction.equals(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_ASK_FOR_CONFIRM)) {
Log.addEvent(call, LogUtils.Events.REDIRECTION_USER_CONFIRMATION);
mPendingRedirectedOutgoingCall = call;
mPendingRedirectedOutgoingCallInfo.put(call.getId(),
new Runnable("CM.oCRC", mLock) {
@Override
public void loggedRun() {
Log.addEvent(call, LogUtils.Events.REDIRECTION_USER_CONFIRMED);
call.setTargetPhoneAccount(phoneAccountHandle);
placeOutgoingCall(call, handle, gatewayInfo, speakerphoneOn,
videoState);
}
});
mPendingUnredirectedOutgoingCallInfo.put(call.getId(),
new Runnable("CM.oCRC", mLock) {
@Override
public void loggedRun() {
call.setTargetPhoneAccount(phoneAccountHandle);
placeOutgoingCall(call, handle, null, speakerphoneOn,
videoState);
}
});
Log.i(this, "onCallRedirectionComplete: UI_TYPE_USER_DEFINED_ASK_FOR_CONFIRM "
+ "callId=%s, callRedirectionAppName=%s",
call.getId(), callRedirectionApp);
showRedirectionDialog(call.getId(),
mRoleManagerAdapter.getApplicationLabelForPackageName(callRedirectionApp));
} else {
call.setTargetPhoneAccount(phoneAccountHandle);
placeOutgoingCall(call, handle, gatewayInfo, speakerphoneOn, videoState);
}
}
/**
* Shows the call redirection confirmation dialog. This is explicitly done here instead of in
* an activity class such as {@link ConfirmCallDialogActivity}. This was originally done with
* an activity class, however due to the fact that the InCall UI is being spun up at the same
* time as the dialog activity, there is a potential race condition where the InCall UI will
* often be shown instead of the dialog. Activity manager chooses not to show the redirection
* dialog in that case since the new top activity from dialer is going to show.
* By showing the dialog here we're able to set the dialog's window type to
* {@link WindowManager.LayoutParams#TYPE_SYSTEM_ALERT} which guarantees it shows above other
* content on the screen.
* @param callId The ID of the call to show the redirection dialog for.
*/
private void showRedirectionDialog(@NonNull String callId, @NonNull CharSequence appName) {
AlertDialog confirmDialog = new AlertDialog.Builder(mContext).create();
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
View dialogView = layoutInflater.inflate(R.layout.call_redirection_confirm_dialog, null);
Button buttonFirstLine = (Button) dialogView.findViewById(R.id.buttonFirstLine);
buttonFirstLine.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent proceedWithoutRedirectedCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_PLACE_UNREDIRECTED_CALL,
null, mContext,
TelecomBroadcastReceiver.class);
proceedWithoutRedirectedCall.putExtra(
TelecomBroadcastIntentProcessor.EXTRA_REDIRECTION_OUTGOING_CALL_ID,
callId);
mContext.sendBroadcast(proceedWithoutRedirectedCall);
confirmDialog.dismiss();
}
});
Button buttonSecondLine = (Button) dialogView.findViewById(R.id.buttonSecondLine);
buttonSecondLine.setText(mContext.getString(
R.string.alert_place_outgoing_call_with_redirection, appName));
buttonSecondLine.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent proceedWithRedirectedCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_PLACE_REDIRECTED_CALL, null,
mContext,
TelecomBroadcastReceiver.class);
proceedWithRedirectedCall.putExtra(
TelecomBroadcastIntentProcessor.EXTRA_REDIRECTION_OUTGOING_CALL_ID,
callId);
mContext.sendBroadcast(proceedWithRedirectedCall);
confirmDialog.dismiss();
}
});
Button buttonThirdLine = (Button) dialogView.findViewById(R.id.buttonThirdLine);
buttonThirdLine.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
cancelRedirection(callId);
confirmDialog.dismiss();
}
});
confirmDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
cancelRedirection(callId);
confirmDialog.dismiss();
}
});
confirmDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
confirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
confirmDialog.setCancelable(false);
confirmDialog.setCanceledOnTouchOutside(false);
confirmDialog.setView(dialogView);
confirmDialog.show();
}
/**
* Signals to Telecom that redirection of the call is to be cancelled.
*/
private void cancelRedirection(String callId) {
Intent cancelRedirectedCall = new Intent(
TelecomBroadcastIntentProcessor.ACTION_CANCEL_REDIRECTED_CALL,
null, mContext,
TelecomBroadcastReceiver.class);
cancelRedirectedCall.putExtra(
TelecomBroadcastIntentProcessor.EXTRA_REDIRECTION_OUTGOING_CALL_ID, callId);
mContext.sendBroadcastAsUser(cancelRedirectedCall, UserHandle.CURRENT);
}
public void processRedirectedOutgoingCallAfterUserInteraction(String callId, String action) {
Log.i(this, "processRedirectedOutgoingCallAfterUserInteraction for Call ID %s, action=%s",
callId, action);
if (mPendingRedirectedOutgoingCall != null) {
String pendingCallId = mPendingRedirectedOutgoingCall.getId();
if (!pendingCallId.equals(callId)) {
Log.i(this, "processRedirectedOutgoingCallAfterUserInteraction for new Call ID %s, "
+ "cancel the previous pending Call with ID %s", callId, pendingCallId);
mPendingRedirectedOutgoingCall.disconnect("Another call redirection requested");
mPendingRedirectedOutgoingCallInfo.remove(pendingCallId);
mPendingUnredirectedOutgoingCallInfo.remove(pendingCallId);
}
if (action.equals(TelecomBroadcastIntentProcessor.ACTION_PLACE_REDIRECTED_CALL)) {
mHandler.post(mPendingRedirectedOutgoingCallInfo.get(callId).prepare());
} else if (action.equals(
TelecomBroadcastIntentProcessor.ACTION_PLACE_UNREDIRECTED_CALL)) {
mHandler.post(mPendingUnredirectedOutgoingCallInfo.get(callId).prepare());
} else if (action.equals(
TelecomBroadcastIntentProcessor.ACTION_CANCEL_REDIRECTED_CALL)) {
Log.addEvent(mPendingRedirectedOutgoingCall,
LogUtils.Events.REDIRECTION_USER_CANCELLED);
mPendingRedirectedOutgoingCall.disconnect("User canceled the redirected call.");
}
mPendingRedirectedOutgoingCall = null;
mPendingRedirectedOutgoingCallInfo.remove(callId);
mPendingUnredirectedOutgoingCallInfo.remove(callId);
} else {
Log.w(this, "processRedirectedOutgoingCallAfterUserInteraction for non-matched Call ID"
+ " %s", callId);
}
}
/**
* 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
PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(
call.getTargetPhoneAccount(), call.getInitiatingUser());
boolean allowVideo = false;
if (account != null) {
allowVideo = account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING);
}
call.setStartWithSpeakerphoneOn(speakerphoneOn || (useSpeakerForVideoCall && allowVideo)
|| (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()) {
Executors.defaultThreadFactory().newThread(() ->
BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext))
.start();
}
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) {
if (call.isAdhocConferenceCall()) {
notifyCreateConferenceFailed(call.getTargetPhoneAccount(), call);
} else {
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, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
holdActiveCallForNewCall(call);
mConnectionSvrFocusMgr.requestFocus(
call,
new RequestCallback(new ActionAnswerCall(call, videoState)));
}
}
private void answerCallForAudioProcessing(Call call) {
// We don't check whether the call has been added to the internal lists yet -- it's optional
// until the call is actually in the AUDIO_PROCESSING state.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
if (activeCall != null && activeCall != call) {
Log.w(this, "answerCallForAudioProcessing: another active call already exists. "
+ "Ignoring request for audio processing and letting the incoming call "
+ "through.");
// The call should already be in the RINGING state, so all we have to do is add the
// call to the internal tracker.
addCall(call);
return;
}
Log.d(this, "answerCallForAudioProcessing: Incoming call = %s", call);
mConnectionSvrFocusMgr.requestFocus(
call,
new RequestCallback(() -> {
synchronized (mLock) {
Log.d(this, "answering call %s for audio processing with cs focus", call);
call.answerForAudioProcessing();
// Skip setting the call state to ANSWERED -- that's only for calls that
// were answered by user intervention.
mPendingAudioProcessingCall = call;
}
}));
}
/**
* Instructs Telecom to bring a call into the AUDIO_PROCESSING state.
*
* Used by the background audio call screener (also the default dialer) to signal that
* they want to manually enter the AUDIO_PROCESSING state. The user will be aware that there is
* an ongoing call at this time.
*
* @param call The call to manipulate
*/
public void enterBackgroundAudioProcessing(Call call, String requestingPackageName) {
if (!mCalls.contains(call)) {
Log.w(this, "Trying to exit audio processing on an untracked call");
return;
}
Call activeCall = getActiveCall();
if (activeCall != null && activeCall != call) {
Log.w(this, "Ignoring enter audio processing because there's already a call active");
return;
}
CharSequence requestingAppName = AppLabelProxy.Util.getAppLabel(
mContext.getPackageManager(), requestingPackageName);
if (requestingAppName == null) {
requestingAppName = requestingPackageName;
}
// We only want this to work on active or ringing calls
if (call.getState() == CallState.RINGING) {
// After the connection service sets up the call with the other end, it'll set the call
// state to AUDIO_PROCESSING
answerCallForAudioProcessing(call);
call.setAudioProcessingRequestingApp(requestingAppName);
} else if (call.getState() == CallState.ACTIVE) {
setCallState(call, CallState.AUDIO_PROCESSING,
"audio processing set by dialer request");
call.setAudioProcessingRequestingApp(requestingAppName);
}
}
/**
* Instructs Telecom to bring a call out of the AUDIO_PROCESSING state.
*
* Used by the background audio call screener (also the default dialer) to signal that it's
* finished doing its thing and the user should be made aware of the call.
*
* @param call The call to manipulate
* @param shouldRing if true, puts the call into SIMULATED_RINGING. Otherwise, makes the call
* active.
*/
public void exitBackgroundAudioProcessing(Call call, boolean shouldRing) {
if (!mCalls.contains(call)) {
Log.w(this, "Trying to exit audio processing on an untracked call");
return;
}
Call activeCall = getActiveCall();
if (activeCall != null) {
Log.w(this, "Ignoring exit audio processing because there's already a call active");
}
if (shouldRing) {
setCallState(call, CallState.SIMULATED_RINGING, "exitBackgroundAudioProcessing");
} else {
setCallState(call, CallState.ACTIVE, "exitBackgroundAudioProcessing");
}
}
/**
* 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 TelephonyProperties.videocall_audio_output()
.orElse(TelecomManager.AUDIO_OUTPUT_DEFAULT)
== TelecomManager.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 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, @android.telecom.Call.RejectReason int rejectReason) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to reject a non-existent call %s", call);
} else {
for (CallsManagerListener listener : mListeners) {
listener.onIncomingCallRejected(call, false /* rejectWithMessage */,
null /* textMessage */);
}
call.reject(rejectReason);
}
}
/**
* Instructs Telecom to transfer the specified call. Intended to be invoked by the in-call
* app through {@link InCallAdapter} after the user opts to transfer the said call.
*/
@VisibleForTesting
public void transferCall(Call call, Uri number, boolean isConfirmationRequired) {
if (!mCalls.contains(call)) {
Log.i(this, "transferCall - Request to transfer a non-existent call %s", call);
} else {
call.transfer(number, isConfirmationRequired);
}
}
/**
* Instructs Telecom to transfer the specified call to another ongoing call.
* Intended to be invoked by the in-call app through {@link InCallAdapter} after the user opts
* to transfer the said call (consultative transfer).
*/
@VisibleForTesting
public void transferCall(Call call, Call otherCall) {
if (!mCalls.contains(call) || !mCalls.contains(otherCall)) {
Log.i(this, "transferCall - Non-existent call %s or %s", call, otherCall);
} else {
call.transfer(otherCall);
}
}
/**
* 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);
int previousState = call.getState();
call.disconnect();
for (CallsManagerListener listener : mListeners) {
listener.onCallStateChanged(call, previousState, call.getState());
}
// Cancel any of the outgoing call futures if they're still around.
if (mPendingCallConfirm != null && !mPendingCallConfirm.isDone()) {
mPendingCallConfirm.complete(null);
mPendingCallConfirm = null;
}
if (mPendingAccountSelection != null && !mPendingAccountSelection.isDone()) {
mPendingAccountSelection.complete(null);
mPendingAccountSelection = null;
}
}
}
/**
* 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 {
if (getOutgoingCall() != null) {
Log.w(this, "There is an outgoing call, so it is unable to unhold this call %s",
call);
return;
}
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
String activeCallId = null;
if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
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 or connection manager, then disconnect it, otherwise invoke
// call.hold() and allow the connection service or connection manager to handle
// the situation.
if (!areFromSameSource(activeCall, call)) {
if (!activeCall.isEmergencyCall()) {
activeCall.disconnect("Swap to " + call.getId());
} else {
Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
activeCall.getId(), call.getId());
// Don't unhold the call as requested; we don't want to drop an
// emergency call.
return;
}
} 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();
}
@Override
public void onRemoteRttRequest(Call call, int requestId) {
Log.i(this, "onRemoteRttRequest: call %s", call.getId());
playRttUpgradeToneForCall(call);
}
public void playRttUpgradeToneForCall(Call call) {
mCallAudioManager.playRttUpgradeTone(call);
}
// 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, boolean isEmergency) {
return constructPossiblePhoneAccounts(handle, user, isVideo, isEmergency, false);
}
public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
boolean isVideo, boolean isEmergency, boolean isConference) {
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. When handling the emergency call,
// it also needs to find the phone accounts excluded by CAPABILITY_EMERGENCY_CALLS_ONLY.
int capabilities = isVideo ? PhoneAccount.CAPABILITY_VIDEO_CALLING : 0;
capabilities |= isConference ? PhoneAccount.CAPABILITY_ADHOC_CONFERENCE_CALLING : 0;
List<PhoneAccountHandle> allAccounts =
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false, user,
capabilities,
isEmergency ? 0 : PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY);
if (mMaxNumberOfSimultaneouslyActiveSims < 0) {
mMaxNumberOfSimultaneouslyActiveSims =
getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims();
}
// 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 (mMaxNumberOfSimultaneouslyActiveSims == 1) {
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;
}
private TelephonyManager getTelephonyManager() {
return mContext.getSystemService(TelephonyManager.class);
}
/**
* 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 (isInEmergencyCall() && 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) {
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(PhoneAccountHandle handle) {
boolean isRttModeSettingOn = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.RTT_CALLING_MODE, 0, mContext.getUserId()) != 0;
// If the carrier config says that we should ignore the RTT mode setting from the user,
// assume that it's off (i.e. only make an RTT call if it's requested through the extra).
boolean shouldIgnoreRttModeSetting = getCarrierConfigForPhoneAccount(handle)
.getBoolean(CarrierConfigManager.KEY_IGNORE_RTT_MODE_SETTING_BOOL, false);
return isRttModeSettingOn && !shouldIgnoreRttModeSetting;
}
private PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
CarrierConfigManager carrierConfigManager =
mContext.getSystemService(CarrierConfigManager.class);
PersistableBundle result = carrierConfigManager.getConfigForSubId(subscriptionId);
return result == null ? new PersistableBundle() : result;
}
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 {
if (setDefault) {
mPhoneAccountRegistrar
.setUserSelectedOutgoingPhoneAccount(account, call.getInitiatingUser());
}
if (mPendingAccountSelection != null) {
mPendingAccountSelection.complete(Pair.create(call, account));
mPendingAccountSelection = null;
}
}
}
/** 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();
Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call, activeCall);
if (activeCall != null && activeCall != call) {
if (canHold(activeCall)) {
activeCall.hold();
return true;
} else if (supportsHold(activeCall)
&& areFromSameSource(activeCall, call)) {
// Handle the case where the active call and the new call are from the same CS or
// connection manager, 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.getTargetPhoneAccount());
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 or connection manager, then disconnect it, otherwise allow the connection
// service or connection manager to figure out the right states.
if (!areFromSameSource(activeCall, call)) {
Log.i(this, "holdActiveCallForNewCall: disconnecting %s so that %s can be "
+ "made active.", activeCall.getId(), call.getId());
if (!activeCall.isEmergencyCall()) {
activeCall.disconnect();
} else {
// It's not possible to hold the active call, and its an emergency call so
// we will silently reject the incoming call instead of answering it.
Log.w(this, "holdActiveCallForNewCall: rejecting incoming call %s as "
+ "the active call is an emergency call and it cannot be held.",
call.getId());
call.reject(false /* rejectWithMessage */, "" /* message */,
"active emergency call can't be held");
}
}
}
}
return false;
}
@VisibleForTesting
public void markCallAsActive(Call call) {
Log.i(this, "markCallAsActive, isSelfManaged: " + call.isSelfManaged());
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 {
if (mPendingAudioProcessingCall == call) {
if (mCalls.contains(call)) {
setCallState(call, CallState.AUDIO_PROCESSING, "active set explicitly");
} else {
call.setState(CallState.AUDIO_PROCESSING, "active set explicitly and adding");
addCall(call);
}
// Clear mPendingAudioProcessingCall so that future attempts to mark the call as
// active (e.g. coming off of hold) don't put the call into audio processing instead
mPendingAudioProcessingCall = null;
return;
}
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}.
*/
@VisibleForTesting
public void markCallAsDisconnected(Call call, DisconnectCause disconnectCause) {
int oldState = call.getState();
if (call.getState() == CallState.SIMULATED_RINGING
&& disconnectCause.getCode() == DisconnectCause.REMOTE) {
// If the remote end hangs up while in SIMULATED_RINGING, the call should
// be marked as missed.
call.setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
// If a call diagnostic service is in use, we will log the original telephony-provided
// disconnect cause, inform the CDS of the disconnection, and then chain the update of the
// call state until AFTER the CDS reports it's result back.
if ((oldState == CallState.ACTIVE || oldState == CallState.DIALING)
&& disconnectCause.getCode() != DisconnectCause.MISSED
&& mCallDiagnosticServiceController.isConnected()
&& mCallDiagnosticServiceController.onCallDisconnected(call, disconnectCause)) {
Log.i(this, "markCallAsDisconnected; callid=%s, postingToFuture.", call.getId());
// Log the original disconnect reason prior to calling into the
// CallDiagnosticService.
Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
// Setup the future with a timeout so that the CDS is time boxed.
CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
mContext.getContentResolver()));
// Post the disconnection updates to the future for completion once the CDS returns
// with it's overridden disconnect message.
future.thenRunAsync(() -> {
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
}, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
.exceptionally((throwable) -> {
Log.e(TAG, throwable, "Error while executing disconnect future.");
return null;
});
} else {
// No CallDiagnosticService, or it doesn't handle this call, so just do this
// synchronously as always.
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
}
if (oldState == CallState.NEW && disconnectCause.getCode() == DisconnectCause.MISSED) {
Log.i(this, "markCallAsDisconnected: logging missed call ");
mCallLogManager.logCall(call, Calls.MISSED_TYPE, true, null);
}
}
/**
* Removes an existing disconnected call, and notifies the in-call app.
*/
void markCallAsRemoved(Call call) {
if (call.isDisconnectHandledViaFuture()) {
Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
// A future is being used due to a CallDiagnosticService handling the call. We will
// chain the removal operation to the end of any outstanding disconnect work.
call.getDisconnectFuture().thenRunAsync(() -> {
performRemoval(call);
}, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
.exceptionally((throwable) -> {
Log.e(TAG, throwable, "Error while executing disconnect future");
return null;
});
} else {
Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
performRemoval(call);
}
}
/**
* Work which is completed when a call is to be removed. Can either be be run synchronously or
* posted to a {@link Call#getDisconnectFuture()}.
* @param call The call.
*/
private void performRemoval(Call call) {
mInCallController.getBindingFuture().thenRunAsync(() -> {
call.maybeCleanupHandover();
removeCall(call);
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
if (mLocallyDisconnectingCalls.contains(call)) {
boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
Log.v(this, "performRemoval: 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, "performRemoval: Auto-unholding held foreground call (call doesn't "
+ "support hold)");
foregroundCall.unhold();
}
}, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
.exceptionally((throwable) -> {
Log.e(TAG, throwable, "Error while executing call removal");
return null;
});
}
/**
* 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} 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,
null /* message */, null /* description */, "CS_DEATH",
ToneGenerator.TONE_PROP_PROMPT));
}
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, CallState.ANSWERED) != null;
}
boolean hasRingingOrSimulatedRingingCall() {
return getFirstCallWithState(
CallState.SIMULATED_RINGING, CallState.RINGING, CallState.ANSWERED) != null;
}
@VisibleForTesting
public boolean onMediaButton(int type) {
if (hasAnyCalls()) {
Call ringingCall = getFirstCallWithState(CallState.RINGING,
CallState.SIMULATED_RINGING);
if (HeadsetMediaButton.SHORT_PRESS == type) {
if (ringingCall == null) {
Call activeCall = getFirstCallWithState(CallState.ACTIVE);
Call onHoldCall = getFirstCallWithState(CallState.ON_HOLD);
if (activeCall != null && onHoldCall != null) {
// Two calls, short-press -> switch calls
Log.addEvent(onHoldCall, LogUtils.Events.INFO,
"two calls, media btn short press - switch call.");
unholdCall(onHoldCall);
return true;
}
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 {
answerCall(ringingCall, 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 {
Call activeCall = getFirstCallWithState(CallState.ACTIVE);
Call onHoldCall = getFirstCallWithState(CallState.ON_HOLD);
if (activeCall != null && onHoldCall != null) {
// Two calls, long-press -> end current call
Log.addEvent(activeCall, LogUtils.Events.INFO,
"two calls, media btn long press - end current call.");
disconnectCall(activeCall);
return true;
}
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 getRingingOrSimulatedRingingCall() {
return getFirstCallWithState(CallState.RINGING,
CallState.ANSWERED, CallState.SIMULATED_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(PhoneAccountHandle targetPhoneAccount) {
Optional<Call> heldCall = mCalls.stream()
.filter(call -> PhoneAccountHandle.areFromSamePackage(call.getTargetPhoneAccount(),
targetPhoneAccount)
&& call.getParentCall() == null
&& 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;
}
@VisibleForTesting
public CompletableFuture<Call> getLatestPostSelectionProcessingFuture() {
return mLatestPostSelectionProcessingFuture;
}
@VisibleForTesting
public CompletableFuture getLatestPreAccountSelectionFuture() {
return mLatestPreAccountSelectionFuture;
}
/**
* 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();
int callDirection = Call.getRemappedCallDirection(parcelableConference.getCallDirection());
PhoneAccountHandle connectionMgr =
mPhoneAccountRegistrar.getSimCallManagerFromHandle(phoneAccount,
mCurrentUserHandle);
Call call = new Call(
callId,
mContext,
this,
mLock,
mConnectionServiceRepository,
mPhoneNumberUtilsAdapter,
null /* handle */,
null /* gatewayInfo */,
connectionMgr,
phoneAccount,
callDirection,
false /* forceAttachToExistingConnection */,
true /* isConference */,
connectTime,
connectElapsedTime,
mClockProxy,
mToastFactory);
setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()),
"new conference call");
call.setHandle(parcelableConference.getHandle(),
parcelableConference.getHandlePresentation());
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}.
*/
@VisibleForTesting
public PhoneAccountRegistrar getPhoneAccountRegistrar() {
return mPhoneAccountRegistrar;
}
/**
* Retrieves the {@link DisconnectedCallNotifier}
* @return The {@link DisconnectedCallNotifier}.
*/
DisconnectedCallNotifier getDisconnectedCallNotifier() {
return mDisconnectedCallNotifier;
}
/**
* 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 autoMissCallAndLog(Call incomingCall, CallFilteringResult result) {
incomingCall.getAnalytics().setMissedReason(incomingCall.getMissedReason());
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*/, result);
}
/**
* 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.i(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();
updateHasActiveRttCall();
updateExternalCallCanPullSupport();
// 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();
}
@VisibleForTesting
public 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();
updateExternalCallCanPullSupport();
// Only broadcast changes for calls that are being tracked.
if (shouldNotify) {
updateCanAddCall();
updateHasActiveRttCall();
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();
}
private void updateHasActiveRttCall() {
boolean hasActiveRttCall = hasActiveRttCall();
if (hasActiveRttCall != mHasActiveRttCall) {
Log.i(this, "updateHasActiveRttCall %s -> %s", mHasActiveRttCall, hasActiveRttCall);
AudioManager.setRttEnabled(hasActiveRttCall);
mHasActiveRttCall = hasActiveRttCall;
}
}
private boolean hasActiveRttCall() {
for (Call call : mCalls) {
if (call.isActive() && call.isRttCall()) {
return true;
}
}
return false;
}
/**
* 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(call.getParcelableCallState()),
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.
if (call.setState(newState, tag)) {
if ((oldState != CallState.AUDIO_PROCESSING) &&
(newState == CallState.DISCONNECTED)) {
maybeSendPostCallScreenIntent(call);
}
int disconnectCode = call.getDisconnectCause().getCode();
if ((newState == CallState.ABORTED || newState == CallState.DISCONNECTED)
&& ((disconnectCode != DisconnectCause.MISSED)
&& (disconnectCode != DisconnectCause.CANCELED))) {
call.setMissedReason(MISSED_REASON_NOT_MISSED);
}
call.getAnalytics().setMissedReason(call.getMissedReason());
maybeShowErrorDialogOnDisconnect(call);
Trace.beginSection("onCallStateChanged");
maybeHandleHandover(call, newState);
notifyCallStateChanged(call, oldState, newState);
Trace.endSection();
} else {
Log.i(this, "failed in setting the state to new state");
}
}
}
private void notifyCallStateChanged(Call call, int oldState, int newState) {
// Only broadcast state change for calls that are being tracked.
if (mCalls.contains(call)) {
updateCanAddCall();
updateHasActiveRttCall();
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();
}
}
}
}
/**
* 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, CallState.ANSWERED);
}
private boolean hasMaximumSelfManagedRingingCalls(Call exceptCall,
PhoneAccountHandle phoneAccountHandle) {
return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(true /* isSelfManaged */, exceptCall,
phoneAccountHandle, CallState.RINGING, CallState.ANSWERED);
}
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 other unholdable calls owned by
* another connection service.
* @param phoneAccountHandle The {@link PhoneAccountHandle} to check.
* @return {@code true} if there are other unholdable calls, {@code false} otherwise.
*/
public boolean hasUnholdableCallsForOtherConnectionService(
PhoneAccountHandle phoneAccountHandle) {
return getNumUnholdableCallsForOtherConnectionService(phoneAccountHandle) > 0;
}
/**
* Determines the number of unholdable calls present in a connection service other than the one
* the passed phone account belonds to.
* @param phoneAccountHandle The handle of the PhoneAccount.
* @return Number of unholdable calls owned by other connection service.
*/
public int getNumUnholdableCallsForOtherConnectionService(
PhoneAccountHandle phoneAccountHandle) {
return (int) mCalls.stream().filter(call ->
!phoneAccountHandle.getComponentName().equals(
call.getTargetPhoneAccount().getComponentName())
&& call.getParentCall() == null
&& !call.isExternalCall()
&& !canHold(call)).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()
&& hasUnholdableCallsForOtherConnectionService(incomingCall.getTargetPhoneAccount())
&& incomingCall.getHandoverSourceCall() == null;
}
@VisibleForTesting
public boolean makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
// Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
// distraction. This does not affect live call count.
if (hasRingingOrSimulatedRingingCall()) {
Call ringingCall = getRingingOrSimulatedRingingCall();
ringingCall.getAnalytics().setCallIsAdditional(true);
ringingCall.getAnalytics().setCallIsInterrupted(true);
if (ringingCall.getState() == CallState.SIMULATED_RINGING) {
if (!ringingCall.hasGoneActiveBefore()) {
// If this is an incoming call that is currently in SIMULATED_RINGING only
// after a call screen, disconnect to make room and mark as missed, since
// the user didn't get a chance to accept/reject.
ringingCall.disconnect("emergency call dialed during simulated ringing "
+ "after screen.");
} else {
// If this is a simulated ringing call after being active and put in
// AUDIO_PROCESSING state again, disconnect normally.
ringingCall.reject(false, null, "emergency call dialed during simulated "
+ "ringing.");
}
} else { // normal incoming ringing call.
// Hang up the ringing call to make room for the emergency call and mark as missed,
// since the user did not reject.
ringingCall.setOverrideDisconnectCauseCode(
new DisconnectCause(DisconnectCause.MISSED));
ringingCall.reject(false, null, "emergency call dialed during ringing.");
}
}
// There is already room!
if (!hasMaximumLiveCalls(emergencyCall)) return true;
Call liveCall = getFirstCallWithState(LIVE_CALL_STATES);
Log.i(this, "makeRoomForOutgoingEmergencyCall call = " + emergencyCall
+ " livecall = " + liveCall);
if (emergencyCall == liveCall) {
// Not likely, but a good correctness check.
return true;
}
if (hasMaximumOutgoingCalls(emergencyCall)) {
Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
if (!outgoingCall.isEmergencyCall()) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect("Disconnecting dialing call in favor of new dialing"
+ " emergency call.");
return true;
}
if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
// Correctness check: if there is an orphaned emergency call in the
// {@link CallState#SELECT_PHONE_ACCOUNT} state, just disconnect it since the user
// has explicitly started a new call.
emergencyCall.getAnalytics().setCallIsAdditional(true);
outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+ " of new outgoing call.");
return true;
}
// If the user tries to make two outgoing calls to different emergency call numbers,
// we will try to connect the first outgoing call and reject the second.
return false;
}
if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.disconnect("disconnecting audio processing call for emergency");
return true;
}
// 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(emergencyCall) && !canHold(liveCall)) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
// Disconnect the active call instead of the holding call because it is historically
// easier to do, rather than disconnect a held call.
liveCall.disconnect("disconnecting to make room for emergency call "
+ emergencyCall.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, "makeRoomForOutgoingEmergencyCall: using child call PhoneAccount = " +
liveCallPhoneAccount);
}
// We may not know which PhoneAccount the emergency call will be placed on yet, but if
// the liveCall PhoneAccount does not support placing emergency calls, then we know it
// will not be that one and we do not want multiple PhoneAccounts active during an
// emergency call if possible. Disconnect the active call in favor of the emergency call
// instead of trying to hold.
if (liveCall.getTargetPhoneAccount() != null) {
PhoneAccount pa = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
liveCall.getTargetPhoneAccount());
if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
liveCall.disconnect("outgoing call does not support emergency calls, "
+ "disconnecting.");
}
return true;
}
// First thing, if we are trying to make an emergency call with the same package name 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.
// By default, for telephony, it will try to hold the existing call before placing the new
// emergency call except for if the carrier does not support holding calls for emergency.
// In this case, telephony will disconnect the call.
if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
emergencyCall.getTargetPhoneAccount())) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccount matches.");
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
return true;
} else if (emergencyCall.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;
}
// Hold the live call if possible before attempting the new outgoing emergency call.
if (canHold(liveCall)) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.hold("calling " + emergencyCall.getId());
return true;
}
// The live call cannot be held so we're out of luck here. There's no room.
return false;
}
@VisibleForTesting
public boolean makeRoomForOutgoingCall(Call call) {
// Already room!
if (!hasMaximumLiveCalls(call)) return true;
// 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 the live call is stuck in a connecting state, then we should disconnect it in favor
// of the new outgoing call.
if (liveCall.getState() == CallState.CONNECTING) {
liveCall.disconnect("Force disconnect CONNECTING call.");
return true;
}
if (hasMaximumOutgoingCalls(call)) {
Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
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("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+ " of new outgoing call.");
return true;
}
return false;
}
// 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 (PhoneAccountHandle.areFromSamePackage(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;
}
/**
* 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;
PhoneAccountHandle connectionMgr =
mPhoneAccountRegistrar.getSimCallManagerFromHandle(connection.getPhoneAccount(),
mCurrentUserHandle);
Call call = new Call(
callId,
mContext,
this,
mLock,
mConnectionServiceRepository,
mPhoneNumberUtilsAdapter,
connection.getHandle() /* handle */,
null /* gatewayInfo */,
connectionMgr,
connection.getPhoneAccount(), /* targetPhoneAccountHandle */
Call.getRemappedCallDirection(connection.getCallDirection()) /* callDirection */,
false /* forceAttachToExistingConnection */,
isDowngradedConference /* isConference */,
connection.getConnectTimeMillis() /* connectTimeMillis */,
connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */
mClockProxy,
mToastFactory);
call.initAnalytics();
call.getAnalytics().setCreatedFromExistingConnection(true);
setCallState(call, Call.getStateFromConnectionState(connection.getState()),
"existing connection");
call.setVideoState(connection.getVideoState());
call.setConnectionCapabilities(connection.getConnectionCapabilities());
call.setConnectionProperties(connection.getConnectionProperties());
call.setHandle(connection.getHandle(), connection.getHandlePresentation());
call.setCallerDisplayName(connection.getCallerDisplayName(),
connection.getCallerDisplayNamePresentation());
call.addListener(this);
call.putExtras(Call.SOURCE_CONNECTION_SERVICE, connection.getExtras());
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);
mRoleManagerAdapter.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;
}
public Timeouts.Adapter getTimeoutsAdapter() {
return mTimeoutsAdapter;
}
public SystemStateHelper getSystemStateHelper() {
return mSystemStateHelper;
}
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 (isInEmergencyCall()) return false;
if (!phoneAccount.isSelfManaged()) {
return !hasMaximumManagedRingingCalls(excludeCall) &&
!hasMaximumManagedHoldingCalls(excludeCall);
} else {
return !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 !isInEmergencyCall() &&
((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, String)}.
* 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);
// 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);
mPendingCallConfirm.complete(mPendingCall);
mPendingCallConfirm = null;
mPendingCall = null;
}
}
/**
* Used to cancel an outgoing call which was marked as pending confirmation in
* {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent, String)}.
* 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;
mPendingCallConfirm.complete(null);
mPendingCallConfirm = null;
}
}
/**
* Called from {@link #startOutgoingCall(Uri, PhoneAccountHandle, Bundle, UserHandle, Intent, String)} 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, CompletableFuture<Call> confirmationFuture) {
if (mPendingCall != null) {
Log.i(this, "startCallConfirmation: call %s is already pending; disconnecting %s",
mPendingCall.getId(), call.getId());
markCallDisconnectedDueToSelfManagedCall(call);
confirmationFuture.complete(null);
return;
}
Log.addEvent(call, LogUtils.Events.USER_CONFIRMATION);
mPendingCall = call;
mPendingCallConfirm = confirmationFuture;
// 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 (mPendingRedirectedOutgoingCallInfo.size() > 0) {
pw.print("mPendingRedirectedOutgoingCallInfo:");
pw.println(mPendingRedirectedOutgoingCallInfo.keySet().stream().collect(
Collectors.joining(", ")));
}
if (mPendingUnredirectedOutgoingCallInfo.size() > 0) {
pw.print("mPendingUnredirectedOutgoingCallInfo:");
pw.println(mPendingUnredirectedOutgoingCallInfo.keySet().stream().collect(
Collectors.joining(", ")));
}
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 (mCallDiagnosticServiceController != null) {
pw.println("mCallDiagnosticServiceController:");
pw.increaseIndent();
mCallDiagnosticServiceController.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();
}
if (mRoleManagerAdapter != null && mRoleManagerAdapter instanceof RoleManagerAdapterImpl) {
RoleManagerAdapterImpl impl = (RoleManagerAdapterImpl) mRoleManagerAdapter;
pw.println("mRoleManager:");
pw.increaseIndent();
impl.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());
if (call.visibleToInCallService()) {
extras.putBoolean(PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true);
}
call.setIntentExtras(extras);
}
private void setCallSourceToAnalytics(Call call, Intent originalIntent) {
if (originalIntent == null) {
return;
}
int callSource = originalIntent.getIntExtra(TelecomManager.EXTRA_CALL_SOURCE,
Analytics.CALL_SOURCE_UNSPECIFIED);
// Call source is only used by metrics, so we simply set it to Analytics directly.
call.getAnalytics().setCallSource(callSource);
}
private boolean isVoicemail(Uri callHandle, PhoneAccount phoneAccount) {
if (callHandle == null) {
return false;
}
if (PhoneAccount.SCHEME_VOICEMAIL.equals(callHandle.getScheme())) {
return true;
}
return phoneAccount != null && mPhoneAccountRegistrar.isVoiceMailNumber(
phoneAccount.getAccountHandle(),
callHandle.getSchemeSpecificPart());
}
/**
* 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 create a new connection has failed.
*
* @param phoneAccountHandle The {@link PhoneAccountHandle}.
* @param call The {@link Call} which could not be added.
*/
private void notifyCreateConferenceFailed(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.createConferenceFailed(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) {
handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, "legacy request denied");
}
/**
* 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 (isInEmergencyCall()) {
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,
mPhoneNumberUtilsAdapter,
handoverFromCall.getHandle(), null,
null, null,
Call.CALL_DIRECTION_OUTGOING, false,
false, mClockProxy, mToastFactory);
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,
mPhoneNumberUtilsAdapter,
srcAddr,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
destAcct,
Call.CALL_DIRECTION_INCOMING /* callDirection */,
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy,
mToastFactory);
if (fromCall == null || isHandoverInProgress() ||
!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) ||
!isHandoverToPhoneAccountSupported(destAcct) ||
isInEmergencyCall()) {
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);
}
public ConnectionServiceFocusManager getConnectionServiceFocusManager() {
return mConnectionSvrFocusMgr;
}
private boolean canHold(Call call) {
return call.can(Connection.CAPABILITY_HOLD) && call.getState() != CallState.DIALING;
}
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() {
synchronized (mLock) {
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() {
synchronized (mLock) {
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() {
synchronized (mLock) {
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}.
if (mCall.getState() == CallState.RINGING) {
mCall.answer(mVideoState);
setCallState(mCall, CallState.ANSWERED, "answered");
} else if (mCall.getState() == CallState.SIMULATED_RINGING) {
// If the call's in simulated ringing, we don't have to wait for the CS --
// we can just declare it active.
setCallState(mCall, CallState.ACTIVE, "answering simulated ringing");
Log.addEvent(mCall, LogUtils.Events.REQUEST_SIMULATED_ACCEPT);
} else if (mCall.getState() == CallState.ANSWERED) {
// In certain circumstances, the connection service can lose track of a request
// to answer a call. Therefore, if the user presses answer again, still send it
// on down, but log a warning in the process and don't change the call state.
mCall.answer(mVideoState);
Log.w(this, "Duplicate answer request for call %s", mCall.getId());
}
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();
}
}
}
public void resetConnectionTime(Call call) {
call.setConnectTimeMillis(System.currentTimeMillis());
call.setConnectElapsedTimeMillis(SystemClock.elapsedRealtime());
if (mCalls.contains(call)) {
for (CallsManagerListener listener : mListeners) {
listener.onConnectionTimeChanged(call);
}
}
}
public Context getContext() {
return mContext;
}
/**
* Determines if there is an ongoing emergency call. This can be either an outgoing emergency
* call, or a number which has been identified by the number as an emergency call.
* @return {@code true} if there is an ongoing emergency call, {@code false} otherwise.
*/
public boolean isInEmergencyCall() {
return mCalls.stream().filter(c -> (c.isEmergencyCall()
|| c.isNetworkIdentifiedEmergencyCall()) && !c.isDisconnected()).count() > 0;
}
/**
* Trigger a recalculation of support for CAPABILITY_CAN_PULL_CALL for external calls due to
* a possible emergency call being added/removed.
*/
private void updateExternalCallCanPullSupport() {
boolean isInEmergencyCall = isInEmergencyCall();
// Remove the capability to pull an external call in the case that we are in an emergency
// call.
mCalls.stream().filter(Call::isExternalCall).forEach(
c->c.setIsPullExternalCallSupported(!isInEmergencyCall));
}
/**
* Trigger display of an error message to the user; we do this outside of dialer for calls which
* fail to be created and added to Dialer.
* @param messageId The string resource id.
*/
private void showErrorMessage(int messageId) {
final Intent errorIntent = new Intent(mContext, ErrorDialogActivity.class);
errorIntent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, messageId);
errorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(errorIntent, UserHandle.CURRENT);
}
/**
* Handles changes to a {@link PhoneAccount}.
*
* Checks for changes to video calling availability and updates whether calls for that phone
* account are video capable.
*
* @param registrar The {@link PhoneAccountRegistrar} originating the change.
* @param phoneAccount The {@link PhoneAccount} which changed.
*/
private void handlePhoneAccountChanged(PhoneAccountRegistrar registrar,
PhoneAccount phoneAccount) {
Log.i(this, "handlePhoneAccountChanged: phoneAccount=%s", phoneAccount);
boolean isVideoNowSupported = phoneAccount.hasCapabilities(
PhoneAccount.CAPABILITY_VIDEO_CALLING);
mCalls.stream()
.filter(c -> phoneAccount.getAccountHandle().equals(c.getTargetPhoneAccount()))
.forEach(c -> c.setVideoCallingSupportedByPhoneAccount(isVideoNowSupported));
}
/**
* Determines if two {@link Call} instances originated from either the same target
* {@link PhoneAccountHandle} or connection manager {@link PhoneAccountHandle}.
* @param call1 The first call
* @param call2 The second call
* @return {@code true} if both calls are from the same target or connection manager
* {@link PhoneAccountHandle}.
*/
public static boolean areFromSameSource(@NonNull Call call1, @NonNull Call call2) {
PhoneAccountHandle call1ConnectionMgr = call1.getConnectionManagerPhoneAccount();
PhoneAccountHandle call2ConnectionMgr = call2.getConnectionManagerPhoneAccount();
if (call1ConnectionMgr != null && call2ConnectionMgr != null
&& PhoneAccountHandle.areFromSamePackage(call1ConnectionMgr, call2ConnectionMgr)) {
// Both calls share the same connection manager package, so they are from the same
// source.
return true;
}
PhoneAccountHandle call1TargetAcct = call1.getTargetPhoneAccount();
PhoneAccountHandle call2TargetAcct = call2.getTargetPhoneAccount();
// Otherwise if the target phone account for both is the same package, they're the same
// source.
return PhoneAccountHandle.areFromSamePackage(call1TargetAcct, call2TargetAcct);
}
public LinkedList<HandlerThread> getGraphHandlerThreads() {
return mGraphHandlerThreads;
}
private void maybeSendPostCallScreenIntent(Call call) {
if (call.isEmergencyCall() || (call.isNetworkIdentifiedEmergencyCall()) ||
(call.getPostCallPackageName() == null)) {
return;
}
Intent intent = new Intent(ACTION_POST_CALL);
intent.setPackage(call.getPostCallPackageName());
intent.putExtra(EXTRA_HANDLE, call.getHandle());
intent.putExtra(EXTRA_DISCONNECT_CAUSE, call.getDisconnectCause().getCode());
long duration = call.getAgeMillis();
int durationCode = DURATION_VERY_SHORT;
if ((duration >= VERY_SHORT_CALL_TIME_MS) && (duration < SHORT_CALL_TIME_MS)) {
durationCode = DURATION_SHORT;
} else if ((duration >= SHORT_CALL_TIME_MS) && (duration < MEDIUM_CALL_TIME_MS)) {
durationCode = DURATION_MEDIUM;
} else if (duration >= MEDIUM_CALL_TIME_MS) {
durationCode = DURATION_LONG;
}
intent.putExtra(EXTRA_CALL_DURATION, durationCode);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, mCurrentUserHandle);
}
@VisibleForTesting
public void addToPendingCallsToDisconnect(Call call) {
mPendingCallsToDisconnect.add(call);
}
@VisibleForTesting
public void addConnectionServiceRepositoryCache(ComponentName componentName,
UserHandle userHandle, ConnectionServiceWrapper service) {
mConnectionServiceRepository.setService(componentName, userHandle, service);
}
}