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