1/*
2 * Copyright (C) 2014 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.server.telecom;
18
19import android.bluetooth.BluetoothDevice;
20import android.bluetooth.BluetoothHeadset;
21import android.bluetooth.BluetoothProfile;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.SystemClock;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.IndentingPrintWriter;
32
33import java.util.List;
34
35/**
36 * Listens to and caches bluetooth headset state.  Used By the CallAudioManager for maintaining
37 * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
38 */
39public class BluetoothManager {
40    public static final int BLUETOOTH_UNINITIALIZED = 0;
41    public static final int BLUETOOTH_DISCONNECTED = 1;
42    public static final int BLUETOOTH_DEVICE_CONNECTED = 2;
43    public static final int BLUETOOTH_AUDIO_PENDING = 3;
44    public static final int BLUETOOTH_AUDIO_CONNECTED = 4;
45
46    public interface BluetoothStateListener {
47        void onBluetoothStateChange(int oldState, int newState);
48    }
49
50    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
51            new BluetoothProfile.ServiceListener() {
52                @Override
53                public void onServiceConnected(int profile, BluetoothProfile proxy) {
54                    Log.startSession("BMSL.oSC");
55                    try {
56                        if (profile == BluetoothProfile.HEADSET) {
57                            mBluetoothHeadset = new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
58                            Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
59                        } else {
60                            Log.w(this, "Connected to non-headset bluetooth service. Not changing" +
61                                    " bluetooth headset.");
62                        }
63                        updateListenerOfBluetoothState(true);
64                    } finally {
65                        Log.endSession();
66                    }
67                }
68
69                @Override
70                public void onServiceDisconnected(int profile) {
71                    Log.startSession("BMSL.oSD");
72                    try {
73                        mBluetoothHeadset = null;
74                        Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
75                        updateListenerOfBluetoothState(false);
76                    } finally {
77                        Log.endSession();
78                    }
79                }
80           };
81
82    /**
83     * Receiver for misc intent broadcasts the BluetoothManager cares about.
84     */
85    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
86        @Override
87        public void onReceive(Context context, Intent intent) {
88            Log.startSession("BM.oR");
89            try {
90                String action = intent.getAction();
91
92                if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
93                    int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
94                            BluetoothHeadset.STATE_DISCONNECTED);
95                    Log.i(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
96                    Log.i(this, "==> new state: %s ", bluetoothHeadsetState);
97                    updateListenerOfBluetoothState(
98                            bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTING);
99                } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
100                    int bluetoothHeadsetAudioState =
101                            intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
102                                    BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
103                    Log.i(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
104                    Log.i(this, "==> new state: %s", bluetoothHeadsetAudioState);
105                    updateListenerOfBluetoothState(
106                            bluetoothHeadsetAudioState ==
107                                    BluetoothHeadset.STATE_AUDIO_CONNECTING
108                            || bluetoothHeadsetAudioState ==
109                                    BluetoothHeadset.STATE_AUDIO_CONNECTED);
110                }
111            } finally {
112                Log.endSession();
113            }
114        }
115    };
116
117    private final Handler mHandler = new Handler(Looper.getMainLooper());
118
119    private final BluetoothAdapterProxy mBluetoothAdapter;
120    private BluetoothStateListener mBluetoothStateListener;
121
122    private BluetoothHeadsetProxy mBluetoothHeadset;
123    private long mBluetoothConnectionRequestTime;
124    private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA", null /*lock*/) {
125        @Override
126        public void loggedRun() {
127            if (!isBluetoothAudioConnected()) {
128                Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " +
129                        "connection. Updating UI.");
130            }
131            updateListenerOfBluetoothState(false);
132        }
133    };
134
135    private final Runnable mRetryConnectAudio = new Runnable("BM.rCA", null /*lock*/) {
136        @Override
137        public void loggedRun() {
138            Log.i(this, "Retrying connecting to bluetooth audio.");
139            if (!mBluetoothHeadset.connectAudio()) {
140                Log.w(this, "Retry of bluetooth audio connection failed. Giving up.");
141            } else {
142                setBluetoothStatePending();
143            }
144        }
145    };
146
147    private final Context mContext;
148    private int mBluetoothState = BLUETOOTH_UNINITIALIZED;
149
150    public BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy) {
151        mBluetoothAdapter = bluetoothAdapterProxy;
152        mContext = context;
153
154        if (mBluetoothAdapter != null) {
155            mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
156                                    BluetoothProfile.HEADSET);
157        }
158
159        // Register for misc other intent broadcasts.
160        IntentFilter intentFilter =
161                new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
162        intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
163        context.registerReceiver(mReceiver, intentFilter);
164    }
165
166    public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
167        mBluetoothStateListener = bluetoothStateListener;
168    }
169
170    //
171    // Bluetooth helper methods.
172    //
173    // - BluetoothAdapter is the Bluetooth system service.  If
174    //   getDefaultAdapter() returns null
175    //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
176    //   to see if BT is enabled on the device.
177    //
178    // - BluetoothHeadset is the API for the control connection to a
179    //   Bluetooth Headset.  This lets you completely connect/disconnect a
180    //   headset (which we don't do from the Phone UI!) but also lets you
181    //   get the address of the currently active headset and see whether
182    //   it's currently connected.
183
184    /**
185     * @return true if the Bluetooth on/off switch in the UI should be
186     *         available to the user (i.e. if the device is BT-capable
187     *         and a headset is connected.)
188     */
189    @VisibleForTesting
190    public boolean isBluetoothAvailable() {
191        Log.v(this, "isBluetoothAvailable()...");
192
193        // There's no need to ask the Bluetooth system service if BT is enabled:
194        //
195        //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
196        //    if ((adapter == null) || !adapter.isEnabled()) {
197        //        Log.d(this, "  ==> FALSE (BT not enabled)");
198        //        return false;
199        //    }
200        //    Log.d(this, "  - BT enabled!  device name " + adapter.getName()
201        //                 + ", address " + adapter.getAddress());
202        //
203        // ...since we already have a BluetoothHeadset instance.  We can just
204        // call isConnected() on that, and assume it'll be false if BT isn't
205        // enabled at all.
206
207        // Check if there's a connected headset, using the BluetoothHeadset API.
208        boolean isConnected = false;
209        if (mBluetoothHeadset != null) {
210            List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
211
212            if (deviceList.size() > 0) {
213                isConnected = true;
214                for (int i = 0; i < deviceList.size(); i++) {
215                    BluetoothDevice device = deviceList.get(i);
216                    Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device)
217                            + "for headset: " + device);
218                }
219            }
220        }
221
222        Log.v(this, "  ==> " + isConnected);
223        return isConnected;
224    }
225
226    /**
227     * @return true if a BT Headset is available, and its audio is currently connected.
228     */
229    @VisibleForTesting
230    public boolean isBluetoothAudioConnected() {
231        if (mBluetoothHeadset == null) {
232            Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
233            return false;
234        }
235        List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
236
237        if (deviceList.isEmpty()) {
238            return false;
239        }
240        for (int i = 0; i < deviceList.size(); i++) {
241            BluetoothDevice device = deviceList.get(i);
242            boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
243            Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
244                    + "for headset: " + device);
245            if (isAudioOn) {
246                return true;
247            }
248        }
249        return false;
250    }
251
252    /**
253     * Helper method used to control the onscreen "Bluetooth" indication;
254     *
255     * @return true if a BT device is available and its audio is currently connected,
256     *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
257     *              call within the last 5 seconds (which presumably means
258     *              that the BT audio connection is currently being set
259     *              up, and will be connected soon.)
260     */
261    @VisibleForTesting
262    public boolean isBluetoothAudioConnectedOrPending() {
263        if (isBluetoothAudioConnected()) {
264            Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
265            return true;
266        }
267
268        // If we issued a connectAudio() call "recently enough", even
269        // if BT isn't actually connected yet, let's still pretend BT is
270        // on.  This makes the onscreen indication more responsive.
271        if (isBluetoothAudioPending()) {
272            long timeSinceRequest =
273                    SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
274            Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
275                    + timeSinceRequest + " msec ago)");
276            return true;
277        }
278
279        Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
280        return false;
281    }
282
283    private boolean isBluetoothAudioPending() {
284        return mBluetoothState == BLUETOOTH_AUDIO_PENDING;
285    }
286
287    /**
288     * Notified audio manager of a change to the bluetooth state.
289     */
290    private void updateListenerOfBluetoothState(boolean canBePending) {
291        int newState;
292        if (isBluetoothAudioConnected()) {
293            newState = BLUETOOTH_AUDIO_CONNECTED;
294        } else if (canBePending && isBluetoothAudioPending()) {
295            newState = BLUETOOTH_AUDIO_PENDING;
296        } else if (isBluetoothAvailable()) {
297            newState = BLUETOOTH_DEVICE_CONNECTED;
298        } else {
299            newState = BLUETOOTH_DISCONNECTED;
300        }
301        if (mBluetoothState != newState) {
302            mBluetoothStateListener.onBluetoothStateChange(mBluetoothState, newState);
303            mBluetoothState = newState;
304        }
305    }
306
307    @VisibleForTesting
308    public void connectBluetoothAudio() {
309        Log.v(this, "connectBluetoothAudio()...");
310        if (mBluetoothHeadset != null) {
311            if (!mBluetoothHeadset.connectAudio()) {
312                mHandler.postDelayed(mRetryConnectAudio.prepare(),
313                        Timeouts.getRetryBluetoothConnectAudioBackoffMillis(
314                                mContext.getContentResolver()));
315            }
316        }
317        // The call to connectAudio is asynchronous and may take some time to complete. However,
318        // if connectAudio() returns false, we know that it has failed and therefore will
319        // schedule a retry to happen some time later. We set bluetooth state to pending now and
320        // show bluetooth as connected in the UI, but confirmation that we are connected will
321        // arrive through mReceiver.
322        setBluetoothStatePending();
323    }
324
325    private void setBluetoothStatePending() {
326        mBluetoothState = BLUETOOTH_AUDIO_PENDING;
327        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
328        mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
329        mBluetoothConnectionTimeout.cancel();
330        // If the mBluetoothConnectionTimeout runnable has run, the session had been cleared...
331        // Create a new Session before putting it back in the queue to possibly run again.
332        mHandler.postDelayed(mBluetoothConnectionTimeout.prepare(),
333                Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
334    }
335
336    @VisibleForTesting
337    public void disconnectBluetoothAudio() {
338        Log.v(this, "disconnectBluetoothAudio()...");
339        if (mBluetoothHeadset != null) {
340            mBluetoothState = BLUETOOTH_DEVICE_CONNECTED;
341            mBluetoothHeadset.disconnectAudio();
342        } else {
343            mBluetoothState = BLUETOOTH_DISCONNECTED;
344        }
345        mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
346        mBluetoothConnectionTimeout.cancel();
347    }
348
349    /**
350     * Dumps the state of the {@link BluetoothManager}.
351     *
352     * @param pw The {@code IndentingPrintWriter} to write the state to.
353     */
354    public void dump(IndentingPrintWriter pw) {
355        pw.println("isBluetoothAvailable: " + isBluetoothAvailable());
356        pw.println("isBluetoothAudioConnected: " + isBluetoothAudioConnected());
357        pw.println("isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
358
359        if (mBluetoothAdapter != null) {
360            if (mBluetoothHeadset != null) {
361                List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
362
363                if (deviceList.size() > 0) {
364                    BluetoothDevice device = deviceList.get(0);
365                    pw.println("BluetoothHeadset.getCurrentDevice: " + device);
366                    pw.println("BluetoothHeadset.State: "
367                            + mBluetoothHeadset.getConnectionState(device));
368                    pw.println("BluetoothHeadset audio connected: " +
369                            mBluetoothHeadset.isAudioConnected(device));
370                }
371            } else {
372                pw.println("mBluetoothHeadset is null");
373            }
374        } else {
375            pw.println("mBluetoothAdapter is null; device is not BT capable");
376        }
377    }
378
379    /**
380     * Set the bluetooth headset proxy for testing purposes.
381     * @param bluetoothHeadsetProxy
382     */
383    @VisibleForTesting
384    public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) {
385        mBluetoothHeadset = bluetoothHeadsetProxy;
386    }
387
388    /**
389     * Set mBluetoothState for testing.
390     * @param state
391     */
392    @VisibleForTesting
393    public void setInternalBluetoothState(int state) {
394        mBluetoothState = state;
395    }
396}
397