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.BluetoothDevice; 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.ServiceConnection; 23 import android.net.Uri; 24 import android.os.IBinder; 25 import android.telecom.Call; 26 import android.telecom.CallAudioState; 27 import android.telecom.PhoneAccount; 28 import android.telecom.PhoneAccountHandle; 29 import android.telecom.TelecomManager; 30 import android.text.TextUtils; 31 import android.widget.Toast; 32 33 import androidx.annotation.Nullable; 34 35 import com.android.car.dialer.R; 36 import com.android.car.dialer.bluetooth.PhoneAccountManager; 37 import com.android.car.dialer.log.L; 38 import com.android.car.telephony.common.CallDetail; 39 import com.android.car.telephony.common.TelecomUtils; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 45 import javax.inject.Inject; 46 import javax.inject.Singleton; 47 48 import dagger.hilt.android.qualifiers.ApplicationContext; 49 50 /** 51 * The entry point for all interactions between UI and telecom. 52 */ 53 @Singleton 54 public final class UiCallManager { 55 private static String TAG = "CD.TelecomMgr"; 56 57 private static final String EVENT_SCO_CONNECT = "com.android.bluetooth.hfpclient.SCO_CONNECT"; 58 private static final String EVENT_SCO_DISCONNECT = 59 "com.android.bluetooth.hfpclient.SCO_DISCONNECT"; 60 61 private Context mContext; 62 private final TelecomManager mTelecomManager; 63 private final PhoneAccountManager mPhoneAccountManager; 64 private InCallServiceImpl mInCallService; 65 66 @Inject UiCallManager( @pplicationContext Context context, TelecomManager telecomManager, PhoneAccountManager phoneAccountManager)67 UiCallManager( 68 @ApplicationContext Context context, 69 TelecomManager telecomManager, 70 PhoneAccountManager phoneAccountManager) { 71 L.d(TAG, "SetUp"); 72 mContext = context; 73 mTelecomManager = telecomManager; 74 mPhoneAccountManager = phoneAccountManager; 75 76 Intent intent = new Intent(context, InCallServiceImpl.class); 77 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 78 context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 79 } 80 81 private final ServiceConnection mInCallServiceConnection = new ServiceConnection() { 82 83 @Override 84 public void onServiceConnected(ComponentName name, IBinder binder) { 85 L.d(TAG, "onServiceConnected: %s, service: %s", name, binder); 86 mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); 87 } 88 89 @Override 90 public void onServiceDisconnected(ComponentName name) { 91 L.d(TAG, "onServiceDisconnected: %s", name); 92 mInCallService = null; 93 } 94 }; 95 96 /** 97 * Tears down the {@link UiCallManager}. Calling this function will null out the global 98 * accessible {@link UiCallManager} instance. Remember to re-initialize the 99 * {@link UiCallManager}. 100 */ tearDown()101 public void tearDown() { 102 if (mInCallService != null) { 103 mContext.unbindService(mInCallServiceConnection); 104 mInCallService = null; 105 } 106 // Clear out the mContext reference to avoid memory leak. 107 mContext = null; 108 } 109 getMuted()110 public boolean getMuted() { 111 L.d(TAG, "getMuted"); 112 if (mInCallService == null) { 113 return false; 114 } 115 CallAudioState audioState = mInCallService.getCallAudioState(); 116 return audioState != null && audioState.isMuted(); 117 } 118 setMuted(boolean muted)119 public void setMuted(boolean muted) { 120 L.d(TAG, "setMuted: " + muted); 121 if (mInCallService == null) { 122 return; 123 } 124 mInCallService.setMuted(muted); 125 } 126 getSupportedAudioRouteMask()127 public int getSupportedAudioRouteMask() { 128 L.d(TAG, "getSupportedAudioRouteMask"); 129 130 CallAudioState audioState = getCallAudioStateOrNull(); 131 return audioState != null ? audioState.getSupportedRouteMask() : 0; 132 } 133 134 /** Returns a list of supported CallAudioRoute for the given {@link PhoneAccountHandle}. */ getSupportedAudioRoute(@ullable PhoneAccountHandle phoneAccountHandle)135 public List<Integer> getSupportedAudioRoute(@Nullable PhoneAccountHandle phoneAccountHandle) { 136 List<Integer> audioRouteList = new ArrayList<>(); 137 138 BluetoothDevice device = mPhoneAccountManager.getMatchingDevice(phoneAccountHandle); 139 if (device != null) { 140 // if this is bluetooth phone call, we can only select audio route between vehicle 141 // and phone. 142 // Vehicle speaker route. 143 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 144 // Headset route. 145 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 146 } else { 147 // Most likely we are making phone call with on board SIM card. 148 int supportedAudioRouteMask = getSupportedAudioRouteMask(); 149 150 if ((supportedAudioRouteMask & CallAudioState.ROUTE_EARPIECE) != 0) { 151 audioRouteList.add(CallAudioState.ROUTE_EARPIECE); 152 } else if ((supportedAudioRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { 153 audioRouteList.add(CallAudioState.ROUTE_WIRED_HEADSET); 154 } 155 if ((supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0) { 156 audioRouteList.add(CallAudioState.ROUTE_BLUETOOTH); 157 } 158 if ((supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0) { 159 audioRouteList.add(CallAudioState.ROUTE_SPEAKER); 160 } 161 } 162 163 return audioRouteList; 164 } 165 166 /** 167 * Returns the current audio route given the SCO state. See {@link CallDetail#getScoState()}. 168 * The available routes are defined in {@link CallAudioState}. 169 */ getAudioRoute(int scoState)170 public int getAudioRoute(int scoState) { 171 if (scoState != CallDetail.STATE_AUDIO_ERROR) { 172 if (scoState == CallDetail.STATE_AUDIO_CONNECTED) { 173 return CallAudioState.ROUTE_BLUETOOTH; 174 } else { 175 return CallAudioState.ROUTE_EARPIECE; 176 } 177 } else { 178 CallAudioState audioState = getCallAudioStateOrNull(); 179 int audioRoute = audioState != null ? audioState.getRoute() : 0; 180 L.d(TAG, "getAudioRoute " + audioRoute); 181 return audioRoute; 182 } 183 } 184 185 /** 186 * Re-route the audio out phone of the ongoing phone call. 187 */ setAudioRoute(int audioRoute, Call primaryCall)188 public void setAudioRoute(int audioRoute, Call primaryCall) { 189 if (primaryCall == null) { 190 return; 191 } 192 193 boolean isConference = !primaryCall.getChildren().isEmpty() 194 && primaryCall.getDetails().hasProperty(Call.Details.PROPERTY_CONFERENCE); 195 Call call = isConference ? primaryCall.getChildren().get(0) : primaryCall; 196 197 if (audioRoute == CallAudioState.ROUTE_BLUETOOTH) { 198 call.sendCallEvent(EVENT_SCO_CONNECT, null); 199 setMuted(false); 200 } else if ((audioRoute & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) { 201 call.sendCallEvent(EVENT_SCO_DISCONNECT, null); 202 } 203 // TODO: Implement routing audio if current call is not a bluetooth call. 204 } 205 getCallAudioStateOrNull()206 private CallAudioState getCallAudioStateOrNull() { 207 return mInCallService != null ? mInCallService.getCallAudioState() : null; 208 } 209 210 /** 211 * Places call through TelecomManager 212 * 213 * @return {@code true} if a call is successfully placed, false if number is invalid. 214 */ placeCall(String number)215 public boolean placeCall(String number) { 216 if (isValidNumber(number)) { 217 Uri uri = Uri.fromParts("tel", number, null); 218 L.d(TAG, "android.telecom.TelecomManager#placeCall: %s", number); 219 220 try { 221 mTelecomManager.placeCall(uri, null); 222 return true; 223 } catch (IllegalStateException e) { 224 Toast.makeText(mContext, R.string.error_telephony_not_available, 225 Toast.LENGTH_SHORT).show(); 226 L.w(TAG, e.toString()); 227 return false; 228 } 229 } else { 230 L.d(TAG, "invalid number dialed", number); 231 Toast.makeText(mContext, R.string.error_invalid_phone_number, 232 Toast.LENGTH_SHORT).show(); 233 return false; 234 } 235 } 236 237 /** 238 * Runs basic validation check of a phone number, to verify it is not empty. 239 */ isValidNumber(String number)240 private boolean isValidNumber(String number) { 241 if (TextUtils.isEmpty(number)) { 242 return false; 243 } 244 return true; 245 } 246 callVoicemail()247 public void callVoicemail() { 248 L.d(TAG, "callVoicemail"); 249 250 String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext); 251 if (TextUtils.isEmpty(voicemailNumber)) { 252 L.w(TAG, "Unable to get voicemail number."); 253 return; 254 } 255 placeCall(voicemailNumber); 256 } 257 258 /** Check if emergency call is supported by any phone account. */ isEmergencyCallSupported()259 public boolean isEmergencyCallSupported() { 260 List<PhoneAccountHandle> phoneAccountHandleList = 261 mTelecomManager.getCallCapablePhoneAccounts(); 262 for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandleList) { 263 PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(phoneAccountHandle); 264 L.d(TAG, "phoneAccount: %s", phoneAccount); 265 if (phoneAccount != null && phoneAccount.hasCapabilities( 266 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 267 return true; 268 } 269 } 270 return false; 271 } 272 273 /** Return the current active call list from delegated {@link InCallServiceImpl} */ getCallList()274 public List<Call> getCallList() { 275 return mInCallService == null ? Collections.emptyList() : mInCallService.getCallList(); 276 } 277 } 278