| /* |
| * Copyright (C) 2015 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.car.dialer.telecom; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadsetClient; |
| import android.bluetooth.BluetoothHeadsetClientCall; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.net.Uri; |
| import android.os.IBinder; |
| import android.os.ServiceManager; |
| import android.telecom.Call; |
| import android.telecom.CallAudioState; |
| import android.telecom.CallAudioState.CallAudioRoute; |
| import android.telecom.PhoneAccount; |
| import android.telecom.PhoneAccountHandle; |
| import android.telecom.TelecomManager; |
| import android.text.TextUtils; |
| import android.widget.Toast; |
| |
| import com.android.car.dialer.R; |
| import com.android.car.dialer.bluetooth.BluetoothHeadsetClientProvider; |
| import com.android.car.dialer.log.L; |
| import com.android.car.telephony.common.TelecomUtils; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.telephony.ITelephony; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * The entry point for all interactions between UI and telecom. |
| */ |
| public class UiCallManager { |
| private static String TAG = "CD.TelecomMgr"; |
| |
| @VisibleForTesting |
| static final String HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME |
| = "com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService"; |
| private static UiCallManager sUiCallManager; |
| |
| private Context mContext; |
| |
| private TelecomManager mTelecomManager; |
| private InCallServiceImpl mInCallService; |
| private BluetoothHeadsetClientProvider mBluetoothHeadsetClientProvider; |
| |
| /** |
| * Initialized a globally accessible {@link UiCallManager} which can be retrieved by |
| * {@link #get}. If this function is called a second time before calling {@link #tearDown()}, |
| * an exception will be thrown. |
| * |
| * @param applicationContext Application context. |
| */ |
| public static UiCallManager init(Context applicationContext) { |
| if (sUiCallManager == null) { |
| sUiCallManager = new UiCallManager(applicationContext); |
| } else { |
| throw new IllegalStateException("UiCallManager has been initialized."); |
| } |
| return sUiCallManager; |
| } |
| |
| /** |
| * Gets the global {@link UiCallManager} instance. Make sure |
| * {@link #init(Context)} is called before calling this method. |
| */ |
| public static UiCallManager get() { |
| if (sUiCallManager == null) { |
| throw new IllegalStateException( |
| "Call UiCallManager.init(Context) before calling this function"); |
| } |
| return sUiCallManager; |
| } |
| |
| /** |
| * This is used only for testing |
| */ |
| @VisibleForTesting |
| public static void set(UiCallManager uiCallManager) { |
| sUiCallManager = uiCallManager; |
| } |
| |
| private UiCallManager(Context context) { |
| L.d(TAG, "SetUp"); |
| mContext = context; |
| |
| mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); |
| Intent intent = new Intent(context, InCallServiceImpl.class); |
| intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); |
| context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); |
| |
| mBluetoothHeadsetClientProvider = BluetoothHeadsetClientProvider.singleton(context); |
| } |
| |
| private final ServiceConnection mInCallServiceConnection = new ServiceConnection() { |
| |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder binder) { |
| L.d(TAG, "onServiceConnected: %s, service: %s", name, binder); |
| mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName name) { |
| L.d(TAG, "onServiceDisconnected: %s", name); |
| mInCallService = null; |
| } |
| }; |
| |
| /** |
| * Tears down the {@link UiCallManager}. Calling this function will null out the global |
| * accessible {@link UiCallManager} instance. Remember to re-initialize the |
| * {@link UiCallManager}. |
| */ |
| public void tearDown() { |
| if (mInCallService != null) { |
| mContext.unbindService(mInCallServiceConnection); |
| mInCallService = null; |
| } |
| // Clear out the mContext reference to avoid memory leak. |
| mContext = null; |
| sUiCallManager = null; |
| } |
| |
| public boolean getMuted() { |
| L.d(TAG, "getMuted"); |
| if (mInCallService == null) { |
| return false; |
| } |
| CallAudioState audioState = mInCallService.getCallAudioState(); |
| return audioState != null && audioState.isMuted(); |
| } |
| |
| public void setMuted(boolean muted) { |
| L.d(TAG, "setMuted: " + muted); |
| if (mInCallService == null) { |
| return; |
| } |
| mInCallService.setMuted(muted); |
| } |
| |
| public int getSupportedAudioRouteMask() { |
| L.d(TAG, "getSupportedAudioRouteMask"); |
| |
| CallAudioState audioState = getCallAudioStateOrNull(); |
| return audioState != null ? audioState.getSupportedRouteMask() : 0; |
| } |
| |
| public List<Integer> getSupportedAudioRoute() { |
| List<Integer> audioRouteList = new ArrayList<>(); |
| |
| boolean isBluetoothPhoneCall = isBluetoothCall(); |
| if (isBluetoothPhoneCall) { |
| // if this is bluetooth phone call, we can only select audio route between vehicle |
| // and phone. |
| // Vehicle speaker route. |
| audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); |
| // Headset route. |
| audioRouteList.add(CallAudioState.ROUTE_EARPIECE); |
| } else { |
| // Most likely we are making phone call with on board SIM card. |
| int supportedAudioRouteMask = getSupportedAudioRouteMask(); |
| |
| if ((supportedAudioRouteMask & CallAudioState.ROUTE_EARPIECE) != 0) { |
| audioRouteList.add(CallAudioState.ROUTE_EARPIECE); |
| } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0) { |
| audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); |
| } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { |
| audioRouteList.add(CallAudioState.ROUTE_WIRED_HEADSET); |
| } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0) { |
| audioRouteList.add(CallAudioState.ROUTE_SPEAKER); |
| } |
| } |
| |
| return audioRouteList; |
| } |
| |
| public boolean isBluetoothCall() { |
| PhoneAccountHandle phoneAccountHandle = |
| mTelecomManager.getUserSelectedOutgoingPhoneAccount(); |
| if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) { |
| return HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME.equals( |
| phoneAccountHandle.getComponentName().getClassName()); |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Returns the current audio route. If {@link BluetoothHeadsetClient} hasn't been connected, |
| * return {@link CallAudioState#ROUTE_EARPIECE}. |
| * The available routes are defined in {@link CallAudioState}. |
| */ |
| public int getAudioRoute() { |
| if (isBluetoothCall()) { |
| BluetoothHeadsetClient bluetoothHeadsetClient = mBluetoothHeadsetClientProvider.get(); |
| List<BluetoothDevice> devices = bluetoothHeadsetClient != null |
| ? bluetoothHeadsetClient.getConnectedDevices() |
| : Collections.emptyList(); |
| // BluetoothHeadsetClient might haven't been initialized that the proxy object hasn't |
| // been bind by calling BluetoothAdapter#getProfileProxy. |
| if (devices.isEmpty()) { |
| return CallAudioState.ROUTE_EARPIECE; |
| } |
| |
| // TODO: Make this handle multiple devices |
| BluetoothDevice device = devices.get(0); |
| int audioState = bluetoothHeadsetClient.getAudioState(device); |
| |
| if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) { |
| return CallAudioState.ROUTE_BLUETOOTH; |
| } else { |
| return CallAudioState.ROUTE_EARPIECE; |
| } |
| } else { |
| CallAudioState audioState = getCallAudioStateOrNull(); |
| int audioRoute = audioState != null ? audioState.getRoute() : 0; |
| L.d(TAG, "getAudioRoute " + audioRoute); |
| return audioRoute; |
| } |
| } |
| |
| /** |
| * Re-route the audio out phone of the ongoing phone call. |
| */ |
| public void setAudioRoute(@CallAudioRoute int audioRoute) { |
| BluetoothHeadsetClient bluetoothHeadsetClient = mBluetoothHeadsetClientProvider.get(); |
| if (bluetoothHeadsetClient != null && isBluetoothCall()) { |
| for (BluetoothDevice device : bluetoothHeadsetClient.getConnectedDevices()) { |
| List<BluetoothHeadsetClientCall> currentCalls = |
| bluetoothHeadsetClient.getCurrentCalls(device); |
| if (currentCalls != null && !currentCalls.isEmpty()) { |
| if (audioRoute == CallAudioState.ROUTE_BLUETOOTH) { |
| bluetoothHeadsetClient.connectAudio(device); |
| } else if ((audioRoute & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) { |
| bluetoothHeadsetClient.disconnectAudio(device); |
| } |
| } |
| } |
| } |
| // TODO: Implement routing audio if current call is not a bluetooth call. |
| } |
| |
| private CallAudioState getCallAudioStateOrNull() { |
| return mInCallService != null ? mInCallService.getCallAudioState() : null; |
| } |
| |
| /** |
| * Places call through TelecomManager |
| * |
| * @return {@code true} if a call is successfully placed, false if number is invalid. |
| */ |
| public boolean placeCall(String number) { |
| if (isValidNumber(number)) { |
| Uri uri = Uri.fromParts("tel", number, null); |
| L.d(TAG, "android.telecom.TelecomManager#placeCall: %s", number); |
| |
| // Check if ITelephony is present before placing a call to avoid crash. This is a |
| // temporary fix where system performance is the root cause of the availability of |
| // the service. |
| // TODO(b/138866013): revert when the system is stable and is able to bring up the |
| // ITelephony in time. |
| ITelephony telephony = ITelephony.Stub.asInterface( |
| ServiceManager.getService(Context.TELEPHONY_SERVICE)); |
| if (telephony != null) { |
| mTelecomManager.placeCall(uri, null); |
| return true; |
| } |
| |
| Toast.makeText(mContext, R.string.error_telephony_not_available, |
| Toast.LENGTH_SHORT).show(); |
| return false; |
| } else { |
| L.d(TAG, "invalid number dialed", number); |
| Toast.makeText(mContext, R.string.error_invalid_phone_number, |
| Toast.LENGTH_SHORT).show(); |
| return false; |
| } |
| } |
| |
| /** |
| * Runs basic validation check of a phone number, to verify it is not empty. |
| */ |
| private boolean isValidNumber(String number) { |
| if (TextUtils.isEmpty(number)) { |
| return false; |
| } |
| return true; |
| } |
| |
| public void callVoicemail() { |
| L.d(TAG, "callVoicemail"); |
| |
| String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext); |
| if (TextUtils.isEmpty(voicemailNumber)) { |
| L.w(TAG, "Unable to get voicemail number."); |
| return; |
| } |
| placeCall(voicemailNumber); |
| } |
| |
| /** Check if emergency call is supported by any phone account. */ |
| public boolean isEmergencyCallSupported() { |
| List<PhoneAccountHandle> phoneAccountHandleList = |
| mTelecomManager.getCallCapablePhoneAccounts(); |
| for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandleList) { |
| PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(phoneAccountHandle); |
| L.d(TAG, "phoneAccount: %s", phoneAccount); |
| if (phoneAccount != null && phoneAccount.hasCapabilities( |
| PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| /** Return the current active call list from delegated {@link InCallServiceImpl} */ |
| public List<Call> getCallList() { |
| return mInCallService == null ? Collections.emptyList() : mInCallService.getCalls(); |
| } |
| } |