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 
17 package com.android.nfc.handover;
18 
19 import java.nio.BufferUnderflowException;
20 import java.nio.ByteBuffer;
21 import java.nio.charset.Charset;
22 import java.util.Arrays;
23 import java.util.Random;
24 
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothDevice;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.nfc.FormatException;
30 import android.nfc.NdefMessage;
31 import android.nfc.NdefRecord;
32 import android.os.UserHandle;
33 import android.util.Log;
34 
35 /**
36  * Manages handover of NFC to other technologies.
37  */
38 public 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 
IncomingHandoverData(NdefMessage handoverSelect, BluetoothHandoverData handoverData)81         public IncomingHandoverData(NdefMessage handoverSelect,
82                                     BluetoothHandoverData handoverData) {
83             this.handoverSelect = handoverSelect;
84             this.handoverData = handoverData;
85         }
86     }
87 
HandoverDataParser()88     public HandoverDataParser() {
89         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
90     }
91 
createCollisionRecord()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 
createBluetoothAlternateCarrierRecord(boolean activating)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 
createBluetoothOobDataRecord()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 
isHandoverSupported()130     public boolean isHandoverSupported() {
131         return (mBluetoothAdapter != null);
132     }
133 
createHandoverRequestMessage()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 
createBluetoothHandoverSelectMessage(boolean activating)147     NdefMessage createBluetoothHandoverSelectMessage(boolean activating) {
148         return new NdefMessage(createHandoverSelectRecord(
149                 createBluetoothAlternateCarrierRecord(activating)),
150                 createBluetoothOobDataRecord());
151     }
152 
createHandoverSelectRecord(NdefRecord alternateCarrier)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 
createHandoverRequestRecord()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      */
getIncomingHandoverData(NdefMessage handoverRequest)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 
getOutgoingHandoverData(NdefMessage handoverSelect)225     public BluetoothHandoverData getOutgoingHandoverData(NdefMessage handoverSelect) {
226         return parseBluetooth(handoverSelect);
227     }
228 
tryBluetoothHandoverRequest(BluetoothHandoverData bluetoothData)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 
isCarrierActivating(NdefRecord handoverRec, byte[] carrierId)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 
parseBluetoothHandoverSelect(NdefMessage m)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 
parseBluetooth(NdefMessage m)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 
parseNokia(ByteBuffer payload)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 
parseBtOob(ByteBuffer payload)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 
parseBleOob(ByteBuffer payload)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 
parseMacFromBluetoothRecord(ByteBuffer payload)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 
addressToReverseBytes(String address)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