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.StandardCharsets;
     22 import java.util.ArrayList;
     23 import java.nio.charset.Charset;
     24 import java.util.Arrays;
     25 import java.util.Random;
     26 
     27 import android.bluetooth.BluetoothAdapter;
     28 import android.bluetooth.BluetoothDevice;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.nfc.FormatException;
     32 import android.nfc.NdefMessage;
     33 import android.nfc.NdefRecord;
     34 import android.os.UserHandle;
     35 import android.util.Log;
     36 
     37 /**
     38  * Manages handover of NFC to other technologies.
     39  */
     40 public 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