1/*
2 * Copyright (C) 2013 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.phone;
18
19import com.google.android.collect.Lists;
20import com.google.common.base.Preconditions;
21
22import android.bluetooth.BluetoothAdapter;
23import android.bluetooth.BluetoothDevice;
24import android.bluetooth.BluetoothHeadset;
25import android.bluetooth.BluetoothProfile;
26import android.content.BroadcastReceiver;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.os.SystemClock;
31import android.os.SystemProperties;
32import android.util.Log;
33
34import com.android.internal.telephony.CallManager;
35import com.android.internal.telephony.Connection;
36import com.android.services.telephony.common.Call;
37
38import java.util.List;
39
40/**
41 * Listens to and caches bluetooth headset state.  Used By the AudioRouter for maintaining
42 * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
43 * headset to the phone call.
44 */
45public class BluetoothManager implements CallModeler.Listener {
46    private static final String LOG_TAG = BluetoothManager.class.getSimpleName();
47    private static final boolean DBG =
48            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
49    private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
50
51    private final BluetoothAdapter mBluetoothAdapter;
52    private final CallManager mCallManager;
53    private final Context mContext;
54    private final CallModeler mCallModeler;
55
56    private BluetoothHeadset mBluetoothHeadset;
57    private int mBluetoothHeadsetState = BluetoothProfile.STATE_DISCONNECTED;
58    private int mBluetoothHeadsetAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
59    private boolean mShowBluetoothIndication = false;
60    private boolean mBluetoothConnectionPending = false;
61    private long mBluetoothConnectionRequestTime;
62
63    // Broadcast receiver for various intent broadcasts (see onCreate())
64    private final BroadcastReceiver mReceiver = new BluetoothBroadcastReceiver();
65
66    private final List<BluetoothIndicatorListener> mListeners = Lists.newArrayList();
67
68    public BluetoothManager(Context context, CallManager callManager, CallModeler callModeler) {
69        mContext = context;
70        mCallManager = callManager;
71        mCallModeler = callModeler;
72        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
73
74        init(mContext);
75    }
76
77    /* package */ boolean isBluetoothHeadsetAudioOn() {
78        return (mBluetoothHeadsetAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
79    }
80
81    //
82    // Bluetooth helper methods.
83    //
84    // - BluetoothAdapter is the Bluetooth system service.  If
85    //   getDefaultAdapter() returns null
86    //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
87    //   to see if BT is enabled on the device.
88    //
89    // - BluetoothHeadset is the API for the control connection to a
90    //   Bluetooth Headset.  This lets you completely connect/disconnect a
91    //   headset (which we don't do from the Phone UI!) but also lets you
92    //   get the address of the currently active headset and see whether
93    //   it's currently connected.
94
95    /**
96     * @return true if the Bluetooth on/off switch in the UI should be
97     *         available to the user (i.e. if the device is BT-capable
98     *         and a headset is connected.)
99     */
100    /* package */ boolean isBluetoothAvailable() {
101        if (VDBG) log("isBluetoothAvailable()...");
102
103        // There's no need to ask the Bluetooth system service if BT is enabled:
104        //
105        //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
106        //    if ((adapter == null) || !adapter.isEnabled()) {
107        //        if (DBG) log("  ==> FALSE (BT not enabled)");
108        //        return false;
109        //    }
110        //    if (DBG) log("  - BT enabled!  device name " + adapter.getName()
111        //                 + ", address " + adapter.getAddress());
112        //
113        // ...since we already have a BluetoothHeadset instance.  We can just
114        // call isConnected() on that, and assume it'll be false if BT isn't
115        // enabled at all.
116
117        // Check if there's a connected headset, using the BluetoothHeadset API.
118        boolean isConnected = false;
119        if (mBluetoothHeadset != null) {
120            List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
121
122            if (deviceList.size() > 0) {
123                BluetoothDevice device = deviceList.get(0);
124                isConnected = true;
125
126                if (VDBG) log("  - headset state = " +
127                              mBluetoothHeadset.getConnectionState(device));
128                if (VDBG) log("  - headset address: " + device);
129                if (VDBG) log("  - isConnected: " + isConnected);
130            }
131        }
132
133        if (VDBG) log("  ==> " + isConnected);
134        return isConnected;
135    }
136
137    /**
138     * @return true if a BT Headset is available, and its audio is currently connected.
139     */
140    /* package */ boolean isBluetoothAudioConnected() {
141        if (mBluetoothHeadset == null) {
142            if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
143            return false;
144        }
145        List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
146
147        if (deviceList.isEmpty()) {
148            return false;
149        }
150        BluetoothDevice device = deviceList.get(0);
151        boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
152        if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
153        return isAudioOn;
154    }
155
156    /**
157     * Helper method used to control the onscreen "Bluetooth" indication;
158     * see InCallControlState.bluetoothIndicatorOn.
159     *
160     * @return true if a BT device is available and its audio is currently connected,
161     *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
162     *              call within the last 5 seconds (which presumably means
163     *              that the BT audio connection is currently being set
164     *              up, and will be connected soon.)
165     */
166    /* package */ boolean isBluetoothAudioConnectedOrPending() {
167        if (isBluetoothAudioConnected()) {
168            if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
169            return true;
170        }
171
172        // If we issued a connectAudio() call "recently enough", even
173        // if BT isn't actually connected yet, let's still pretend BT is
174        // on.  This makes the onscreen indication more responsive.
175        if (mBluetoothConnectionPending) {
176            long timeSinceRequest =
177                    SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
178            if (timeSinceRequest < 5000 /* 5 seconds */) {
179                if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
180                             + timeSinceRequest + " msec ago)");
181                return true;
182            } else {
183                if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
184                             + timeSinceRequest + " msec ago)");
185                mBluetoothConnectionPending = false;
186                return false;
187            }
188        }
189
190        if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE");
191        return false;
192    }
193
194    /**
195     * @return true if the onscreen UI should currently be showing the
196     * special "bluetooth is active" indication in a couple of places (in
197     * which UI elements turn blue and/or show the bluetooth logo.)
198     *
199     * This depends on the BluetoothHeadset state *and* the current
200     * telephony state; see shouldShowBluetoothIndication().
201     *
202     * @see CallCard
203     * @see NotificationMgr.updateInCallNotification
204     */
205    /* package */ boolean showBluetoothIndication() {
206        return mShowBluetoothIndication;
207    }
208
209    /**
210     * Recomputes the mShowBluetoothIndication flag based on the current
211     * bluetooth state and current telephony state.
212     *
213     * This needs to be called any time the bluetooth headset state or the
214     * telephony state changes.
215     */
216    /* package */ void updateBluetoothIndication() {
217        mShowBluetoothIndication = shouldShowBluetoothIndication(mBluetoothHeadsetState,
218                                                                 mBluetoothHeadsetAudioState,
219                                                                 mCallManager);
220
221        notifyListeners(mShowBluetoothIndication);
222    }
223
224    public void addBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
225        if (!mListeners.contains(listener)) {
226            mListeners.add(listener);
227        }
228    }
229
230    public void removeBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
231        if (mListeners.contains(listener)) {
232            mListeners.remove(listener);
233        }
234    }
235
236    private void notifyListeners(boolean showBluetoothOn) {
237        for (int i = 0; i < mListeners.size(); i++) {
238            mListeners.get(i).onBluetoothIndicationChange(showBluetoothOn, this);
239        }
240    }
241
242    private void init(Context context) {
243        Preconditions.checkNotNull(context);
244
245        if (mBluetoothAdapter != null) {
246            mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
247                                    BluetoothProfile.HEADSET);
248        }
249
250        // Register for misc other intent broadcasts.
251        IntentFilter intentFilter =
252                new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
253        intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
254        context.registerReceiver(mReceiver, intentFilter);
255
256        mCallModeler.addListener(this);
257    }
258
259    private void tearDown() {
260        if (mBluetoothHeadset != null) {
261            mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
262            mBluetoothHeadset = null;
263        }
264    }
265
266    private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
267             new BluetoothProfile.ServiceListener() {
268         @Override
269         public void onServiceConnected(int profile, BluetoothProfile proxy) {
270             mBluetoothHeadset = (BluetoothHeadset) proxy;
271             if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
272         }
273
274         @Override
275         public void onServiceDisconnected(int profile) {
276             mBluetoothHeadset = null;
277         }
278    };
279
280    /**
281     * UI policy helper function for the couple of places in the UI that
282     * have some way of indicating that "bluetooth is in use."
283     *
284     * @return true if the onscreen UI should indicate that "bluetooth is in use",
285     *         based on the specified bluetooth headset state, and the
286     *         current state of the phone.
287     * @see showBluetoothIndication()
288     */
289    private static boolean shouldShowBluetoothIndication(int bluetoothState,
290                                                         int bluetoothAudioState,
291                                                         CallManager cm) {
292        // We want the UI to indicate that "bluetooth is in use" in two
293        // slightly different cases:
294        //
295        // (a) The obvious case: if a bluetooth headset is currently in
296        //     use for an ongoing call.
297        //
298        // (b) The not-so-obvious case: if an incoming call is ringing,
299        //     and we expect that audio *will* be routed to a bluetooth
300        //     headset once the call is answered.
301
302        switch (cm.getState()) {
303            case OFFHOOK:
304                // This covers normal active calls, and also the case if
305                // the foreground call is DIALING or ALERTING.  In this
306                // case, bluetooth is considered "active" if a headset
307                // is connected *and* audio is being routed to it.
308                return ((bluetoothState == BluetoothHeadset.STATE_CONNECTED)
309                        && (bluetoothAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED));
310
311            case RINGING:
312                // If an incoming call is ringing, we're *not* yet routing
313                // audio to the headset (since there's no in-call audio
314                // yet!)  In this case, if a bluetooth headset is
315                // connected at all, we assume that it'll become active
316                // once the user answers the phone.
317                return (bluetoothState == BluetoothHeadset.STATE_CONNECTED);
318
319            default:  // Presumably IDLE
320                return false;
321        }
322    }
323
324    private void dumpBluetoothState() {
325        log("============== dumpBluetoothState() =============");
326        log("= isBluetoothAvailable: " + isBluetoothAvailable());
327        log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
328        log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
329        log("= PhoneApp.showBluetoothIndication: "
330            + showBluetoothIndication());
331        log("=");
332        if (mBluetoothAdapter != null) {
333            if (mBluetoothHeadset != null) {
334                List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
335
336                if (deviceList.size() > 0) {
337                    BluetoothDevice device = deviceList.get(0);
338                    log("= BluetoothHeadset.getCurrentDevice: " + device);
339                    log("= BluetoothHeadset.State: "
340                        + mBluetoothHeadset.getConnectionState(device));
341                    log("= BluetoothHeadset audio connected: " +
342                        mBluetoothHeadset.isAudioConnected(device));
343                }
344            } else {
345                log("= mBluetoothHeadset is null");
346            }
347        } else {
348            log("= mBluetoothAdapter is null; device is not BT capable");
349        }
350    }
351
352    /* package */ void connectBluetoothAudio() {
353        if (VDBG) log("connectBluetoothAudio()...");
354        if (mBluetoothHeadset != null) {
355            // TODO(BT) check return
356            mBluetoothHeadset.connectAudio();
357        }
358
359        // Watch out: The bluetooth connection doesn't happen instantly;
360        // the connectAudio() call returns instantly but does its real
361        // work in another thread.  The mBluetoothConnectionPending flag
362        // is just a little trickery to ensure that the onscreen UI updates
363        // instantly. (See isBluetoothAudioConnectedOrPending() above.)
364        mBluetoothConnectionPending = true;
365        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
366    }
367
368    /* package */ void disconnectBluetoothAudio() {
369        if (VDBG) log("disconnectBluetoothAudio()...");
370        if (mBluetoothHeadset != null) {
371            mBluetoothHeadset.disconnectAudio();
372        }
373        mBluetoothConnectionPending = false;
374    }
375
376    /**
377     * Receiver for misc intent broadcasts the BluetoothManager cares about.
378     */
379    private class BluetoothBroadcastReceiver extends BroadcastReceiver {
380        @Override
381        public void onReceive(Context context, Intent intent) {
382            String action = intent.getAction();
383
384            if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
385                mBluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
386                                                          BluetoothHeadset.STATE_DISCONNECTED);
387                if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
388                if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetState);
389                // Also update any visible UI if necessary
390                updateBluetoothIndication();
391            } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
392                mBluetoothHeadsetAudioState =
393                        intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
394                                           BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
395                if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
396                if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetAudioState);
397                updateBluetoothIndication();
398            }
399        }
400    }
401
402    @Override
403    public void onDisconnect(Call call) {
404        updateBluetoothIndication();
405    }
406
407    @Override
408    public void onIncoming(Call call) {
409        // An incoming call can affect bluetooth indicator, so we update it whenever there is
410        // a change to any of the calls.
411        updateBluetoothIndication();
412    }
413
414    @Override
415    public void onUpdate(List<Call> calls) {
416        updateBluetoothIndication();
417    }
418
419    @Override
420    public void onPostDialAction(Connection.PostDialState state, int callId, String chars, char c) {
421        // no-op
422    }
423
424    private void log(String msg) {
425        Log.d(LOG_TAG, msg);
426    }
427
428    /* package */ interface BluetoothIndicatorListener {
429        public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager manager);
430    }
431}
432