1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.dialer.telecom; 17 18 import android.bluetooth.BluetoothAdapter; 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHeadsetClient; 21 import android.bluetooth.BluetoothHeadsetClientCall; 22 import android.bluetooth.BluetoothProfile; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.ServiceConnection; 27 import android.net.Uri; 28 import android.os.IBinder; 29 import android.telecom.Call; 30 import android.telecom.CallAudioState; 31 import android.telecom.PhoneAccount; 32 import android.telecom.PhoneAccountHandle; 33 import android.telecom.TelecomManager; 34 import android.text.TextUtils; 35 import android.widget.Toast; 36 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.car.dialer.R; 40 import com.android.car.dialer.log.L; 41 import com.android.car.telephony.common.TelecomUtils; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.List; 46 47 /** 48 * The entry point for all interactions between UI and telecom. 49 */ 50 public class UiCallManager { 51 private static String TAG = "CD.TelecomMgr"; 52 53 @VisibleForTesting 54 static final String HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME 55 = "com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService"; 56 private static UiCallManager sUiCallManager; 57 58 private Context mContext; 59 60 private TelecomManager mTelecomManager; 61 private InCallServiceImpl mInCallService; 62 private BluetoothHeadsetClient mBluetoothHeadsetClient; 63 64 /** 65 * Initialized a globally accessible {@link UiCallManager} which can be retrieved by 66 * {@link #get}. If this function is called a second time before calling {@link #tearDown()}, 67 * an exception will be thrown. 68 * 69 * @param applicationContext Application context. 70 */ init(Context applicationContext)71 public static UiCallManager init(Context applicationContext) { 72 if (sUiCallManager == null) { 73 sUiCallManager = new UiCallManager(applicationContext); 74 } else { 75 throw new IllegalStateException("UiCallManager has been initialized."); 76 } 77 return sUiCallManager; 78 } 79 80 /** 81 * Gets the global {@link UiCallManager} instance. Make sure 82 * {@link #init(Context)} is called before calling this method. 83 */ get()84 public static UiCallManager get() { 85 if (sUiCallManager == null) { 86 throw new IllegalStateException( 87 "Call UiCallManager.init(Context) before calling this function"); 88 } 89 return sUiCallManager; 90 } 91 92 /** 93 * This is used only for testing 94 */ 95 @VisibleForTesting set(UiCallManager uiCallManager)96 public static void set(UiCallManager uiCallManager) { 97 sUiCallManager = uiCallManager; 98 } 99 UiCallManager(Context context)100 private UiCallManager(Context context) { 101 L.d(TAG, "SetUp"); 102 mContext = context; 103 104 mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); 105 Intent intent = new Intent(context, InCallServiceImpl.class); 106 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 107 context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 108 109 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 110 if (adapter != null) { 111 adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() { 112 @Override 113 public void onServiceConnected(int profile, BluetoothProfile proxy) { 114 if (profile == BluetoothProfile.HEADSET_CLIENT) { 115 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; 116 } 117 } 118 119 @Override 120 public void onServiceDisconnected(int profile) { 121 } 122 }, BluetoothProfile.HEADSET_CLIENT); 123 } 124 } 125 126 private final ServiceConnection mInCallServiceConnection = new ServiceConnection() { 127 128 @Override 129 public void onServiceConnected(ComponentName name, IBinder binder) { 130 L.d(TAG, "onServiceConnected: %s, service: %s", name, binder); 131 mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); 132 } 133 134 @Override 135 public void onServiceDisconnected(ComponentName name) { 136 L.d(TAG, "onServiceDisconnected: %s", name); 137 mInCallService = null; 138 } 139 }; 140 141 /** 142 * Tears down the {@link UiCallManager}. Calling this function will null out the global 143 * accessible {@link UiCallManager} instance. Remember to re-initialize the 144 * {@link UiCallManager}. 145 */ tearDown()146 public void tearDown() { 147 if (mInCallService != null) { 148 mContext.unbindService(mInCallServiceConnection); 149 mInCallService = null; 150 } 151 // Clear out the mContext reference to avoid memory leak. 152 mContext = null; 153 sUiCallManager = null; 154 } 155 getMuted()156 public boolean getMuted() { 157 L.d(TAG, "getMuted"); 158 if (mInCallService == null) { 159 return false; 160 } 161 CallAudioState audioState = mInCallService.getCallAudioState(); 162 return audioState != null && audioState.isMuted(); 163 } 164 setMuted(boolean muted)165 public void setMuted(boolean muted) { 166 L.d(TAG, "setMuted: " + muted); 167 if (mInCallService == null) { 168 return; 169 } 170 mInCallService.setMuted(muted); 171 } 172 getSupportedAudioRouteMask()173 public int getSupportedAudioRouteMask() { 174 L.d(TAG, "getSupportedAudioRouteMask"); 175 176 CallAudioState audioState = getCallAudioStateOrNull(); 177 return audioState != null ? audioState.getSupportedRouteMask() : 0; 178 } 179 getSupportedAudioRoute()180 public List<Integer> getSupportedAudioRoute() { 181 List<Integer> audioRouteList = new ArrayList<>(); 182 183 boolean isBluetoothPhoneCall = isBluetoothCall(); 184 if (isBluetoothPhoneCall) { 185 // if this is bluetooth phone call, we can only select audio route between vehicle 186 // and phone. 187 // Vehicle speaker route. 188 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 189 // Headset route. 190 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 191 } else { 192 // Most likely we are making phone call with on board SIM card. 193 int supportedAudioRouteMask = getSupportedAudioRouteMask(); 194 195 if ((supportedAudioRouteMask & CallAudioState.ROUTE_EARPIECE) != 0) { 196 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 197 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0) { 198 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 199 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { 200 audioRouteList.add(CallAudioState.ROUTE_WIRED_HEADSET); 201 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0) { 202 audioRouteList.add(CallAudioState.ROUTE_SPEAKER); 203 } 204 } 205 206 return audioRouteList; 207 } 208 isBluetoothCall()209 public boolean isBluetoothCall() { 210 PhoneAccountHandle phoneAccountHandle = 211 mTelecomManager.getUserSelectedOutgoingPhoneAccount(); 212 if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) { 213 return HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME.equals( 214 phoneAccountHandle.getComponentName().getClassName()); 215 } else { 216 return false; 217 } 218 } 219 220 /** 221 * Returns the current audio route. 222 * The available routes are defined in {@link CallAudioState}. 223 */ getAudioRoute()224 public int getAudioRoute() { 225 List<BluetoothDevice> devices = mBluetoothHeadsetClient != null 226 ? mBluetoothHeadsetClient.getConnectedDevices() 227 : Collections.emptyList(); 228 229 if (isBluetoothCall() && !devices.isEmpty()) { 230 // TODO: Make this handle multiple devices 231 BluetoothDevice device = devices.get(0); 232 int audioState = mBluetoothHeadsetClient.getAudioState(device); 233 234 if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) { 235 return CallAudioState.ROUTE_BLUETOOTH; 236 } else { 237 return CallAudioState.ROUTE_EARPIECE; 238 } 239 } else { 240 CallAudioState audioState = getCallAudioStateOrNull(); 241 int audioRoute = audioState != null ? audioState.getRoute() : 0; 242 L.d(TAG, "getAudioRoute " + audioRoute); 243 return audioRoute; 244 } 245 } 246 247 /** 248 * Re-route the audio out phone of the ongoing phone call. 249 */ setAudioRoute(int audioRoute)250 public void setAudioRoute(int audioRoute) { 251 if (mBluetoothHeadsetClient != null && isBluetoothCall()) { 252 for (BluetoothDevice device : mBluetoothHeadsetClient.getConnectedDevices()) { 253 List<BluetoothHeadsetClientCall> currentCalls = 254 mBluetoothHeadsetClient.getCurrentCalls(device); 255 if (currentCalls != null && !currentCalls.isEmpty()) { 256 if (audioRoute == CallAudioState.ROUTE_BLUETOOTH) { 257 mBluetoothHeadsetClient.connectAudio(device); 258 } else if ((audioRoute & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) { 259 mBluetoothHeadsetClient.disconnectAudio(device); 260 } 261 } 262 } 263 } 264 // TODO: Implement routing audio if current call is not a bluetooth call. 265 } 266 getCallAudioStateOrNull()267 private CallAudioState getCallAudioStateOrNull() { 268 return mInCallService != null ? mInCallService.getCallAudioState() : null; 269 } 270 271 /** 272 * Places call through TelecomManager 273 * 274 * @return {@code true} if a call is successfully placed, false if number is invalid. 275 */ placeCall(String number)276 public boolean placeCall(String number) { 277 if (isValidNumber(number)) { 278 Uri uri = Uri.fromParts("tel", number, null); 279 L.d(TAG, "android.telecom.TelecomManager#placeCall: %s", number); 280 281 try { 282 mTelecomManager.placeCall(uri, null); 283 return true; 284 } catch (IllegalStateException e) { 285 Toast.makeText(mContext, R.string.error_telephony_not_available, 286 Toast.LENGTH_SHORT).show(); 287 L.w(TAG, e.toString()); 288 return false; 289 } 290 } else { 291 L.d(TAG, "invalid number dialed", number); 292 Toast.makeText(mContext, R.string.error_invalid_phone_number, 293 Toast.LENGTH_SHORT).show(); 294 return false; 295 } 296 } 297 298 /** 299 * Runs basic validation check of a phone number, to verify it is not empty. 300 */ isValidNumber(String number)301 private boolean isValidNumber(String number) { 302 if (TextUtils.isEmpty(number)) { 303 return false; 304 } 305 return true; 306 } 307 callVoicemail()308 public void callVoicemail() { 309 L.d(TAG, "callVoicemail"); 310 311 String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext); 312 if (TextUtils.isEmpty(voicemailNumber)) { 313 L.w(TAG, "Unable to get voicemail number."); 314 return; 315 } 316 placeCall(voicemailNumber); 317 } 318 319 /** Check if emergency call is supported by any phone account. */ isEmergencyCallSupported()320 public boolean isEmergencyCallSupported() { 321 List<PhoneAccountHandle> phoneAccountHandleList = 322 mTelecomManager.getCallCapablePhoneAccounts(); 323 for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandleList) { 324 PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(phoneAccountHandle); 325 L.d(TAG, "phoneAccount: %s", phoneAccount); 326 if (phoneAccount != null && phoneAccount.hasCapabilities( 327 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 328 return true; 329 } 330 } 331 return false; 332 } 333 334 335 /** Return the current active call list from delegated {@link InCallServiceImpl} */ getCallList()336 public List<Call> getCallList() { 337 return mInCallService == null ? Collections.emptyList() : mInCallService.getCalls(); 338 } 339 } 340