• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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