Home | History | Annotate | Download | only in handover
      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 
     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