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