1 /*
2  * Copyright 2017 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 
17 package com.android.bluetooth.hfp;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothHeadset;
23 import android.bluetooth.IBluetoothHeadsetPhone;
24 import android.content.ActivityNotFoundException;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.ServiceConnection;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.media.AudioManager;
33 import android.os.IBinder;
34 import android.os.PowerManager;
35 import android.os.RemoteException;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.util.List;
41 
42 /**
43  * Defines system calls that is used by state machine/service to either send or receive
44  * messages from the Android System.
45  */
46 @VisibleForTesting
47 public class HeadsetSystemInterface {
48     private static final String TAG = HeadsetSystemInterface.class.getSimpleName();
49     private static final boolean DBG = false;
50 
51     private final HeadsetService mHeadsetService;
52     private final AudioManager mAudioManager;
53     private final HeadsetPhoneState mHeadsetPhoneState;
54     private PowerManager.WakeLock mVoiceRecognitionWakeLock;
55     private volatile IBluetoothHeadsetPhone mPhoneProxy;
56     private final ServiceConnection mPhoneProxyConnection = new ServiceConnection() {
57         @Override
58         public void onServiceConnected(ComponentName className, IBinder service) {
59             if (DBG) {
60                 Log.d(TAG, "Proxy object connected");
61             }
62             synchronized (HeadsetSystemInterface.this) {
63                 mPhoneProxy = IBluetoothHeadsetPhone.Stub.asInterface(service);
64             }
65         }
66 
67         @Override
68         public void onServiceDisconnected(ComponentName className) {
69             if (DBG) {
70                 Log.d(TAG, "Proxy object disconnected");
71             }
72             synchronized (HeadsetSystemInterface.this) {
73                 mPhoneProxy = null;
74             }
75         }
76     };
77 
HeadsetSystemInterface(HeadsetService headsetService)78     HeadsetSystemInterface(HeadsetService headsetService) {
79         if (headsetService == null) {
80             Log.wtf(TAG, "HeadsetService parameter is null");
81         }
82         mHeadsetService = headsetService;
83         mAudioManager = (AudioManager) mHeadsetService.getSystemService(Context.AUDIO_SERVICE);
84         PowerManager powerManager =
85                 (PowerManager) mHeadsetService.getSystemService(Context.POWER_SERVICE);
86         mVoiceRecognitionWakeLock =
87                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":VoiceRecognition");
88         mVoiceRecognitionWakeLock.setReferenceCounted(false);
89         mHeadsetPhoneState = new HeadsetPhoneState(mHeadsetService);
90     }
91 
92     /**
93      * Initialize this system interface
94      */
init()95     public synchronized void init() {
96         // Bind to Telecom phone proxy service
97         Intent intent = new Intent(IBluetoothHeadsetPhone.class.getName());
98         intent.setComponent(resolveSystemService(mHeadsetService.getPackageManager(), 0, intent));
99         if (intent.getComponent() == null || !mHeadsetService.bindService(intent,
100                 mPhoneProxyConnection, 0)) {
101             // Crash the stack if cannot bind to Telecom
102             Log.wtf(TAG, "Could not bind to IBluetoothHeadsetPhone Service, intent=" + intent);
103         }
104     }
105 
106     /**
107      * Special function for use by the system to resolve service
108      * intents to system apps.  Throws an exception if there are
109      * multiple potential matches to the Intent.  Returns null if
110      * there are no matches.
111      */
resolveSystemService(@onNull PackageManager pm, @PackageManager.ComponentInfoFlags int flags, Intent intent)112     private @Nullable ComponentName resolveSystemService(@NonNull PackageManager pm,
113             @PackageManager.ComponentInfoFlags int flags, Intent intent) {
114         if (intent.getComponent() != null) {
115             return intent.getComponent();
116         }
117 
118         List<ResolveInfo> results = pm.queryIntentServices(intent, flags);
119         if (results == null) {
120             return null;
121         }
122         ComponentName comp = null;
123         for (int i = 0; i < results.size(); i++) {
124             ResolveInfo ri = results.get(i);
125             if ((ri.serviceInfo.applicationInfo.flags& ApplicationInfo.FLAG_SYSTEM) == 0) {
126                 continue;
127             }
128             ComponentName foundComp = new ComponentName(ri.serviceInfo.applicationInfo.packageName,
129                     ri.serviceInfo.name);
130             if (comp != null) {
131                 throw new IllegalStateException("Multiple system services handle " + this
132                         + ": " + comp + ", " + foundComp);
133             }
134             comp = foundComp;
135         }
136         return comp;
137     }
138 
139     /**
140      * Stop this system interface
141      */
stop()142     public synchronized void stop() {
143         if (mPhoneProxy != null) {
144             if (DBG) {
145                 Log.d(TAG, "Unbinding phone proxy");
146             }
147             mPhoneProxy = null;
148             // Synchronization should make sure unbind can be successful
149             mHeadsetService.unbindService(mPhoneProxyConnection);
150         }
151         mHeadsetPhoneState.cleanup();
152     }
153 
154     /**
155      * Get audio manager. Most audio manager oprations are pass through and therefore are not
156      * individually managed by this class
157      *
158      * @return audio manager for setting audio parameters
159      */
160     @VisibleForTesting
getAudioManager()161     public AudioManager getAudioManager() {
162         return mAudioManager;
163     }
164 
165     /**
166      * Get wake lock for voice recognition
167      *
168      * @return wake lock for voice recognition
169      */
170     @VisibleForTesting
getVoiceRecognitionWakeLock()171     public PowerManager.WakeLock getVoiceRecognitionWakeLock() {
172         return mVoiceRecognitionWakeLock;
173     }
174 
175     /**
176      * Get HeadsetPhoneState instance to interact with Telephony service
177      *
178      * @return HeadsetPhoneState interface to interact with Telephony service
179      */
180     @VisibleForTesting
getHeadsetPhoneState()181     public HeadsetPhoneState getHeadsetPhoneState() {
182         return mHeadsetPhoneState;
183     }
184 
185     /**
186      * Answer the current incoming call in Telecom service
187      *
188      * @param device the Bluetooth device used for answering this call
189      */
190     @VisibleForTesting
answerCall(BluetoothDevice device)191     public void answerCall(BluetoothDevice device) {
192         if (device == null) {
193             Log.w(TAG, "answerCall device is null");
194             return;
195         }
196 
197         if (mPhoneProxy != null) {
198             try {
199                 mHeadsetService.setActiveDevice(device);
200                 mPhoneProxy.answerCall();
201             } catch (RemoteException e) {
202                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
203             }
204         } else {
205             Log.e(TAG, "Handsfree phone proxy null for answering call");
206         }
207     }
208 
209     /**
210      * Hangup the current call, could either be Telecom call or virtual call
211      *
212      * @param device the Bluetooth device used for hanging up this call
213      */
214     @VisibleForTesting
hangupCall(BluetoothDevice device)215     public void hangupCall(BluetoothDevice device) {
216         if (device == null) {
217             Log.w(TAG, "hangupCall device is null");
218             return;
219         }
220         // Close the virtual call if active. Virtual call should be
221         // terminated for CHUP callback event
222         if (mHeadsetService.isVirtualCallStarted()) {
223             mHeadsetService.stopScoUsingVirtualVoiceCall();
224         } else {
225             if (mPhoneProxy != null) {
226                 try {
227                     mPhoneProxy.hangupCall();
228                 } catch (RemoteException e) {
229                     Log.e(TAG, Log.getStackTraceString(new Throwable()));
230                 }
231             } else {
232                 Log.e(TAG, "Handsfree phone proxy null for hanging up call");
233             }
234         }
235     }
236 
237     /**
238      * Instructs Telecom to play the specified DTMF tone for the current foreground call
239      *
240      * @param dtmf dtmf code
241      * @param device the Bluetooth device that sent this code
242      */
243     @VisibleForTesting
sendDtmf(int dtmf, BluetoothDevice device)244     public boolean sendDtmf(int dtmf, BluetoothDevice device) {
245         if (device == null) {
246             Log.w(TAG, "sendDtmf device is null");
247             return false;
248         }
249         if (mPhoneProxy != null) {
250             try {
251                 return mPhoneProxy.sendDtmf(dtmf);
252             } catch (RemoteException e) {
253                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
254             }
255         } else {
256             Log.e(TAG, "Handsfree phone proxy null for sending DTMF");
257         }
258         return false;
259     }
260 
261     /**
262      * Instructs Telecom hold an incoming call
263      *
264      * @param chld index of the call to hold
265      */
266     @VisibleForTesting
processChld(int chld)267     public boolean processChld(int chld) {
268         if (mPhoneProxy != null) {
269             try {
270                 return mPhoneProxy.processChld(chld);
271             } catch (RemoteException e) {
272                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
273             }
274         } else {
275             Log.e(TAG, "Handsfree phone proxy null for sending DTMF");
276         }
277         return false;
278     }
279 
280     /**
281      * Get the the alphabetic name of current registered operator.
282      *
283      * @return null on error, empty string if not available
284      */
285     @VisibleForTesting
getNetworkOperator()286     public String getNetworkOperator() {
287         final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
288         if (phoneProxy == null) {
289             Log.e(TAG, "getNetworkOperator() failed: mPhoneProxy is null");
290             return null;
291         }
292         try {
293             // Should never return null
294             return mPhoneProxy.getNetworkOperator();
295         } catch (RemoteException exception) {
296             Log.e(TAG, "getNetworkOperator() failed: " + exception.getMessage());
297             exception.printStackTrace();
298             return null;
299         }
300     }
301 
302     /**
303      * Get the phone number of this device
304      *
305      * @return null if unavailable
306      */
307     @VisibleForTesting
getSubscriberNumber()308     public String getSubscriberNumber() {
309         final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
310         if (phoneProxy == null) {
311             Log.e(TAG, "getSubscriberNumber() failed: mPhoneProxy is null");
312             return null;
313         }
314         try {
315             return mPhoneProxy.getSubscriberNumber();
316         } catch (RemoteException exception) {
317             Log.e(TAG, "getSubscriberNumber() failed: " + exception.getMessage());
318             exception.printStackTrace();
319             return null;
320         }
321     }
322 
323 
324     /**
325      * Ask the Telecomm service to list current list of calls through CLCC response
326      * {@link BluetoothHeadset#clccResponse(int, int, int, int, boolean, String, int)}
327      *
328      * @return
329      */
330     @VisibleForTesting
listCurrentCalls()331     public boolean listCurrentCalls() {
332         final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
333         if (phoneProxy == null) {
334             Log.e(TAG, "listCurrentCalls() failed: mPhoneProxy is null");
335             return false;
336         }
337         try {
338             return mPhoneProxy.listCurrentCalls();
339         } catch (RemoteException exception) {
340             Log.e(TAG, "listCurrentCalls() failed: " + exception.getMessage());
341             exception.printStackTrace();
342             return false;
343         }
344     }
345 
346     /**
347      * Request Telecom service to send an update of the current call state to the headset service
348      * through {@link BluetoothHeadset#phoneStateChanged(int, int, int, String, int)}
349      */
350     @VisibleForTesting
queryPhoneState()351     public void queryPhoneState() {
352         final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
353         if (phoneProxy != null) {
354             try {
355                 mPhoneProxy.queryPhoneState();
356             } catch (RemoteException e) {
357                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
358             }
359         } else {
360             Log.e(TAG, "Handsfree phone proxy null for query phone state");
361         }
362     }
363 
364     /**
365      * Check if we are currently in a phone call
366      *
367      * @return True iff we are in a phone call
368      */
369     @VisibleForTesting
isInCall()370     public boolean isInCall() {
371         return ((mHeadsetPhoneState.getNumActiveCall() > 0) || (mHeadsetPhoneState.getNumHeldCall()
372                 > 0) || ((mHeadsetPhoneState.getCallState() != HeadsetHalConstants.CALL_STATE_IDLE)
373                 && (mHeadsetPhoneState.getCallState() != HeadsetHalConstants.CALL_STATE_INCOMING)));
374     }
375 
376     /**
377      * Check if there is currently an incoming call
378      *
379      * @return True iff there is an incoming call
380      */
381     @VisibleForTesting
isRinging()382     public boolean isRinging() {
383         return mHeadsetPhoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING;
384     }
385 
386     /**
387      * Check if call status is idle
388      *
389      * @return true if call state is neither ringing nor in call
390      */
391     @VisibleForTesting
isCallIdle()392     public boolean isCallIdle() {
393         return !isInCall() && !isRinging();
394     }
395 
396     /**
397      * Activate voice recognition on Android system
398      *
399      * @return true if activation succeeds, caller should wait for
400      * {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} callback that will then
401      * trigger {@link HeadsetService#startVoiceRecognition(BluetoothDevice)}, false if failed to
402      * activate
403      */
404     @VisibleForTesting
activateVoiceRecognition()405     public boolean activateVoiceRecognition() {
406         Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
407         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
408         try {
409             mHeadsetService.startActivity(intent);
410         } catch (ActivityNotFoundException e) {
411             Log.e(TAG, "activateVoiceRecognition, failed due to activity not found for " + intent);
412             return false;
413         }
414         return true;
415     }
416 
417     /**
418      * Deactivate voice recognition on Android system
419      *
420      * @return true if activation succeeds, caller should wait for
421      * {@link BluetoothHeadset#stopVoiceRecognition(BluetoothDevice)} callback that will then
422      * trigger {@link HeadsetService#stopVoiceRecognition(BluetoothDevice)}, false if failed to
423      * activate
424      */
425     @VisibleForTesting
deactivateVoiceRecognition()426     public boolean deactivateVoiceRecognition() {
427         // TODO: need a method to deactivate voice recognition on Android
428         return true;
429     }
430 
431 }
432