1/*
2 * Copyright (C) 2012 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
17package com.android.nfc.handover;
18
19import java.nio.BufferUnderflowException;
20import java.nio.ByteBuffer;
21import java.nio.charset.StandardCharsets;
22import java.util.ArrayList;
23import java.nio.charset.Charset;
24import java.util.Arrays;
25import java.util.Random;
26
27import android.bluetooth.BluetoothAdapter;
28import android.bluetooth.BluetoothDevice;
29import android.content.Context;
30import android.content.Intent;
31import android.nfc.FormatException;
32import android.nfc.NdefMessage;
33import android.nfc.NdefRecord;
34import android.os.UserHandle;
35import android.util.Log;
36
37/**
38 * Manages handover of NFC to other technologies.
39 */
40public class HandoverDataParser {
41    private static final String TAG = "NfcHandover";
42    private static final boolean DBG = false;
43
44    private static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob"
45            .getBytes(StandardCharsets.US_ASCII);
46    private static final byte[] TYPE_BLE_OOB = "application/vnd.bluetooth.le.oob"
47            .getBytes(StandardCharsets.US_ASCII);
48
49    private static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(StandardCharsets.US_ASCII);
50
51    private static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr";
52
53    private static final int CARRIER_POWER_STATE_INACTIVE = 0;
54    private static final int CARRIER_POWER_STATE_ACTIVE = 1;
55    private static final int CARRIER_POWER_STATE_ACTIVATING = 2;
56    private static final int CARRIER_POWER_STATE_UNKNOWN = 3;
57
58    private static final int BT_HANDOVER_TYPE_MAC = 0x1B;
59    private static final int BT_HANDOVER_TYPE_LE_ROLE = 0x1C;
60    private static final int BT_HANDOVER_TYPE_LONG_LOCAL_NAME = 0x09;
61    private static final int BT_HANDOVER_TYPE_SHORT_LOCAL_NAME = 0x08;
62    public static final int BT_HANDOVER_LE_ROLE_CENTRAL_ONLY = 0x01;
63
64    private final BluetoothAdapter mBluetoothAdapter;
65
66    private final Object mLock = new Object();
67    // Variables below synchronized on mLock
68
69    private String mLocalBluetoothAddress;
70
71    public static class BluetoothHandoverData {
72        public boolean valid = false;
73        public BluetoothDevice device;
74        public String name;
75        public boolean carrierActivating = false;
76        public int transport = BluetoothDevice.TRANSPORT_AUTO;
77    }
78
79    public static class IncomingHandoverData {
80        public final NdefMessage handoverSelect;
81        public final BluetoothHandoverData handoverData;
82
83        public IncomingHandoverData(NdefMessage handoverSelect,
84                                    BluetoothHandoverData handoverData) {
85            this.handoverSelect = handoverSelect;
86            this.handoverData = handoverData;
87        }
88    }
89
90    public HandoverDataParser() {
91        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
92    }
93
94    static NdefRecord createCollisionRecord() {
95        byte[] random = new byte[2];
96        new Random().nextBytes(random);
97        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, RTD_COLLISION_RESOLUTION, null, random);
98    }
99
100    NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) {
101        byte[] payload = new byte[4];
102        payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING :
103            CARRIER_POWER_STATE_ACTIVE);  // Carrier Power State: Activating or active
104        payload[1] = 1;   // length of carrier data reference
105        payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record
106        payload[3] = 0;  // Auxiliary data reference count
107        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null,
108                payload);
109    }
110
111    NdefRecord createBluetoothOobDataRecord() {
112        byte[] payload = new byte[8];
113        // Note: this field should be little-endian per the BTSSP spec
114        // The Android 4.1 implementation used big-endian order here.
115        // No single Android implementation has ever interpreted this
116        // length field when parsing this record though.
117        payload[0] = (byte) (payload.length & 0xFF);
118        payload[1] = (byte) ((payload.length >> 8) & 0xFF);
119
120        synchronized (mLock) {
121            if (mLocalBluetoothAddress == null) {
122                mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
123            }
124
125            byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
126            System.arraycopy(addressBytes, 0, payload, 2, 6);
127        }
128
129        return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
130    }
131
132    public boolean isHandoverSupported() {
133        return (mBluetoothAdapter != null);
134    }
135
136    public NdefMessage createHandoverRequestMessage() {
137        if (mBluetoothAdapter == null) {
138            return null;
139        }
140
141        NdefRecord[] dataRecords = new NdefRecord[] {
142                createBluetoothOobDataRecord()
143        };
144        return new NdefMessage(
145                createHandoverRequestRecord(),
146                dataRecords);
147    }
148
149    NdefMessage createBluetoothHandoverSelectMessage(boolean activating) {
150        return new NdefMessage(createHandoverSelectRecord(
151                createBluetoothAlternateCarrierRecord(activating)),
152                createBluetoothOobDataRecord());
153    }
154
155    NdefRecord createHandoverSelectRecord(NdefRecord alternateCarrier) {
156        NdefMessage nestedMessage = new NdefMessage(alternateCarrier);
157        byte[] nestedPayload = nestedMessage.toByteArray();
158
159        ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
160        payload.put((byte)0x12);  // connection handover v1.2
161        payload.put(nestedPayload);
162
163        byte[] payloadBytes = new byte[payload.position()];
164        payload.position(0);
165        payload.get(payloadBytes);
166        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
167                payloadBytes);
168    }
169
170    NdefRecord createHandoverRequestRecord() {
171        NdefRecord[] messages = new NdefRecord[] {
172                createBluetoothAlternateCarrierRecord(false)
173        };
174
175        NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), messages);
176
177        byte[] nestedPayload = nestedMessage.toByteArray();
178
179        ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
180        payload.put((byte) 0x12);  // connection handover v1.2
181        payload.put(nestedMessage.toByteArray());
182
183        byte[] payloadBytes = new byte[payload.position()];
184        payload.position(0);
185        payload.get(payloadBytes);
186        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
187                payloadBytes);
188    }
189
190    /**
191     * Returns null if message is not a Handover Request,
192     * returns the IncomingHandoverData (Hs + parsed data) if it is.
193     */
194    public IncomingHandoverData getIncomingHandoverData(NdefMessage handoverRequest) {
195        if (handoverRequest == null) return null;
196        if (mBluetoothAdapter == null) return null;
197
198        if (DBG) Log.d(TAG, "getIncomingHandoverData():" + handoverRequest.toString());
199
200        NdefRecord handoverRequestRecord = handoverRequest.getRecords()[0];
201        if (handoverRequestRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
202            return null;
203        }
204
205        if (!Arrays.equals(handoverRequestRecord.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) {
206            return null;
207        }
208
209        // we have a handover request, look for BT OOB record
210        BluetoothHandoverData bluetoothData = null;
211        for (NdefRecord dataRecord : handoverRequest.getRecords()) {
212            if (dataRecord.getTnf() == NdefRecord.TNF_MIME_MEDIA) {
213                if (Arrays.equals(dataRecord.getType(), TYPE_BT_OOB)) {
214                    bluetoothData = parseBtOob(ByteBuffer.wrap(dataRecord.getPayload()));
215                }
216            }
217        }
218
219        NdefMessage hs = tryBluetoothHandoverRequest(bluetoothData);
220        if (hs != null) {
221            return new IncomingHandoverData(hs, bluetoothData);
222        }
223
224        return null;
225    }
226
227    public BluetoothHandoverData getOutgoingHandoverData(NdefMessage handoverSelect) {
228        return parseBluetooth(handoverSelect);
229    }
230
231    private NdefMessage tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData) {
232        NdefMessage selectMessage = null;
233        if (bluetoothData != null) {
234            // Note: there could be a race where we conclude
235            // that Bluetooth is already enabled, and shortly
236            // after the user turns it off. That will cause
237            // the transfer to fail, but there's nothing
238            // much we can do about it anyway. It shouldn't
239            // be common for the user to be changing BT settings
240            // while waiting to receive a picture.
241            boolean bluetoothActivating = !mBluetoothAdapter.isEnabled();
242
243            // return BT OOB record so they can perform handover
244            selectMessage = (createBluetoothHandoverSelectMessage(bluetoothActivating));
245            if (DBG) Log.d(TAG, "Waiting for incoming transfer, [" +
246                    bluetoothData.device.getAddress() + "]->[" + mLocalBluetoothAddress + "]");
247        }
248
249        return selectMessage;
250    }
251
252
253
254    boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
255        byte[] payload = handoverRec.getPayload();
256        if (payload == null || payload.length <= 1) return false;
257        // Skip version
258        byte[] payloadNdef = new byte[payload.length - 1];
259        System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1);
260        NdefMessage msg;
261        try {
262            msg = new NdefMessage(payloadNdef);
263        } catch (FormatException e) {
264            return false;
265        }
266
267        for (NdefRecord alt : msg.getRecords()) {
268            byte[] acPayload = alt.getPayload();
269            if (acPayload != null) {
270                ByteBuffer buf = ByteBuffer.wrap(acPayload);
271                int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits
272                int carrierRefLength = buf.get() & 0xFF;
273                if (carrierRefLength != carrierId.length) return false;
274
275                byte[] carrierRefId = new byte[carrierRefLength];
276                buf.get(carrierRefId);
277                if (Arrays.equals(carrierRefId, carrierId)) {
278                    // Found match, returning whether power state is activating
279                    return (cps == CARRIER_POWER_STATE_ACTIVATING);
280                }
281            }
282        }
283
284        return true;
285    }
286
287    BluetoothHandoverData parseBluetoothHandoverSelect(NdefMessage m) {
288        // TODO we could parse this a lot more strictly; right now
289        // we just search for a BT OOB record, and try to cross-reference
290        // the carrier state inside the 'hs' payload.
291        for (NdefRecord oob : m.getRecords()) {
292            if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
293                    Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
294                BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
295                if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) {
296                    data.carrierActivating = true;
297                }
298                return data;
299            }
300
301            if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
302                    Arrays.equals(oob.getType(), TYPE_BLE_OOB)) {
303                return parseBleOob(ByteBuffer.wrap(oob.getPayload()));
304            }
305        }
306
307        return null;
308    }
309
310    public BluetoothHandoverData parseBluetooth(NdefMessage m) {
311        NdefRecord r = m.getRecords()[0];
312        short tnf = r.getTnf();
313        byte[] type = r.getType();
314
315        // Check for BT OOB record
316        if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) {
317            return parseBtOob(ByteBuffer.wrap(r.getPayload()));
318        }
319
320        // Check for BLE OOB record
321        if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BLE_OOB)) {
322            return parseBleOob(ByteBuffer.wrap(r.getPayload()));
323        }
324
325        // Check for Handover Select, followed by a BT OOB record
326        if (tnf == NdefRecord.TNF_WELL_KNOWN &&
327                Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) {
328            return parseBluetoothHandoverSelect(m);
329        }
330
331        // Check for Nokia BT record, found on some Nokia BH-505 Headsets
332        if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) {
333            return parseNokia(ByteBuffer.wrap(r.getPayload()));
334        }
335
336        return null;
337    }
338
339    BluetoothHandoverData parseNokia(ByteBuffer payload) {
340        BluetoothHandoverData result = new BluetoothHandoverData();
341        result.valid = false;
342
343        try {
344            payload.position(1);
345            byte[] address = new byte[6];
346            payload.get(address);
347            result.device = mBluetoothAdapter.getRemoteDevice(address);
348            result.valid = true;
349            payload.position(14);
350            int nameLength = payload.get();
351            byte[] nameBytes = new byte[nameLength];
352            payload.get(nameBytes);
353            result.name = new String(nameBytes, StandardCharsets.UTF_8);
354        } catch (IllegalArgumentException e) {
355            Log.i(TAG, "nokia: invalid BT address");
356        } catch (BufferUnderflowException e) {
357            Log.i(TAG, "nokia: payload shorter than expected");
358        }
359        if (result.valid && result.name == null) result.name = "";
360        return result;
361    }
362
363    BluetoothHandoverData parseBtOob(ByteBuffer payload) {
364        BluetoothHandoverData result = new BluetoothHandoverData();
365        result.valid = false;
366
367        try {
368            payload.position(2); // length
369            byte[] address = parseMacFromBluetoothRecord(payload);
370            result.device = mBluetoothAdapter.getRemoteDevice(address);
371            result.valid = true;
372
373            while (payload.remaining() > 0) {
374                byte[] nameBytes;
375                int len = payload.get();
376                int type = payload.get();
377                switch (type) {
378                    case BT_HANDOVER_TYPE_SHORT_LOCAL_NAME:
379                        nameBytes = new byte[len - 1];
380                        payload.get(nameBytes);
381                        result.name = new String(nameBytes, StandardCharsets.UTF_8);
382                        break;
383                    case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
384                        if (result.name != null) break;  // prefer short name
385                        nameBytes = new byte[len - 1];
386                        payload.get(nameBytes);
387                        result.name = new String(nameBytes, StandardCharsets.UTF_8);
388                        break;
389                    default:
390                        payload.position(payload.position() + len - 1);
391                        break;
392                }
393            }
394        } catch (IllegalArgumentException e) {
395            Log.i(TAG, "BT OOB: invalid BT address");
396        } catch (BufferUnderflowException e) {
397            Log.i(TAG, "BT OOB: payload shorter than expected");
398        }
399        if (result.valid && result.name == null) result.name = "";
400        return result;
401    }
402
403    BluetoothHandoverData parseBleOob(ByteBuffer payload) {
404        BluetoothHandoverData result = new BluetoothHandoverData();
405        result.valid = false;
406        result.transport = BluetoothDevice.TRANSPORT_LE;
407
408        try {
409
410            while (payload.remaining() > 0) {
411                byte[] nameBytes;
412                int len = payload.get();
413                int type = payload.get();
414                switch (type) {
415                    case BT_HANDOVER_TYPE_MAC: // mac address
416                        byte[] address = parseMacFromBluetoothRecord(payload);
417                        payload.position(payload.position() + 1); // advance over random byte
418                        result.device = mBluetoothAdapter.getRemoteDevice(address);
419                        result.valid = true;
420                        break;
421                    case BT_HANDOVER_TYPE_LE_ROLE:
422                        byte role = payload.get();
423                        if (role == BT_HANDOVER_LE_ROLE_CENTRAL_ONLY) {
424                            // only central role supported, can't pair
425                            result.valid = false;
426                            return result;
427                        }
428                        break;
429                    case BT_HANDOVER_TYPE_LONG_LOCAL_NAME:
430                        nameBytes = new byte[len - 1];
431                        payload.get(nameBytes);
432                        result.name = new String(nameBytes, StandardCharsets.UTF_8);
433                        break;
434                    default:
435                        payload.position(payload.position() + len - 1);
436                        break;
437                }
438            }
439        } catch (IllegalArgumentException e) {
440            Log.i(TAG, "BT OOB: invalid BT address");
441        } catch (BufferUnderflowException e) {
442            Log.i(TAG, "BT OOB: payload shorter than expected");
443        }
444        if (result.valid && result.name == null) result.name = "";
445        return result;
446    }
447
448    private byte[] parseMacFromBluetoothRecord(ByteBuffer payload) {
449        byte[] address = new byte[6];
450        payload.get(address);
451        // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
452        // ByteBuffer.get(byte[]), so manually swap order
453        for (int i = 0; i < 3; i++) {
454            byte temp = address[i];
455            address[i] = address[5 - i];
456            address[5 - i] = temp;
457        }
458        return address;
459    }
460
461    static byte[] addressToReverseBytes(String address) {
462        String[] split = address.split(":");
463        byte[] result = new byte[split.length];
464
465        for (int i = 0; i < split.length; i++) {
466            // need to parse as int because parseByte() expects a signed byte
467            result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16);
468        }
469
470        return result;
471    }
472}
473
474