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.HashMap;
     24 import java.util.Random;
     25 
     26 import android.bluetooth.BluetoothAdapter;
     27 import android.bluetooth.BluetoothDevice;
     28 import android.content.BroadcastReceiver;
     29 import android.content.ComponentName;
     30 import android.content.Context;
     31 import android.content.Intent;
     32 import android.content.IntentFilter;
     33 import android.content.ServiceConnection;
     34 import android.net.Uri;
     35 import android.nfc.FormatException;
     36 import android.nfc.NdefMessage;
     37 import android.nfc.NdefRecord;
     38 import android.os.Bundle;
     39 import android.os.Handler;
     40 import android.os.IBinder;
     41 import android.os.Message;
     42 import android.os.Messenger;
     43 import android.os.RemoteException;
     44 import android.os.UserHandle;
     45 import android.util.Log;
     46 
     47 /**
     48  * Manages handover of NFC to other technologies.
     49  */
     50 public class HandoverManager {
     51     static final String TAG = "NfcHandover";
     52     static final boolean DBG = true;
     53 
     54     static final String ACTION_WHITELIST_DEVICE =
     55             "android.btopp.intent.action.WHITELIST_DEVICE";
     56 
     57     static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII"));
     58     static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob".
     59             getBytes(Charset.forName("US_ASCII"));
     60 
     61     static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr";
     62 
     63     static final int CARRIER_POWER_STATE_INACTIVE = 0;
     64     static final int CARRIER_POWER_STATE_ACTIVE = 1;
     65     static final int CARRIER_POWER_STATE_ACTIVATING = 2;
     66     static final int CARRIER_POWER_STATE_UNKNOWN = 3;
     67 
     68     static final int MSG_HANDOVER_COMPLETE = 0;
     69     static final int MSG_HEADSET_CONNECTED = 1;
     70     static final int MSG_HEADSET_NOT_CONNECTED = 2;
     71 
     72     final Context mContext;
     73     final BluetoothAdapter mBluetoothAdapter;
     74     final Messenger mMessenger = new Messenger (new MessageHandler());
     75 
     76     final Object mLock = new Object();
     77     // Variables below synchronized on mLock
     78     HashMap<Integer, PendingHandoverTransfer> mPendingTransfers;
     79     boolean mBluetoothHeadsetConnected;
     80     int mHandoverTransferId;
     81     Messenger mService = null;
     82     boolean mBound;
     83     String mLocalBluetoothAddress;
     84     boolean mEnabled;
     85 
     86     static class BluetoothHandoverData {
     87         public boolean valid = false;
     88         public BluetoothDevice device;
     89         public String name;
     90         public boolean carrierActivating = false;
     91     }
     92 
     93     class MessageHandler extends Handler {
     94         @Override
     95         public void handleMessage(Message msg) {
     96             synchronized (mLock) {
     97                 switch (msg.what) {
     98                     case MSG_HANDOVER_COMPLETE:
     99                         int transferId = msg.arg1;
    100                         Log.d(TAG, "Completed transfer id: " + Integer.toString(transferId));
    101                         if (mPendingTransfers.containsKey(transferId)) {
    102                             mPendingTransfers.remove(transferId);
    103                         } else {
    104                             Log.e(TAG, "Could not find completed transfer id: " + Integer.toString(transferId));
    105                         }
    106                         break;
    107                     case MSG_HEADSET_CONNECTED:
    108                         mBluetoothHeadsetConnected = true;
    109                         break;
    110                     case MSG_HEADSET_NOT_CONNECTED:
    111                         mBluetoothHeadsetConnected = false;
    112                         break;
    113                     default:
    114                         break;
    115                 }
    116             }
    117         }
    118     };
    119 
    120     private ServiceConnection mConnection = new ServiceConnection() {
    121         @Override
    122         public void onServiceConnected(ComponentName name, IBinder service) {
    123             synchronized (mLock) {
    124                 mService = new Messenger(service);
    125                 mBound = true;
    126                 // Register this client
    127                 Message msg = Message.obtain(null, HandoverService.MSG_REGISTER_CLIENT);
    128                 msg.replyTo = mMessenger;
    129                 try {
    130                     mService.send(msg);
    131                 } catch (RemoteException e) {
    132                     Log.e(TAG, "Failed to register client");
    133                 }
    134             }
    135         }
    136 
    137         @Override
    138         public void onServiceDisconnected(ComponentName name) {
    139             synchronized (mLock) {
    140                 if (mBound) {
    141                     try {
    142                         Message msg = Message.obtain(null, HandoverService.MSG_DEREGISTER_CLIENT);
    143                         msg.replyTo = mMessenger;
    144                         mService.send(msg);
    145                     } catch (RemoteException e) {
    146                         // Service may have crashed - ignore
    147                     }
    148                 }
    149                 mService = null;
    150                 mBound = false;
    151             }
    152         }
    153     };
    154 
    155     public HandoverManager(Context context) {
    156         mContext = context;
    157         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    158 
    159         mPendingTransfers = new HashMap<Integer, PendingHandoverTransfer>();
    160 
    161         IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
    162         mContext.registerReceiver(mReceiver, filter, null, null);
    163 
    164         mContext.bindServiceAsUser(new Intent(mContext, HandoverService.class), mConnection,
    165                 Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
    166         mEnabled = true;
    167     }
    168 
    169     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    170         @Override
    171         public void onReceive(Context context, Intent intent) {
    172             String action = intent.getAction();
    173             if (action.equals(Intent.ACTION_USER_SWITCHED)) {
    174                 // Re-bind a service for the current user
    175                 mContext.unbindService(mConnection);
    176                 mContext.bindServiceAsUser(new Intent(mContext, HandoverService.class), mConnection,
    177                         Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
    178             }
    179         }
    180     };
    181 
    182     static NdefRecord createCollisionRecord() {
    183         byte[] random = new byte[2];
    184         new Random().nextBytes(random);
    185         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, RTD_COLLISION_RESOLUTION, null, random);
    186     }
    187 
    188     NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) {
    189         byte[] payload = new byte[4];
    190         payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING :
    191             CARRIER_POWER_STATE_ACTIVE);  // Carrier Power State: Activating or active
    192         payload[1] = 1;   // length of carrier data reference
    193         payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record
    194         payload[3] = 0;  // Auxiliary data reference count
    195         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null, payload);
    196     }
    197 
    198     NdefRecord createBluetoothOobDataRecord() {
    199         byte[] payload = new byte[8];
    200         // Note: this field should be little-endian per the BTSSP spec
    201         // The Android 4.1 implementation used big-endian order here.
    202         // No single Android implementation has ever interpreted this
    203         // length field when parsing this record though.
    204         payload[0] = (byte) (payload.length & 0xFF);
    205         payload[1] = (byte) ((payload.length >> 8) & 0xFF);
    206 
    207         synchronized (mLock) {
    208             if (mLocalBluetoothAddress == null) {
    209                 mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
    210             }
    211 
    212             byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
    213             System.arraycopy(addressBytes, 0, payload, 2, 6);
    214         }
    215 
    216         return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
    217     }
    218 
    219     public void setEnabled(boolean enabled) {
    220         synchronized (mLock) {
    221             mEnabled = enabled;
    222         }
    223     }
    224     public boolean isHandoverSupported() {
    225         return (mBluetoothAdapter != null);
    226     }
    227 
    228     public NdefMessage createHandoverRequestMessage() {
    229         if (mBluetoothAdapter == null) return null;
    230 
    231         return new NdefMessage(createHandoverRequestRecord(), createBluetoothOobDataRecord());
    232     }
    233 
    234     NdefMessage createHandoverSelectMessage(boolean activating) {
    235         return new NdefMessage(createHandoverSelectRecord(activating), createBluetoothOobDataRecord());
    236     }
    237 
    238     NdefRecord createHandoverSelectRecord(boolean activating) {
    239         NdefMessage nestedMessage = new NdefMessage(createBluetoothAlternateCarrierRecord(activating));
    240         byte[] nestedPayload = nestedMessage.toByteArray();
    241 
    242         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
    243         payload.put((byte)0x12);  // connection handover v1.2
    244         payload.put(nestedPayload);
    245 
    246         byte[] payloadBytes = new byte[payload.position()];
    247         payload.position(0);
    248         payload.get(payloadBytes);
    249         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
    250                 payloadBytes);
    251     }
    252 
    253     NdefRecord createHandoverRequestRecord() {
    254         NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(),
    255                 createBluetoothAlternateCarrierRecord(false));
    256         byte[] nestedPayload = nestedMessage.toByteArray();
    257 
    258         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
    259         payload.put((byte)0x12);  // connection handover v1.2
    260         payload.put(nestedMessage.toByteArray());
    261 
    262         byte[] payloadBytes = new byte[payload.position()];
    263         payload.position(0);
    264         payload.get(payloadBytes);
    265         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
    266                 payloadBytes);
    267     }
    268 
    269     /**
    270      * Return null if message is not a Handover Request,
    271      * return the Handover Select response if it is.
    272      */
    273     public NdefMessage tryHandoverRequest(NdefMessage m) {
    274         if (m == null) return null;
    275         if (mBluetoothAdapter == null) return null;
    276 
    277         if (DBG) Log.d(TAG, "tryHandoverRequest():" + m.toString());
    278 
    279         NdefRecord r = m.getRecords()[0];
    280         if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null;
    281         if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null;
    282 
    283         // we have a handover request, look for BT OOB record
    284         BluetoothHandoverData bluetoothData = null;
    285         for (NdefRecord oob : m.getRecords()) {
    286             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
    287                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
    288                 bluetoothData = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
    289                 break;
    290             }
    291         }
    292         if (bluetoothData == null) return null;
    293 
    294         // Note: there could be a race where we conclude
    295         // that Bluetooth is already enabled, and shortly
    296         // after the user turns it off. That will cause
    297         // the transfer to fail, but there's nothing
    298         // much we can do about it anyway. It shouldn't
    299         // be common for the user to be changing BT settings
    300         // while waiting to receive a picture.
    301         boolean bluetoothActivating = !mBluetoothAdapter.isEnabled();
    302         synchronized (mLock) {
    303             if (!mEnabled) return null;
    304 
    305             if (!mBound) {
    306                 Log.e(TAG, "Could not connect to handover service");
    307                 return null;
    308             }
    309             Message msg = Message.obtain(null, HandoverService.MSG_START_INCOMING_TRANSFER);
    310             PendingHandoverTransfer transfer = registerInTransferLocked(bluetoothData.device);
    311             Bundle transferData = new Bundle();
    312             transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer);
    313             msg.setData(transferData);
    314             try {
    315                 mService.send(msg);
    316             } catch (RemoteException e) {
    317                 Log.e(TAG, "Could not connect to handover service");
    318                removeTransferLocked(transfer.id);
    319                return null;
    320             }
    321         }
    322         // BT OOB found, whitelist it for incoming OPP data
    323         whitelistOppDevice(bluetoothData.device);
    324 
    325         // return BT OOB record so they can perform handover
    326         NdefMessage selectMessage = (createHandoverSelectMessage(bluetoothActivating));
    327         if (DBG) Log.d(TAG, "Waiting for incoming transfer, [" +
    328                 bluetoothData.device.getAddress() + "]->[" + mLocalBluetoothAddress + "]");
    329 
    330         return selectMessage;
    331     }
    332 
    333     public boolean tryHandover(NdefMessage m) {
    334         if (m == null) return false;
    335         if (mBluetoothAdapter == null) return false;
    336 
    337         if (DBG) Log.d(TAG, "tryHandover(): " + m.toString());
    338 
    339         BluetoothHandoverData handover = parse(m);
    340         if (handover == null) return false;
    341         if (!handover.valid) return true;
    342 
    343         synchronized (mLock) {
    344             if (!mEnabled) return false;
    345 
    346             if (mBluetoothAdapter == null) {
    347                 if (DBG) Log.d(TAG, "BT handover, but BT not available");
    348                 return true;
    349             }
    350             if (!mBound) {
    351                 Log.e(TAG, "Could not connect to handover service");
    352                 return false;
    353             }
    354 
    355             Message msg = Message.obtain(null, HandoverService.MSG_HEADSET_HANDOVER, 0, 0);
    356             Bundle headsetData = new Bundle();
    357             headsetData.putParcelable(HandoverService.EXTRA_HEADSET_DEVICE, handover.device);
    358             headsetData.putString(HandoverService.EXTRA_HEADSET_NAME, handover.name);
    359             msg.setData(headsetData);
    360             try {
    361                 mService.send(msg);
    362             } catch (RemoteException e) {
    363                 return false;
    364             }
    365         }
    366         return true;
    367     }
    368 
    369     // This starts sending an Uri over BT
    370     public void doHandoverUri(Uri[] uris, NdefMessage m) {
    371         if (mBluetoothAdapter == null) return;
    372 
    373         BluetoothHandoverData data = parse(m);
    374         if (data != null && data.valid) {
    375             // Register a new handover transfer object
    376             synchronized (mLock) {
    377                 if (!mBound) {
    378                     Log.e(TAG, "Could not connect to handover service");
    379                     return;
    380                 }
    381 
    382                 Message msg = Message.obtain(null, HandoverService.MSG_START_OUTGOING_TRANSFER, 0, 0);
    383                 PendingHandoverTransfer transfer = registerOutTransferLocked(data, uris);
    384                 Bundle transferData = new Bundle();
    385                 transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer);
    386                 msg.setData(transferData);
    387                 if (DBG) Log.d(TAG, "Initiating outgoing transfer, [" + mLocalBluetoothAddress +
    388                         "]->[" + data.device.getAddress() + "]");
    389                 try {
    390                     mService.send(msg);
    391                 } catch (RemoteException e) {
    392                     removeTransferLocked(transfer.id);
    393                 }
    394             }
    395         }
    396     }
    397 
    398     PendingHandoverTransfer registerInTransferLocked(BluetoothDevice remoteDevice) {
    399         PendingHandoverTransfer transfer = new PendingHandoverTransfer(
    400                 mHandoverTransferId++, true, remoteDevice, false, null);
    401         mPendingTransfers.put(transfer.id, transfer);
    402 
    403         return transfer;
    404     }
    405 
    406     PendingHandoverTransfer registerOutTransferLocked(BluetoothHandoverData data,
    407             Uri[] uris) {
    408         PendingHandoverTransfer transfer = new PendingHandoverTransfer(
    409                 mHandoverTransferId++, false, data.device, data.carrierActivating, uris);
    410         mPendingTransfers.put(transfer.id, transfer);
    411         return transfer;
    412     }
    413 
    414     void removeTransferLocked(int id) {
    415         mPendingTransfers.remove(id);
    416     }
    417 
    418     void whitelistOppDevice(BluetoothDevice device) {
    419         if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
    420         Intent intent = new Intent(ACTION_WHITELIST_DEVICE);
    421         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    422         mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
    423     }
    424 
    425     boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
    426         byte[] payload = handoverRec.getPayload();
    427         if (payload == null || payload.length <= 1) return false;
    428         // Skip version
    429         byte[] payloadNdef = new byte[payload.length - 1];
    430         System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1);
    431         NdefMessage msg;
    432         try {
    433             msg = new NdefMessage(payloadNdef);
    434         } catch (FormatException e) {
    435             return false;
    436         }
    437 
    438         for (NdefRecord alt : msg.getRecords()) {
    439             byte[] acPayload = alt.getPayload();
    440             if (acPayload != null) {
    441                 ByteBuffer buf = ByteBuffer.wrap(acPayload);
    442                 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits
    443                 int carrierRefLength = buf.get() & 0xFF;
    444                 if (carrierRefLength != carrierId.length) return false;
    445 
    446                 byte[] carrierRefId = new byte[carrierRefLength];
    447                 buf.get(carrierRefId);
    448                 if (Arrays.equals(carrierRefId, carrierId)) {
    449                     // Found match, returning whether power state is activating
    450                     return (cps == CARRIER_POWER_STATE_ACTIVATING);
    451                 }
    452             }
    453         }
    454 
    455         return true;
    456     }
    457 
    458     BluetoothHandoverData parseHandoverSelect(NdefMessage m) {
    459         // TODO we could parse this a lot more strictly; right now
    460         // we just search for a BT OOB record, and try to cross-reference
    461         // the carrier state inside the 'hs' payload.
    462         for (NdefRecord oob : m.getRecords()) {
    463             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
    464                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
    465                 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
    466                 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) {
    467                     data.carrierActivating = true;
    468                 }
    469                 return data;
    470             }
    471         }
    472 
    473         return null;
    474     }
    475 
    476     BluetoothHandoverData parse(NdefMessage m) {
    477         NdefRecord r = m.getRecords()[0];
    478         short tnf = r.getTnf();
    479         byte[] type = r.getType();
    480 
    481         // Check for BT OOB record
    482         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) {
    483             return parseBtOob(ByteBuffer.wrap(r.getPayload()));
    484         }
    485 
    486         // Check for Handover Select, followed by a BT OOB record
    487         if (tnf == NdefRecord.TNF_WELL_KNOWN &&
    488                 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) {
    489             return parseHandoverSelect(m);
    490         }
    491 
    492         // Check for Nokia BT record, found on some Nokia BH-505 Headsets
    493         if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) {
    494             return parseNokia(ByteBuffer.wrap(r.getPayload()));
    495         }
    496 
    497         return null;
    498     }
    499 
    500     BluetoothHandoverData parseNokia(ByteBuffer payload) {
    501         BluetoothHandoverData result = new BluetoothHandoverData();
    502         result.valid = false;
    503 
    504         try {
    505             payload.position(1);
    506             byte[] address = new byte[6];
    507             payload.get(address);
    508             result.device = mBluetoothAdapter.getRemoteDevice(address);
    509             result.valid = true;
    510             payload.position(14);
    511             int nameLength = payload.get();
    512             byte[] nameBytes = new byte[nameLength];
    513             payload.get(nameBytes);
    514             result.name = new String(nameBytes, Charset.forName("UTF-8"));
    515         } catch (IllegalArgumentException e) {
    516             Log.i(TAG, "nokia: invalid BT address");
    517         } catch (BufferUnderflowException e) {
    518             Log.i(TAG, "nokia: payload shorter than expected");
    519         }
    520         if (result.valid && result.name == null) result.name = "";
    521         return result;
    522     }
    523 
    524     BluetoothHandoverData parseBtOob(ByteBuffer payload) {
    525         BluetoothHandoverData result = new BluetoothHandoverData();
    526         result.valid = false;
    527 
    528         try {
    529             payload.position(2);
    530             byte[] address = new byte[6];
    531             payload.get(address);
    532             // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
    533             // ByteBuffer.get(byte[]), so manually swap order
    534             for (int i = 0; i < 3; i++) {
    535                 byte temp = address[i];
    536                 address[i] = address[5 - i];
    537                 address[5 - i] = temp;
    538             }
    539             result.device = mBluetoothAdapter.getRemoteDevice(address);
    540             result.valid = true;
    541 
    542             while (payload.remaining() > 0) {
    543                 byte[] nameBytes;
    544                 int len = payload.get();
    545                 int type = payload.get();
    546                 switch (type) {
    547                     case 0x08:  // short local name
    548                         nameBytes = new byte[len - 1];
    549                         payload.get(nameBytes);
    550                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    551                         break;
    552                     case 0x09:  // long local name
    553                         if (result.name != null) break;  // prefer short name
    554                         nameBytes = new byte[len - 1];
    555                         payload.get(nameBytes);
    556                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    557                         break;
    558                     default:
    559                         payload.position(payload.position() + len - 1);
    560                         break;
    561                 }
    562             }
    563         } catch (IllegalArgumentException e) {
    564             Log.i(TAG, "BT OOB: invalid BT address");
    565         } catch (BufferUnderflowException e) {
    566             Log.i(TAG, "BT OOB: payload shorter than expected");
    567         }
    568         if (result.valid && result.name == null) result.name = "";
    569         return result;
    570     }
    571 
    572     static byte[] addressToReverseBytes(String address) {
    573         String[] split = address.split(":");
    574         byte[] result = new byte[split.length];
    575 
    576         for (int i = 0; i < split.length; i++) {
    577             // need to parse as int because parseByte() expects a signed byte
    578             result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16);
    579         }
    580 
    581         return result;
    582     }
    583 }
    584