| /* |
| * 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, Context.RECEIVER_EXPORTED); |
| 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(); |
| |
| StringBuffer creationLogs = new StringBuffer(); |
| creationLogs.append("requestedAcct:"); |
| if (requestedAccountHandle == null) { |
| creationLogs.append("none"); |
| } else { |
| creationLogs.append(requestedAccountHandle); |
| } |
| creationLogs.append(", selfMgd:"); |
| creationLogs.append(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, creationLogs.toString()); |
| |
| // 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; potentialAccts=%s", |
| potentialPhoneAccounts); |
| 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) { |
| Log.i(CallsManager.this, "findOutgoingCallPhoneAccount; contactPrefAcct=%s", |
| phoneAccountHandle); |
| 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)) { |
| Log.i(CallsManager.this, "findOutgoingCallPhoneAccount; defaultAcctForScheme=%s", |
| 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 = FrameworksUtils.makeAlertDialogBuilder(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); |
| } |
| } |