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