blob: 41d5a3b535a23dc80240b05ee78b540fa62e8135 [file] [log] [blame]
/*
* 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();
}
}