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