summaryrefslogtreecommitdiff
path: root/src/com/android/car/messenger/bluetooth/BluetoothMonitor.java
blob: 095474f7a197885812ba983b30e9fdd7f762bb52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
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.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
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 BluetoothMapClient mBluetoothMapClient;

    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();
            }
        };
        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.
         */
        void onMapDisconnected();

        /**
         * 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) {
        mBluetoothMapClient = client;
        mListeners.forEach(listener -> listener.onMapConnected(client));
    }

    private void onMapDisconnected() {
        mBluetoothMapClient = null;
        mListeners.forEach(listener -> listener.onMapDisconnected());
    }

    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 onDestroy() {
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter != null) {
            adapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, mBluetoothMapClient);
        }
        onMapDisconnected();
        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);
        }
    }
}