• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.google.android.tv.btservices.pairing;
18 
19 import android.bluetooth.BluetoothA2dp;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothHidHost;
23 import android.bluetooth.BluetoothProfile;
24 import android.bluetooth.BluetoothProfile.ServiceListener;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapper;
36 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapperA2dp;
37 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapperHidHost;
38 
39 public final class BluetoothPairer {
40 
41     private static final String TAG = "Atv.BtPairer";
42 
43     // A typical device pairing process will proceed in order or bond, service discovery, and
44     // profile connection.
45 
46     public static final int STATUS_ERROR = -1;
47     public static final int STATUS_INIT = 0;
48     public static final int STATUS_PAIRING = 3;
49     public static final int STATUS_CONNECTING = 4;
50     public static final int STATUS_DONE = 5;
51     public static final int STATUS_CANCELLED = 6;
52     public static final int STATUS_TIMEOUT = 7;
53 
54     private static final int MSG_PAIR = 1;
55     private static final int MSG_ADD_DEVICE = 2;
56     private static final int ADD_DEVICE_RETRY_MS = 1000;
57     private static final int ADD_DEVICE_DELAY = 1000;
58 
59     private static final int MSG_TIMEOUT = 3;
60     private static final int PAIRING_TIMEOUT_MS = 25000;
61     private static final int CONNECTING_TIMEOUT_MS = 15000;
62 
63     private final Context context;
64     private Listener mListener;
65     private boolean mForgetOnFail = true;
66     private BluetoothDevice mDevice;
67     private int mBluetoothProfile;
68     private PairingProfileWrapper mPairingProfileWrapper;
69 
70     private int status = STATUS_INIT;
71 
72     protected interface Listener {
onStatusChanged(BluetoothDevice device, int status)73         void onStatusChanged(BluetoothDevice device, int status);
74     }
75 
76     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
77         @Override
78         public void handleMessage(Message msg) {
79             switch (msg.what) {
80                 case MSG_PAIR:
81                     startBonding();
82                     break;
83                 case MSG_ADD_DEVICE:
84                     addDevice();
85                     break;
86                 case MSG_TIMEOUT:
87                     timeout();
88                     break;
89                 default:
90                     Log.i(TAG, "No handler case available for message: " + msg.what);
91             }
92         }
93     };
94 
95     private final BroadcastReceiver linkStatusReceiver = new BroadcastReceiver() {
96         @Override
97         public void onReceive(Context context, Intent intent) {
98             final String action = intent.getAction();
99             if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
100                 onBondStateChanged(intent);
101             } else if (getBroadcastListeningState(mBluetoothProfile).equals(action)) {
102                 onConnectionStateChanged(intent);
103             } else if (BluetoothDevice.ACTION_UUID.equals(intent.getAction())) {
104                 onServicesDiscovered();
105             }
106         }
107     };
108 
BluetoothPairer(Context context, int bluetoothProfile)109     protected BluetoothPairer(Context context, int bluetoothProfile) {
110         this.context = context.getApplicationContext();
111         if (bluetoothProfile != BluetoothProfile.A2DP
112                 && bluetoothProfile != BluetoothProfile.HID_HOST) {
113             throw new IllegalArgumentException(bluetoothProfile + " is not a supported profile");
114         }
115         mBluetoothProfile = bluetoothProfile;
116         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
117         ServiceListener serviceConnection =
118                 new BluetoothProfile.ServiceListener() {
119                     @Override
120                     public void onServiceDisconnected(int profile) {
121                         // TODO handle unexpected disconnection
122                         Log.i(TAG, "Service disconnected, perhaps unexpectedly");
123                         mPairingProfileWrapper = null;
124                     }
125 
126                     @Override
127                     public void onServiceConnected(int profile, BluetoothProfile proxy) {
128                         Log.i(TAG, "Connection made to bluetooth proxy.");
129                         mPairingProfileWrapper = getPairingProfileWrapper(profile, proxy);
130                     }
131                 };
132         adapter.getProfileProxy(this.context, serviceConnection, bluetoothProfile);
133         registerReceiver();
134     }
135 
getPairingProfileWrapper(int bluetoothProfile, BluetoothProfile proxy)136     private static PairingProfileWrapper getPairingProfileWrapper(int bluetoothProfile,
137             BluetoothProfile proxy) {
138         if (BluetoothProfile.A2DP == bluetoothProfile) {
139             return new PairingProfileWrapperA2dp(proxy);
140         } else if (BluetoothProfile.HID_HOST == bluetoothProfile) {
141             return new PairingProfileWrapperHidHost(proxy);
142         } else {
143             return null;
144         }
145     }
146 
147     // Bond and add the device as input after bonded.
startPairing(BluetoothDevice device, Listener listener, boolean forgetOnFail)148     protected void startPairing(BluetoothDevice device, Listener listener, boolean forgetOnFail) {
149         Log.i(TAG, "startPairing: " + device.getAddress() + " " + device.getName());
150         if (isInProgress()) {
151             Log.e(TAG, "Pairing already in progress, you must cancel the "
152                     + "previous request first");
153             return;
154         }
155         mForgetOnFail = forgetOnFail;
156         mDevice = device;
157         mListener = listener;
158         if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
159             Log.i(TAG, "Already bonded " + device);
160             onBonded();
161             return;
162         }
163         mHandler.removeMessages(MSG_PAIR);
164         mHandler.sendEmptyMessage(MSG_PAIR);
165         mHandler.removeMessages(MSG_TIMEOUT);
166         mHandler.sendEmptyMessageDelayed(MSG_TIMEOUT, PAIRING_TIMEOUT_MS);
167     }
168 
dispose()169     protected void dispose() {
170         if (isInProgress()) {
171             doCancel(STATUS_CANCELLED);
172         }
173         mHandler.removeCallbacksAndMessages(null);
174         unregisterReceiver();
175         if (mPairingProfileWrapper != null) {
176             BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
177             adapter.closeProfileProxy(mBluetoothProfile, mPairingProfileWrapper.getProxy());
178         }
179     }
180 
registerReceiver()181     private void registerReceiver() {
182         IntentFilter filter = new IntentFilter();
183         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
184         filter.addAction(BluetoothDevice.ACTION_UUID);
185         filter.addAction(getBroadcastListeningState(mBluetoothProfile));
186         context.registerReceiver(linkStatusReceiver, filter);
187     }
188 
getBroadcastListeningState(int bluetoothProfile)189     private static String getBroadcastListeningState(int bluetoothProfile) {
190         if (BluetoothProfile.A2DP == bluetoothProfile) {
191             return BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED;
192         } else if (BluetoothProfile.HID_HOST == bluetoothProfile) {
193             return BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED;
194         } else {
195             return null;
196         }
197     }
198 
unregisterReceiver()199     private void unregisterReceiver() {
200         context.unregisterReceiver(linkStatusReceiver);
201     }
202 
isInProgress()203     private boolean isInProgress() {
204         return status != STATUS_INIT && status != STATUS_ERROR && status != STATUS_CANCELLED
205                 && status != STATUS_DONE && status != STATUS_TIMEOUT;
206     }
207 
onBondStateChanged(Intent intent)208     private void onBondStateChanged(Intent intent) {
209         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
210         if (!device.equals(mDevice)) {
211             return;
212         }
213 
214         int bondState =
215                 intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
216         int previousBondState = intent.getIntExtra(
217                 BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE);
218         if (bondState == BluetoothDevice.BOND_NONE &&
219                 previousBondState == BluetoothDevice.BOND_BONDING) {
220             onBondFailed();
221         } else if (bondState == BluetoothDevice.BOND_BONDED) {
222             onBonded();
223         }
224     }
225 
onConnectionStateChanged(Intent intent)226     private void onConnectionStateChanged(Intent intent) {
227         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
228         int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
229         if (mDevice == null || !mDevice.equals(device)) {
230             Log.e(TAG, "addAsInput (handler): non-device connecting: " + device);
231             return;
232         }
233         switch (state) {
234             case BluetoothProfile.STATE_CONNECTED:
235                 mHandler.post(BluetoothPairer.this::onAdded);
236                 break;
237             case BluetoothProfile.STATE_DISCONNECTED:
238                 mHandler.post(BluetoothPairer.this::onAddFailed);
239                 break;
240             case BluetoothProfile.STATE_CONNECTING:
241             case BluetoothProfile.STATE_DISCONNECTING:
242                 Log.i(TAG, "addAsInput (handler): no action for transient states");
243                 break;
244             default:
245                 Log.e(TAG, "addAsInput (handler): unknown state " + state);
246         }
247     }
248 
onServicesDiscovered()249     private void onServicesDiscovered() {
250         // regardless of the UUID content, at this point, we're sure we can initiate a
251         // profile connection.
252         if (!mHandler.hasMessages(MSG_ADD_DEVICE)) {
253             mHandler.sendEmptyMessageDelayed(MSG_ADD_DEVICE, ADD_DEVICE_DELAY);
254         }
255     }
256 
startBonding()257     private void startBonding() {
258         updateStatus(STATUS_PAIRING);
259         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
260             if (!mDevice.createBond()) {
261                 onBondFailed();
262             }
263         } else {
264             onBonded();
265         }
266     }
267 
268     // Open a connection to the profile host.
onBonded()269     private void onBonded() {
270         mHandler.removeMessages(MSG_TIMEOUT);
271         mHandler.sendEmptyMessageDelayed(MSG_TIMEOUT, CONNECTING_TIMEOUT_MS);
272         serviceDiscovery();
273     }
274 
onBondFailed()275     private void onBondFailed() {
276         Log.e(TAG, "There was an error bonding with the device.");
277         updateStatus(STATUS_ERROR);
278         unpairDevice(mDevice);
279         mDevice = null;
280     }
281 
282     // For a2dp devices, we must complete discovery before initiating a profile connection.
283     // See b/135210487.
serviceDiscovery()284     private void serviceDiscovery() {
285         if (mDevice == null) {
286             Log.e(TAG, "serviceDiscovery(): mDevice is null");
287             return;
288         }
289         mDevice.fetchUuidsWithSdp();
290     }
291 
292     // Adding as input = CONNECTING
addDevice()293     private void addDevice() {
294         if (mDevice == null) {
295             Log.i(TAG, "No device to add");
296             mHandler.post(this::onAddFailed);
297             return;
298         }
299         if (mPairingProfileWrapper == null) {
300             Log.i(TAG, "No Bluetooth proxy");
301             mHandler.removeMessages(MSG_ADD_DEVICE);
302             mHandler.sendEmptyMessageDelayed(MSG_ADD_DEVICE, ADD_DEVICE_RETRY_MS);
303             return;
304         }
305         updateStatus(STATUS_CONNECTING);
306         if (isDeviceAdded()) {
307             mHandler.post(this::onAdded);
308             return;
309         }
310         mPairingProfileWrapper.connect(mDevice);
311     }
312 
onAdded()313     private void onAdded() {
314         updateStatus(STATUS_DONE);
315         mHandler.removeMessages(MSG_TIMEOUT);
316         mDevice = null;
317     }
318 
onAddFailed()319     private void onAddFailed() {
320         Log.e(TAG, "There was an error adding the device as input.");
321         updateStatus(STATUS_ERROR);
322         mHandler.removeMessages(MSG_TIMEOUT);
323         unpairDevice(mDevice);
324         mDevice = null;
325     }
326 
updateStatus(int status)327     private void updateStatus(int status) {
328         this.status = status;
329         if (mListener != null) {
330             mListener.onStatusChanged(mDevice, status);
331         }
332     }
333 
isDeviceAdded()334     private boolean isDeviceAdded() {
335         if (mPairingProfileWrapper == null) {
336             return false;
337         }
338         if (mDevice == null) {
339             return false;
340         }
341         for (BluetoothDevice device : mPairingProfileWrapper.getConnectedDevices()) {
342             Log.i(TAG, "Device connected: " + device);
343             if (TextUtils.equals(device.getAddress(), mDevice.getAddress())) {
344                 return true;
345             }
346         }
347         return false;
348     }
349 
doCancel(int status)350     private void doCancel(int status) {
351         if (isInProgress()) {
352             Log.i(TAG, "Pairing process has already begun, forcing cancel anyway.");
353             updateStatus(status);
354         }
355         mHandler.removeMessages(MSG_TIMEOUT);
356         mHandler.removeMessages(MSG_PAIR);
357         if (mDevice != null && mPairingProfileWrapper != null) {
358             mPairingProfileWrapper.disconnect(mDevice);
359         }
360         unpairDevice(mDevice);
361         mDevice = null;
362         updateStatus(STATUS_ERROR);
363     }
364 
unpairDevice(BluetoothDevice device)365     private void unpairDevice(BluetoothDevice device) {
366         if (device == null) {
367             return;
368         }
369         if (BluetoothDevice.BOND_BONDING == device.getBondState()) {
370             device.cancelBondProcess();
371         }
372         if (mForgetOnFail) {
373             final boolean successful = device.removeBond();
374             if (successful) {
375                 Log.i(TAG, "Bluetooth device successfully unpaired: " + device.getName());
376             } else {
377                 Log.i(TAG, "Failed to unpair device: " + device.getName());
378             }
379         }
380     }
381 
timeout()382     private void timeout() {
383         Log.e(TAG, "Bluetooth pairing timed out, cancelling...");
384         doCancel(STATUS_TIMEOUT);
385     }
386 }
387