blob: 0015ebc162e212f15ad24ce9b4ddb509380f372a [file] [log] [blame]
package com.android.car.messenger.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.SdpMasRecord;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources.NotFoundException;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.car.messenger.R;
import com.android.car.messenger.log.L;
import java.util.HashSet;
import java.util.Set;
/**
* Provides a callback interface for subscribers to be notified of bluetooth MAP/SDP changes.
*/
public class BluetoothMonitor {
private static final String TAG = "CM.BluetoothMonitor";
private final Context mContext;
private final BluetoothMapReceiver mBluetoothMapReceiver;
private final BluetoothSdpReceiver mBluetoothSdpReceiver;
private final MapDeviceMonitor mMapDeviceMonitor;
private final BluetoothProfile.ServiceListener mMapServiceListener;
private final Set<OnBluetoothEventListener> mListeners;
public BluetoothMonitor(@NonNull Context context) {
mContext = context;
mBluetoothMapReceiver = new BluetoothMapReceiver();
mBluetoothSdpReceiver = new BluetoothSdpReceiver();
mMapDeviceMonitor = new MapDeviceMonitor();
mMapServiceListener = new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
L.d(TAG, "Connected to MAP service!");
onMapConnected((BluetoothMapClient) proxy);
}
@Override
public void onServiceDisconnected(int profile) {
L.d(TAG, "Disconnected from MAP service!");
onMapDisconnected(profile);
}
};
mListeners = new HashSet<>();
connectToMap();
}
/**
* Registers a listener to receive Bluetooth MAP events.
* If this listener is already registered, calling this method has no effect.
*
* @param listener the listener to register
* @return true if this listener was not already registered
*/
public boolean registerListener(@NonNull OnBluetoothEventListener listener) {
return mListeners.add(listener);
}
/**
* Unregisters a listener from receiving Bluetooth MAP events.
* If this listener is not registered, calling this method has no effect.
*
* @param listener the listener to unregister
* @return true if the set of registered listeners contained this listener
*/
public boolean unregisterListener(OnBluetoothEventListener listener) {
return mListeners.remove(listener);
}
public interface OnBluetoothEventListener {
/**
* Callback issued when a new message was received.
*
* @param intent intent containing the message details
*/
void onMessageReceived(Intent intent);
/**
* Callback issued when a new message was sent successfully.
*
* @param intent intent containing the message details
*/
void onMessageSent(Intent intent);
/**
* Callback issued when a new device has connected to bluetooth.
*
* @param device the connected device
*/
void onDeviceConnected(BluetoothDevice device);
/**
* Callback issued when a previously connected device has disconnected from bluetooth.
*
* @param device the disconnected device
*/
void onDeviceDisconnected(BluetoothDevice device);
/**
* Callback issued when a new MAP client has been connected.
*
* @param client the MAP client
*/
void onMapConnected(BluetoothMapClient client);
/**
* Callback issued when a MAP client has been disconnected.
*
* @param profile see {@link BluetoothProfile.ServiceListener#onServiceDisconnected(int)}
*/
void onMapDisconnected(int profile);
/**
* Callback issued when a new SDP record has been detected.
*
* @param device the device detected
* @param supportsReply true if the device supports SMS replies through bluetooth
*/
void onSdpRecord(BluetoothDevice device, boolean supportsReply);
}
private void onMessageReceived(Intent intent) {
mListeners.forEach(listener -> listener.onMessageReceived(intent));
}
private void onMessageSent(Intent intent) {
mListeners.forEach(listener -> listener.onMessageSent(intent));
}
private void onDeviceConnected(BluetoothDevice device) {
mListeners.forEach(listener -> listener.onDeviceConnected(device));
}
private void onDeviceDisconnected(BluetoothDevice device) {
mListeners.forEach(listener -> listener.onDeviceDisconnected(device));
}
private void onMapConnected(BluetoothMapClient client) {
mListeners.forEach(listener -> listener.onMapConnected(client));
}
private void onMapDisconnected(int profile) {
mListeners.forEach(listener -> listener.onMapDisconnected(profile));
boolean shouldReconnectToMap = false;
try {
shouldReconnectToMap = mContext.getResources().getBoolean(
R.bool.config_loadExistingMessages);
} catch (NotFoundException e) {
// Should only happen for robolectric unit tests
L.e(TAG, e, "Could not find loadExistingMessages config");
}
if (shouldReconnectToMap) {
connectToMap();
}
}
private void onSdpRecord(BluetoothDevice device, boolean supportsReply) {
mListeners.forEach(listener -> listener.onSdpRecord(device, supportsReply));
}
/** Connects to the MAP client. */
private void connectToMap() {
L.d(TAG, "Connecting to MAP service");
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
// This can happen on devices that don't support Bluetooth.
L.e(TAG, "BluetoothAdapter is null! Unable to connect to MAP client.");
return;
}
if (!adapter.getProfileProxy(mContext, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
// This *should* never happen. Unless arguments passed are incorrect somehow...
L.wtf(TAG, "Unable to get MAP profile!");
return;
}
}
/**
* Performs {@link Context} related cleanup (such as unregistering from receivers).
*/
public void cleanup() {
mListeners.clear();
mBluetoothMapReceiver.unregisterReceivers();
mBluetoothSdpReceiver.unregisterReceivers();
mMapDeviceMonitor.unregisterReceivers();
}
@VisibleForTesting
BluetoothProfile.ServiceListener getServiceListener() {
return mMapServiceListener;
}
/** Monitors for new device connections and disconnections */
private class MapDeviceMonitor extends BroadcastReceiver {
MapDeviceMonitor() {
L.d(TAG, "Registering Map device monitor");
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
mContext.registerReceiver(this, intentFilter,
android.Manifest.permission.BLUETOOTH, null);
}
void unregisterReceivers() {
mContext.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
final int STATE_NOT_FOUND = -1;
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, STATE_NOT_FOUND);
int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
STATE_NOT_FOUND);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (state == STATE_NOT_FOUND || previousState == STATE_NOT_FOUND || device == null) {
L.w(TAG, "Skipping broadcast, missing required extra");
return;
}
if (previousState == BluetoothProfile.STATE_CONNECTED
&& state != BluetoothProfile.STATE_CONNECTED) {
L.d(TAG, "Device losing MAP connection: %s", device);
onDeviceDisconnected(device);
}
if (previousState == BluetoothProfile.STATE_CONNECTING
&& state == BluetoothProfile.STATE_CONNECTED) {
L.d(TAG, "Device connected: %s", device);
onDeviceConnected(device);
}
}
}
/** Monitors for new incoming messages and sent-message broadcast. */
private class BluetoothMapReceiver extends BroadcastReceiver {
BluetoothMapReceiver() {
L.d(TAG, "Registering receiver for bluetooth MAP");
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
mContext.registerReceiver(this, intentFilter);
}
void unregisterReceivers() {
mContext.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) return;
switch (intent.getAction()) {
case BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY:
L.d(TAG, "SMS sent successfully.");
onMessageSent(intent);
break;
case BluetoothMapClient.ACTION_MESSAGE_RECEIVED:
L.d(TAG, "SMS message received.");
onMessageReceived(intent);
break;
default:
L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
break;
}
}
}
/** Monitors for new SDP records */
private class BluetoothSdpReceiver extends BroadcastReceiver {
// reply or "upload" feature is indicated by the 3rd bit
private static final int REPLY_FEATURE_FLAG_POSITION = 3;
private static final int REPLY_FEATURE_MIN_VERSION = 0x102;
BluetoothSdpReceiver() {
L.d(TAG, "Registering receiver for sdp");
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
mContext.registerReceiver(this, intentFilter);
}
void unregisterReceivers() {
mContext.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) {
L.d(TAG, "get SDP record: %s", intent.getExtras());
Parcelable parcelable = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
if (!(parcelable instanceof SdpMasRecord)) {
L.d(TAG, "not SdpMasRecord: %s", parcelable);
return;
}
SdpMasRecord masRecord = (SdpMasRecord) parcelable;
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
onSdpRecord(device, supportsReply(masRecord));
} else {
L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
}
}
private boolean isOn(int input, int position) {
return ((input >> position) & 1) == 1;
}
private boolean supportsReply(@NonNull SdpMasRecord masRecord) {
final int version = masRecord.getProfileVersion();
final int features = masRecord.getSupportedFeatures();
// We only consider the device as supporting the reply feature if the version
// is 1.02 at minimum and the feature flag is turned on.
return version >= REPLY_FEATURE_MIN_VERSION
&& isOn(features, REPLY_FEATURE_FLAG_POSITION);
}
}
}