1 package com.android.car.messenger.bluetooth;
2 
3 import android.bluetooth.BluetoothAdapter;
4 import android.bluetooth.BluetoothDevice;
5 import android.bluetooth.BluetoothMapClient;
6 import android.bluetooth.BluetoothProfile;
7 import android.bluetooth.SdpMasRecord;
8 import android.content.BroadcastReceiver;
9 import android.content.Context;
10 import android.content.Intent;
11 import android.content.IntentFilter;
12 import android.os.Parcelable;
13 import androidx.annotation.NonNull;
14 import androidx.annotation.VisibleForTesting;
15 import com.android.car.messenger.log.L;
16 import java.util.HashSet;
17 import java.util.Set;
18 
19 
20 /**
21  * Provides a callback interface for subscribers to be notified of bluetooth MAP/SDP changes.
22  */
23 public class BluetoothMonitor {
24     private static final String TAG = "CM.BluetoothMonitor";
25 
26     private final Context mContext;
27     private final BluetoothMapReceiver mBluetoothMapReceiver;
28     private final BluetoothSdpReceiver mBluetoothSdpReceiver;
29     private final MapDeviceMonitor mMapDeviceMonitor;
30     private final BluetoothProfile.ServiceListener mMapServiceListener;
31     private BluetoothMapClient mBluetoothMapClient;
32 
33     private final Set<OnBluetoothEventListener> mListeners;
34 
BluetoothMonitor(@onNull Context context)35     public BluetoothMonitor(@NonNull Context context) {
36         mContext = context;
37         mBluetoothMapReceiver = new BluetoothMapReceiver();
38         mBluetoothSdpReceiver = new BluetoothSdpReceiver();
39         mMapDeviceMonitor = new MapDeviceMonitor();
40         mMapServiceListener = new BluetoothProfile.ServiceListener() {
41             @Override
42             public void onServiceConnected(int profile, BluetoothProfile proxy) {
43                 L.d(TAG, "Connected to MAP service!");
44                 onMapConnected((BluetoothMapClient) proxy);
45             }
46 
47             @Override
48             public void onServiceDisconnected(int profile) {
49                 L.d(TAG, "Disconnected from MAP service!");
50                 onMapDisconnected();
51             }
52         };
53         mListeners = new HashSet<>();
54         connectToMap();
55     }
56 
57     /**
58      * Registers a listener to receive Bluetooth MAP events.
59      * If this listener is already registered, calling this method has no effect.
60      *
61      * @param listener the listener to register
62      * @return true if this listener was not already registered
63      */
registerListener(@onNull OnBluetoothEventListener listener)64     public boolean registerListener(@NonNull OnBluetoothEventListener listener) {
65         return mListeners.add(listener);
66     }
67 
68     /**
69      * Unregisters a listener from receiving Bluetooth MAP events.
70      * If this listener is not registered, calling this method has no effect.
71      *
72      * @param listener the listener to unregister
73      * @return true if the set of registered listeners contained this listener
74      */
unregisterListener(OnBluetoothEventListener listener)75     public boolean unregisterListener(OnBluetoothEventListener listener) {
76         return mListeners.remove(listener);
77     }
78 
79     public interface OnBluetoothEventListener {
80         /**
81          * Callback issued when a new message was received.
82          *
83          * @param intent intent containing the message details
84          */
onMessageReceived(Intent intent)85         void onMessageReceived(Intent intent);
86 
87         /**
88          * Callback issued when a new message was sent successfully.
89          *
90          * @param intent intent containing the message details
91          */
onMessageSent(Intent intent)92         void onMessageSent(Intent intent);
93 
94         /**
95          * Callback issued when a new device has connected to bluetooth.
96          *
97          * @param device the connected device
98          */
onDeviceConnected(BluetoothDevice device)99         void onDeviceConnected(BluetoothDevice device);
100 
101         /**
102          * Callback issued when a previously connected device has disconnected from bluetooth.
103          *
104          * @param device the disconnected device
105          */
onDeviceDisconnected(BluetoothDevice device)106         void onDeviceDisconnected(BluetoothDevice device);
107 
108         /**
109          * Callback issued when a new MAP client has been connected.
110          *
111          * @param client the MAP client
112          */
onMapConnected(BluetoothMapClient client)113         void onMapConnected(BluetoothMapClient client);
114 
115         /**
116          * Callback issued when a MAP client has been disconnected.
117          */
onMapDisconnected()118         void onMapDisconnected();
119 
120         /**
121          * Callback issued when a new SDP record has been detected.
122          *
123          * @param device        the device detected
124          * @param supportsReply true if the device supports SMS replies through bluetooth
125          */
onSdpRecord(BluetoothDevice device, boolean supportsReply)126         void onSdpRecord(BluetoothDevice device, boolean supportsReply);
127     }
128 
onMessageReceived(Intent intent)129     private void onMessageReceived(Intent intent) {
130         mListeners.forEach(listener -> listener.onMessageReceived(intent));
131     }
132 
onMessageSent(Intent intent)133     private void onMessageSent(Intent intent) {
134         mListeners.forEach(listener -> listener.onMessageSent(intent));
135     }
136 
onDeviceConnected(BluetoothDevice device)137     private void onDeviceConnected(BluetoothDevice device) {
138         mListeners.forEach(listener -> listener.onDeviceConnected(device));
139     }
140 
onDeviceDisconnected(BluetoothDevice device)141     private void onDeviceDisconnected(BluetoothDevice device) {
142         mListeners.forEach(listener -> listener.onDeviceDisconnected(device));
143     }
144 
onMapConnected(BluetoothMapClient client)145     private void onMapConnected(BluetoothMapClient client) {
146         mBluetoothMapClient = client;
147         mListeners.forEach(listener -> listener.onMapConnected(client));
148     }
149 
onMapDisconnected()150     private void onMapDisconnected() {
151         mBluetoothMapClient = null;
152         mListeners.forEach(listener -> listener.onMapDisconnected());
153     }
154 
onSdpRecord(BluetoothDevice device, boolean supportsReply)155     private void onSdpRecord(BluetoothDevice device, boolean supportsReply) {
156         mListeners.forEach(listener -> listener.onSdpRecord(device, supportsReply));
157     }
158 
159     /** Connects to the MAP client. */
connectToMap()160     private void connectToMap() {
161         L.d(TAG, "Connecting to MAP service");
162 
163         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
164         if (adapter == null) {
165             // This can happen on devices that don't support Bluetooth.
166             L.e(TAG, "BluetoothAdapter is null! Unable to connect to MAP client.");
167             return;
168         }
169 
170         if (!adapter.getProfileProxy(mContext, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
171             // This *should* never happen.  Unless arguments passed are incorrect somehow...
172             L.wtf(TAG, "Unable to get MAP profile!");
173             return;
174         }
175     }
176 
177     /**
178      * Performs {@link Context} related cleanup (such as unregistering from receivers).
179      */
onDestroy()180     public void onDestroy() {
181         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
182         if (adapter != null) {
183             adapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, mBluetoothMapClient);
184         }
185         onMapDisconnected();
186         mListeners.clear();
187         mBluetoothMapReceiver.unregisterReceivers();
188         mBluetoothSdpReceiver.unregisterReceivers();
189         mMapDeviceMonitor.unregisterReceivers();
190     }
191 
192     @VisibleForTesting
getServiceListener()193     BluetoothProfile.ServiceListener getServiceListener() {
194         return mMapServiceListener;
195     }
196 
197     /** Monitors for new device connections and disconnections */
198     private class MapDeviceMonitor extends BroadcastReceiver {
MapDeviceMonitor()199         MapDeviceMonitor() {
200             L.d(TAG, "Registering Map device monitor");
201 
202             IntentFilter intentFilter = new IntentFilter();
203             intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
204             mContext.registerReceiver(this, intentFilter,
205                     android.Manifest.permission.BLUETOOTH, null);
206         }
207 
unregisterReceivers()208         void unregisterReceivers() {
209             mContext.unregisterReceiver(this);
210         }
211 
212         @Override
onReceive(Context context, Intent intent)213         public void onReceive(Context context, Intent intent) {
214             final int STATE_NOT_FOUND = -1;
215             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, STATE_NOT_FOUND);
216             int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
217                     STATE_NOT_FOUND);
218 
219             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
220 
221             if (state == STATE_NOT_FOUND || previousState == STATE_NOT_FOUND || device == null) {
222                 L.w(TAG, "Skipping broadcast, missing required extra");
223                 return;
224             }
225 
226             if (previousState == BluetoothProfile.STATE_CONNECTED
227                     && state != BluetoothProfile.STATE_CONNECTED) {
228                 L.d(TAG, "Device losing MAP connection: %s", device);
229 
230                 onDeviceDisconnected(device);
231             }
232 
233             if (previousState == BluetoothProfile.STATE_CONNECTING
234                     && state == BluetoothProfile.STATE_CONNECTED) {
235                 L.d(TAG, "Device connected: %s", device);
236 
237                 onDeviceConnected(device);
238             }
239         }
240     }
241 
242     /** Monitors for new incoming messages and sent-message broadcast. */
243     private class BluetoothMapReceiver extends BroadcastReceiver {
BluetoothMapReceiver()244         BluetoothMapReceiver() {
245             L.d(TAG, "Registering receiver for bluetooth MAP");
246 
247             IntentFilter intentFilter = new IntentFilter();
248             intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
249             intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
250             mContext.registerReceiver(this, intentFilter);
251         }
252 
unregisterReceivers()253         void unregisterReceivers() {
254             mContext.unregisterReceiver(this);
255         }
256 
257         @Override
onReceive(Context context, Intent intent)258         public void onReceive(Context context, Intent intent) {
259 
260             if (intent == null || intent.getAction() == null) return;
261 
262             switch (intent.getAction()) {
263                 case BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY:
264                     L.d(TAG, "SMS sent successfully.");
265                     onMessageSent(intent);
266                     break;
267                 case BluetoothMapClient.ACTION_MESSAGE_RECEIVED:
268                     L.d(TAG, "SMS message received.");
269                     onMessageReceived(intent);
270                     break;
271                 default:
272                     L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
273                     break;
274             }
275         }
276     }
277 
278     /** Monitors for new SDP records */
279     private class BluetoothSdpReceiver extends BroadcastReceiver {
280 
281         // reply or "upload" feature is indicated by the 3rd bit
282         private static final int REPLY_FEATURE_FLAG_POSITION = 3;
283         private static final int REPLY_FEATURE_MIN_VERSION = 0x102;
284 
BluetoothSdpReceiver()285         BluetoothSdpReceiver() {
286             L.d(TAG, "Registering receiver for sdp");
287 
288             IntentFilter intentFilter = new IntentFilter();
289             intentFilter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
290             mContext.registerReceiver(this, intentFilter);
291         }
292 
unregisterReceivers()293         void unregisterReceivers() {
294             mContext.unregisterReceiver(this);
295         }
296 
297         @Override
onReceive(Context context, Intent intent)298         public void onReceive(Context context, Intent intent) {
299             if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) {
300                 L.d(TAG, "get SDP record: %s", intent.getExtras());
301 
302                 Parcelable parcelable = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
303                 if (!(parcelable instanceof SdpMasRecord)) {
304                     L.d(TAG, "not SdpMasRecord: %s", parcelable);
305                     return;
306                 }
307 
308                 SdpMasRecord masRecord = (SdpMasRecord) parcelable;
309                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
310                 onSdpRecord(device, supportsReply(masRecord));
311             } else {
312                 L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
313             }
314         }
315 
isOn(int input, int position)316         private boolean isOn(int input, int position) {
317             return ((input >> position) & 1) == 1;
318         }
319 
supportsReply(@onNull SdpMasRecord masRecord)320         private boolean supportsReply(@NonNull SdpMasRecord masRecord) {
321             final int version = masRecord.getProfileVersion();
322             final int features = masRecord.getSupportedFeatures();
323             // We only consider the device as supporting the reply feature if the version
324             // is 1.02 at minimum and the feature flag is turned on.
325             return version >= REPLY_FEATURE_MIN_VERSION
326                     && isOn(features, REPLY_FEATURE_FLAG_POSITION);
327         }
328     }
329 }
330