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.io.File;
     20 import java.nio.BufferUnderflowException;
     21 import java.nio.ByteBuffer;
     22 import java.nio.charset.Charset;
     23 import java.text.SimpleDateFormat;
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.Date;
     27 import java.util.HashMap;
     28 import java.util.Iterator;
     29 import java.util.Map;
     30 import java.util.Random;
     31 
     32 import android.app.Notification;
     33 import android.app.NotificationManager;
     34 import android.app.PendingIntent;
     35 import android.app.Notification.Builder;
     36 import android.bluetooth.BluetoothA2dp;
     37 import android.bluetooth.BluetoothAdapter;
     38 import android.bluetooth.BluetoothDevice;
     39 import android.bluetooth.BluetoothHeadset;
     40 import android.bluetooth.BluetoothProfile;
     41 import android.content.BroadcastReceiver;
     42 import android.content.ContentResolver;
     43 import android.content.Context;
     44 import android.content.Intent;
     45 import android.content.IntentFilter;
     46 import android.media.MediaScannerConnection;
     47 import android.net.Uri;
     48 import android.nfc.FormatException;
     49 import android.nfc.NdefMessage;
     50 import android.nfc.NdefRecord;
     51 import android.os.Environment;
     52 import android.os.Handler;
     53 import android.os.Message;
     54 import android.os.SystemClock;
     55 import android.util.Log;
     56 import android.util.Pair;
     57 
     58 import com.android.nfc.NfcService;
     59 import com.android.nfc.R;
     60 
     61 
     62 /**
     63  * Manages handover of NFC to other technologies.
     64  */
     65 public class HandoverManager implements BluetoothProfile.ServiceListener,
     66         BluetoothHeadsetHandover.Callback {
     67     static final String TAG = "NfcHandover";
     68     static final boolean DBG = true;
     69 
     70     static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII"));
     71     static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob".
     72             getBytes(Charset.forName("US_ASCII"));
     73 
     74     static final String ACTION_BT_OPP_TRANSFER_PROGRESS =
     75             "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS";
     76 
     77     static final String ACTION_BT_OPP_TRANSFER_DONE =
     78             "android.btopp.intent.action.BT_OPP_TRANSFER_DONE";
     79 
     80     static final String EXTRA_BT_OPP_TRANSFER_STATUS =
     81             "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS";
     82 
     83     static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE =
     84             "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE";
     85 
     86     static final String EXTRA_BT_OPP_ADDRESS =
     87             "android.btopp.intent.extra.BT_OPP_ADDRESS";
     88 
     89     static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0;
     90 
     91     static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1;
     92 
     93     static final String EXTRA_BT_OPP_TRANSFER_DIRECTION =
     94             "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION";
     95 
     96     static final int DIRECTION_BLUETOOTH_INCOMING = 0;
     97 
     98     static final int DIRECTION_BLUETOOTH_OUTGOING = 1;
     99 
    100     static final String EXTRA_BT_OPP_TRANSFER_ID =
    101             "android.btopp.intent.extra.BT_OPP_TRANSFER_ID";
    102 
    103     static final String EXTRA_BT_OPP_TRANSFER_PROGRESS =
    104             "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS";
    105 
    106     static final String EXTRA_BT_OPP_TRANSFER_URI =
    107             "android.btopp.intent.extra.BT_OPP_TRANSFER_URI";
    108 
    109     // permission needed to be able to receive handover status requests
    110     static final String HANDOVER_STATUS_PERMISSION =
    111             "com.android.permission.HANDOVER_STATUS";
    112 
    113     static final int MSG_HANDOVER_POWER_CHECK = 0;
    114 
    115     // We poll whether we can safely disable BT every POWER_CHECK_MS
    116     static final int POWER_CHECK_MS = 20000;
    117 
    118     static final String ACTION_WHITELIST_DEVICE =
    119             "android.btopp.intent.action.WHITELIST_DEVICE";
    120 
    121     static final String ACTION_CANCEL_HANDOVER_TRANSFER =
    122             "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER";
    123     static final String EXTRA_SOURCE_ADDRESS =
    124             "com.android.nfc.handover.extra.SOURCE_ADDRESS";
    125 
    126     static final int SOURCE_BLUETOOTH_INCOMING = 0;
    127 
    128     static final int SOURCE_BLUETOOTH_OUTGOING = 1;
    129 
    130     static final int CARRIER_POWER_STATE_INACTIVE = 0;
    131     static final int CARRIER_POWER_STATE_ACTIVE = 1;
    132     static final int CARRIER_POWER_STATE_ACTIVATING = 2;
    133     static final int CARRIER_POWER_STATE_UNKNOWN = 3;
    134 
    135     final Context mContext;
    136     final BluetoothAdapter mBluetoothAdapter;
    137     final NotificationManager mNotificationManager;
    138     final HandoverPowerManager mHandoverPowerManager;
    139 
    140     // Variables below synchronized on HandoverManager.this
    141     final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers;
    142 
    143     BluetoothHeadset mBluetoothHeadset;
    144     BluetoothA2dp mBluetoothA2dp;
    145     BluetoothHeadsetHandover mBluetoothHeadsetHandover;
    146     boolean mBluetoothHeadsetConnected;
    147 
    148     String mLocalBluetoothAddress;
    149     int mNotificationId;
    150 
    151     static class BluetoothHandoverData {
    152         public boolean valid = false;
    153         public BluetoothDevice device;
    154         public String name;
    155         public boolean carrierActivating = false;
    156     }
    157 
    158     class HandoverPowerManager implements Handler.Callback {
    159         final Handler handler;
    160         final Context context;
    161 
    162         public HandoverPowerManager(Context context) {
    163             this.handler = new Handler(this);
    164             this.context = context;
    165         }
    166 
    167         /**
    168          * Enables Bluetooth and will automatically disable it
    169          * when there is no Bluetooth activity intitiated by NFC
    170          * anymore.
    171          */
    172         synchronized boolean enableBluetooth() {
    173             // Enable BT
    174             boolean result = mBluetoothAdapter.enableNoAutoConnect();
    175 
    176             if (result) {
    177                 // Start polling for BT activity to make sure we eventually disable
    178                 // it again.
    179                 handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS);
    180             }
    181             return result;
    182         }
    183 
    184         synchronized boolean isBluetoothEnabled() {
    185             return mBluetoothAdapter.isEnabled();
    186         }
    187 
    188         synchronized void resetTimer() {
    189             if (handler.hasMessages(MSG_HANDOVER_POWER_CHECK)) {
    190                 handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
    191                 handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS);
    192             }
    193         }
    194 
    195         void stopMonitoring() {
    196             handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
    197         }
    198 
    199         @Override
    200         public boolean handleMessage(Message msg) {
    201             switch (msg.what) {
    202                 case MSG_HANDOVER_POWER_CHECK:
    203                     // Check for any alive transfers
    204                     boolean transferAlive = false;
    205                     synchronized (HandoverManager.this) {
    206                         for (HandoverTransfer transfer : mTransfers.values()) {
    207                             if (transfer.isRunning()) {
    208                                 transferAlive = true;
    209                             }
    210                         }
    211 
    212                         if (!transferAlive && !mBluetoothHeadsetConnected) {
    213                             mBluetoothAdapter.disable();
    214                             handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
    215                         } else {
    216                             handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS);
    217                         }
    218                     }
    219                     return true;
    220             }
    221             return false;
    222         }
    223     }
    224 
    225     /**
    226      * A HandoverTransfer object represents a set of files
    227      * that were received through NFC connection handover
    228      * from the same source address.
    229      *
    230      * For Bluetooth, files are received through OPP, and
    231      * we have no knowledge how many files will be transferred
    232      * as part of a single transaction.
    233      * Hence, a transfer has a notion of being "alive": if
    234      * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS
    235      * milliseconds, we consider a new file transfer from the
    236      * same source address as part of the same transfer.
    237      * The corresponding URIs will be grouped in a single folder.
    238      *
    239      */
    240     class HandoverTransfer implements Handler.Callback,
    241             MediaScannerConnection.OnScanCompletedListener {
    242         // In the states below we still accept new file transfer
    243         static final int STATE_NEW = 0;
    244         static final int STATE_IN_PROGRESS = 1;
    245         static final int STATE_W4_NEXT_TRANSFER = 2;
    246 
    247         // In the states below no new files are accepted.
    248         static final int STATE_W4_MEDIA_SCANNER = 3;
    249         static final int STATE_FAILED = 4;
    250         static final int STATE_SUCCESS = 5;
    251         static final int STATE_CANCELLED = 6;
    252 
    253         static final int MSG_NEXT_TRANSFER_TIMER = 0;
    254         static final int MSG_TRANSFER_TIMEOUT = 1;
    255 
    256         // We need to receive an update within this time period
    257         // to still consider this transfer to be "alive" (ie
    258         // a reason to keep the handover transport enabled).
    259         static final int ALIVE_CHECK_MS = 20000;
    260 
    261         // The amount of time to wait for a new transfer
    262         // once the current one completes.
    263         static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
    264 
    265         static final String BEAM_DIR = "beam";
    266 
    267         final BluetoothDevice device;
    268         final String sourceAddress;
    269         final boolean incoming;  // whether this is an incoming transfer
    270         final int notificationId; // Unique ID of this transfer used for notifications
    271         final Handler handler;
    272         final PendingIntent cancelIntent;
    273 
    274         int state;
    275         Long lastUpdate; // Last time an event occurred for this transfer
    276         float progress; // Progress in range [0..1]
    277         ArrayList<Uri> btUris; // Received uris from Bluetooth OPP
    278         ArrayList<String> btMimeTypes; // Mime-types received from Bluetooth OPP
    279 
    280         ArrayList<String> paths; // Raw paths on the filesystem for Beam-stored files
    281         HashMap<String, String> mimeTypes; // Mime-types associated with each path
    282         HashMap<String, Uri> mediaUris; // URIs found by the media scanner for each path
    283         int urisScanned;
    284 
    285         public HandoverTransfer(String sourceAddress, boolean incoming) {
    286             synchronized (HandoverManager.this) {
    287                 this.notificationId = mNotificationId++;
    288             }
    289             this.lastUpdate = SystemClock.elapsedRealtime();
    290             this.progress = 0.0f;
    291             this.state = STATE_NEW;
    292             this.btUris = new ArrayList<Uri>();
    293             this.btMimeTypes = new ArrayList<String>();
    294             this.paths = new ArrayList<String>();
    295             this.mimeTypes = new HashMap<String, String>();
    296             this.mediaUris = new HashMap<String, Uri>();
    297             this.sourceAddress = sourceAddress;
    298             this.incoming = incoming;
    299             this.handler = new Handler(mContext.getMainLooper(), this);
    300             this.cancelIntent = buildCancelIntent();
    301             this.urisScanned = 0;
    302             this.device = mBluetoothAdapter.getRemoteDevice(sourceAddress);
    303 
    304             handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    305         }
    306 
    307         public synchronized void updateFileProgress(float progress) {
    308             if (!isRunning()) return; // Ignore when we're no longer running
    309 
    310             handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    311 
    312             this.progress = progress;
    313 
    314             // We're still receiving data from this device - keep it in
    315             // the whitelist for a while longer
    316             if (incoming) whitelistOppDevice(device);
    317 
    318             updateStateAndNotification(STATE_IN_PROGRESS);
    319         }
    320 
    321         public synchronized void finishTransfer(boolean success, Uri uri, String mimeType) {
    322             if (!isRunning()) return; // Ignore when we're no longer running
    323 
    324             if (success && uri != null) {
    325                 if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
    326                 this.progress = 1.0f;
    327                 if (mimeType == null) {
    328                     mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
    329                 }
    330                 if (mimeType != null) {
    331                     btUris.add(uri);
    332                     btMimeTypes.add(mimeType);
    333                 } else {
    334                     if (DBG) Log.d(TAG, "Could not get mimeType for file.");
    335                 }
    336             } else {
    337                 Log.e(TAG, "Handover transfer failed");
    338                 // Do wait to see if there's another file coming.
    339             }
    340             handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    341             handler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
    342             updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
    343         }
    344 
    345         public synchronized boolean isRunning() {
    346             if (state != STATE_NEW && state != STATE_IN_PROGRESS && state != STATE_W4_NEXT_TRANSFER) {
    347                 return false;
    348             } else {
    349                 return true;
    350             }
    351         }
    352 
    353         synchronized void cancel() {
    354             if (!isRunning()) return;
    355 
    356             // Delete all files received so far
    357             for (Uri uri : btUris) {
    358                 File file = new File(uri.getPath());
    359                 if (file.exists()) file.delete();
    360             }
    361 
    362             updateStateAndNotification(STATE_CANCELLED);
    363         }
    364 
    365         synchronized void updateNotification() {
    366             if (!incoming) return; // No notifications for outgoing transfers
    367 
    368             Builder notBuilder = new Notification.Builder(mContext);
    369 
    370             if (state == STATE_NEW || state == STATE_IN_PROGRESS ||
    371                     state == STATE_W4_NEXT_TRANSFER || state == STATE_W4_MEDIA_SCANNER) {
    372                 notBuilder.setAutoCancel(false);
    373                 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
    374                 notBuilder.setTicker(mContext.getString(R.string.beam_progress));
    375                 notBuilder.setContentTitle(mContext.getString(R.string.beam_progress));
    376                 notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark,
    377                         mContext.getString(R.string.cancel), cancelIntent);
    378                 notBuilder.setDeleteIntent(cancelIntent);
    379                 // We do have progress indication on a per-file basis, but in a multi-file
    380                 // transfer we don't know the total progress. So for now, just show an
    381                 // indeterminate progress bar.
    382                 notBuilder.setProgress(100, 0, true);
    383             } else if (state == STATE_SUCCESS) {
    384                 notBuilder.setAutoCancel(true);
    385                 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
    386                 notBuilder.setTicker(mContext.getString(R.string.beam_complete));
    387                 notBuilder.setContentTitle(mContext.getString(R.string.beam_complete));
    388                 notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view));
    389 
    390                 Intent viewIntent = buildViewIntent();
    391                 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, viewIntent, 0);
    392 
    393                 notBuilder.setContentIntent(contentIntent);
    394 
    395                 // Play Beam success sound
    396                 NfcService.getInstance().playSound(NfcService.SOUND_END);
    397             } else if (state == STATE_FAILED) {
    398                 notBuilder.setAutoCancel(false);
    399                 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
    400                 notBuilder.setTicker(mContext.getString(R.string.beam_failed));
    401                 notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
    402             } else if (state == STATE_CANCELLED) {
    403                 notBuilder.setAutoCancel(false);
    404                 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
    405                 notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
    406                 notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
    407             } else {
    408                 return;
    409             }
    410 
    411             mNotificationManager.notify(mNotificationId, notBuilder.build());
    412         }
    413 
    414         synchronized void updateStateAndNotification(int newState) {
    415             this.state = newState;
    416             this.lastUpdate = SystemClock.elapsedRealtime();
    417 
    418             if (handler.hasMessages(MSG_TRANSFER_TIMEOUT)) {
    419                 // Update timeout timer
    420                 handler.removeMessages(MSG_TRANSFER_TIMEOUT);
    421                 handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    422             }
    423             updateNotification();
    424         }
    425 
    426         synchronized void processFiles() {
    427             // Check the amount of files we received in this transfer;
    428             // If more than one, create a separate directory for it.
    429             String extRoot = Environment.getExternalStorageDirectory().getPath();
    430             File beamPath = new File(extRoot + "/" + BEAM_DIR);
    431 
    432             if (!checkMediaStorage(beamPath) || btUris.size() == 0) {
    433                 Log.e(TAG, "Media storage not valid or no uris received.");
    434                 updateStateAndNotification(STATE_FAILED);
    435                 return;
    436             }
    437 
    438             if (btUris.size() > 1) {
    439                 beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
    440                 if (!beamPath.isDirectory() && !beamPath.mkdir()) {
    441                     Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
    442                     updateStateAndNotification(STATE_FAILED);
    443                     return;
    444                 }
    445             }
    446 
    447             for (int i = 0; i < btUris.size(); i++) {
    448                 Uri uri = btUris.get(i);
    449                 String mimeType = btMimeTypes.get(i);
    450 
    451                 File srcFile = new File(uri.getPath());
    452 
    453                 File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
    454                         uri.getLastPathSegment());
    455                 if (!srcFile.renameTo(dstFile)) {
    456                     if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
    457                     srcFile.delete();
    458                     return;
    459                 } else {
    460                     paths.add(dstFile.getAbsolutePath());
    461                     mimeTypes.put(dstFile.getAbsolutePath(), mimeType);
    462                     if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
    463                 }
    464             }
    465 
    466             // We can either add files to the media provider, or provide an ACTION_VIEW
    467             // intent to the file directly. We base this decision on the mime type
    468             // of the first file; if it's media the platform can deal with,
    469             // use the media provider, if it's something else, just launch an ACTION_VIEW
    470             // on the file.
    471             String mimeType = mimeTypes.get(paths.get(0));
    472             if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
    473                     mimeType.startsWith("audio/")) {
    474                 String[] arrayPaths = new String[paths.size()];
    475                 MediaScannerConnection.scanFile(mContext, paths.toArray(arrayPaths), null, this);
    476                 updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
    477             } else {
    478                 // We're done.
    479                 updateStateAndNotification(STATE_SUCCESS);
    480             }
    481 
    482         }
    483 
    484         public boolean handleMessage(Message msg) {
    485             if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
    486                 // We didn't receive a new transfer in time, finalize this one
    487                 if (incoming) {
    488                     processFiles();
    489                 } else {
    490                     updateStateAndNotification(STATE_SUCCESS);
    491                 }
    492                 return true;
    493             } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
    494                 // No update on this transfer for a while, check
    495                 // to see if it's still running, and fail it if it is.
    496                 if (isRunning()) {
    497                     updateStateAndNotification(STATE_FAILED);
    498                 }
    499             }
    500             return false;
    501         }
    502 
    503         public synchronized void onScanCompleted(String path, Uri uri) {
    504             if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
    505             if (uri != null) {
    506                 mediaUris.put(path, uri);
    507             }
    508             urisScanned++;
    509             if (urisScanned == paths.size()) {
    510                 // We're done
    511                 updateStateAndNotification(STATE_SUCCESS);
    512             }
    513         }
    514 
    515         boolean checkMediaStorage(File path) {
    516             if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    517                 if (!path.isDirectory() && !path.mkdir()) {
    518                     Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
    519                     return false;
    520                 }
    521                 return true;
    522             } else {
    523                 Log.e(TAG, "External storage not mounted, can't store file.");
    524                 return false;
    525             }
    526         }
    527 
    528         synchronized Intent buildViewIntent() {
    529             if (paths.size() == 0) return null;
    530 
    531             Intent viewIntent = new Intent(Intent.ACTION_VIEW);
    532 
    533             String filePath = paths.get(0);
    534             Uri mediaUri = mediaUris.get(filePath);
    535             Uri uri =  mediaUri != null ? mediaUri :
    536                 Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
    537             viewIntent.setDataAndTypeAndNormalize(uri, mimeTypes.get(filePath));
    538 
    539             return viewIntent;
    540         }
    541 
    542         PendingIntent buildCancelIntent() {
    543             Intent intent = new Intent(ACTION_CANCEL_HANDOVER_TRANSFER);
    544             intent.putExtra(EXTRA_SOURCE_ADDRESS, sourceAddress);
    545             PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
    546 
    547             return pi;
    548         }
    549 
    550         synchronized File generateUniqueDestination(String path, String fileName) {
    551             int dotIndex = fileName.lastIndexOf(".");
    552             String extension = null;
    553             String fileNameWithoutExtension = null;
    554             if (dotIndex < 0) {
    555                 extension = "";
    556                 fileNameWithoutExtension = fileName;
    557             } else {
    558                 extension = fileName.substring(dotIndex);
    559                 fileNameWithoutExtension = fileName.substring(0, dotIndex);
    560             }
    561             File dstFile = new File(path + File.separator + fileName);
    562             int count = 0;
    563             while (dstFile.exists()) {
    564                 dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
    565                         Integer.toString(count) + extension);
    566                 count++;
    567             }
    568             return dstFile;
    569         }
    570 
    571         synchronized File generateMultiplePath(String beamRoot) {
    572             // Generate a unique directory with the date
    573             String format = "yyyy-MM-dd";
    574             SimpleDateFormat sdf = new SimpleDateFormat(format);
    575             String newPath = beamRoot + "beam-" + sdf.format(new Date());
    576             File newFile = new File(newPath);
    577             int count = 0;
    578             while (newFile.exists()) {
    579                 newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
    580                         Integer.toString(count);
    581                 newFile = new File(newPath);
    582                 count++;
    583             }
    584 
    585             return newFile;
    586         }
    587     }
    588 
    589     synchronized HandoverTransfer getOrCreateHandoverTransfer(String sourceAddress, boolean incoming,
    590             boolean create) {
    591         Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming);
    592         if (mTransfers.containsKey(key)) {
    593             HandoverTransfer transfer = mTransfers.get(key);
    594             if (transfer.isRunning()) {
    595                 return transfer;
    596             } else {
    597                 if (create) mTransfers.remove(key); // new one created below
    598             }
    599         }
    600         if (create) {
    601             HandoverTransfer transfer = new HandoverTransfer(sourceAddress, incoming);
    602             mTransfers.put(key, transfer);
    603 
    604             return transfer;
    605         } else {
    606             return null;
    607         }
    608     }
    609 
    610     public HandoverManager(Context context) {
    611         mContext = context;
    612         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    613         if (mBluetoothAdapter != null) {
    614             mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET);
    615             mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP);
    616         }
    617 
    618         mNotificationManager = (NotificationManager) mContext.getSystemService(
    619                 Context.NOTIFICATION_SERVICE);
    620 
    621         mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>();
    622         mHandoverPowerManager = new HandoverPowerManager(context);
    623 
    624         IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE);
    625         filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS);
    626         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    627         filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER);
    628         mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null);
    629     }
    630 
    631     synchronized void cleanupTransfers() {
    632         Iterator<Map.Entry<Pair<String, Boolean>, HandoverTransfer>> it = mTransfers.entrySet().iterator();
    633         while (it.hasNext()) {
    634             Map.Entry<Pair<String, Boolean>, HandoverTransfer> pair = it.next();
    635             HandoverTransfer transfer = pair.getValue();
    636             if (!transfer.isRunning()) {
    637                 it.remove();
    638             }
    639         }
    640     }
    641 
    642     static NdefRecord createCollisionRecord() {
    643         byte[] random = new byte[2];
    644         new Random().nextBytes(random);
    645         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, random);
    646     }
    647 
    648     NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) {
    649         byte[] payload = new byte[4];
    650         payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING :
    651             CARRIER_POWER_STATE_ACTIVE);  // Carrier Power State: Activating or active
    652         payload[1] = 1;   // length of carrier data reference
    653         payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record
    654         payload[3] = 0;  // Auxiliary data reference count
    655         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null, payload);
    656     }
    657 
    658     NdefRecord createBluetoothOobDataRecord() {
    659         byte[] payload = new byte[8];
    660         payload[0] = 0;
    661         payload[1] = (byte)payload.length;
    662 
    663         synchronized (HandoverManager.this) {
    664             if (mLocalBluetoothAddress == null) {
    665                 mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
    666             }
    667 
    668             byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
    669             System.arraycopy(addressBytes, 0, payload, 2, 6);
    670         }
    671 
    672         return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
    673     }
    674 
    675     public boolean isHandoverSupported() {
    676         return (mBluetoothAdapter != null);
    677     }
    678 
    679     public NdefMessage createHandoverRequestMessage() {
    680         if (mBluetoothAdapter == null) return null;
    681 
    682         return new NdefMessage(createHandoverRequestRecord(), createBluetoothOobDataRecord());
    683     }
    684 
    685     NdefMessage createHandoverSelectMessage(boolean activating) {
    686         return new NdefMessage(createHandoverSelectRecord(activating), createBluetoothOobDataRecord());
    687     }
    688 
    689     NdefRecord createHandoverSelectRecord(boolean activating) {
    690         NdefMessage nestedMessage = new NdefMessage(createBluetoothAlternateCarrierRecord(activating));
    691         byte[] nestedPayload = nestedMessage.toByteArray();
    692 
    693         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
    694         payload.put((byte)0x12);  // connection handover v1.2
    695         payload.put(nestedPayload);
    696 
    697         byte[] payloadBytes = new byte[payload.position()];
    698         payload.position(0);
    699         payload.get(payloadBytes);
    700         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
    701                 payloadBytes);
    702 
    703     }
    704 
    705     NdefRecord createHandoverRequestRecord() {
    706         NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(),
    707                 createBluetoothAlternateCarrierRecord(false));
    708         byte[] nestedPayload = nestedMessage.toByteArray();
    709 
    710         ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
    711         payload.put((byte)0x12);  // connection handover v1.2
    712         payload.put(nestedMessage.toByteArray());
    713 
    714         byte[] payloadBytes = new byte[payload.position()];
    715         payload.position(0);
    716         payload.get(payloadBytes);
    717         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
    718                 payloadBytes);
    719     }
    720 
    721     /**
    722      * Return null if message is not a Handover Request,
    723      * return the Handover Select response if it is.
    724      */
    725     public NdefMessage tryHandoverRequest(NdefMessage m) {
    726         if (m == null) return null;
    727         if (mBluetoothAdapter == null) return null;
    728 
    729         if (DBG) Log.d(TAG, "tryHandoverRequest():" + m.toString());
    730 
    731         NdefRecord r = m.getRecords()[0];
    732         if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null;
    733         if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null;
    734 
    735         // we have a handover request, look for BT OOB record
    736         BluetoothHandoverData bluetoothData = null;
    737         for (NdefRecord oob : m.getRecords()) {
    738             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
    739                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
    740                 bluetoothData = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
    741                 break;
    742             }
    743         }
    744         if (bluetoothData == null) return null;
    745 
    746         boolean bluetoothActivating = false;
    747 
    748         synchronized(HandoverManager.this) {
    749             if (!mHandoverPowerManager.isBluetoothEnabled()) {
    750                 if (!mHandoverPowerManager.enableBluetooth()) {
    751                     return null;
    752                 }
    753                 bluetoothActivating = true;
    754             } else {
    755                 mHandoverPowerManager.resetTimer();
    756             }
    757 
    758             // Create the initial transfer object
    759             HandoverTransfer transfer = getOrCreateHandoverTransfer(
    760                     bluetoothData.device.getAddress(), true, true);
    761             transfer.updateNotification();
    762         }
    763 
    764         // BT OOB found, whitelist it for incoming OPP data
    765         whitelistOppDevice(bluetoothData.device);
    766 
    767         // return BT OOB record so they can perform handover
    768         return (createHandoverSelectMessage(bluetoothActivating));
    769     }
    770 
    771     void whitelistOppDevice(BluetoothDevice device) {
    772         if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
    773         Intent intent = new Intent(ACTION_WHITELIST_DEVICE);
    774         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    775         mContext.sendBroadcast(intent);
    776     }
    777 
    778     public boolean tryHandover(NdefMessage m) {
    779         if (m == null) return false;
    780         if (mBluetoothAdapter == null) return false;
    781 
    782         if (DBG) Log.d(TAG, "tryHandover(): " + m.toString());
    783 
    784         BluetoothHandoverData handover = parse(m);
    785         if (handover == null) return false;
    786         if (!handover.valid) return true;
    787 
    788         synchronized (HandoverManager.this) {
    789             if (mBluetoothAdapter == null ||
    790                     mBluetoothA2dp == null ||
    791                     mBluetoothHeadset == null) {
    792                 if (DBG) Log.d(TAG, "BT handover, but BT not available");
    793                 return true;
    794             }
    795             if (mBluetoothHeadsetHandover != null) {
    796                 if (DBG) Log.d(TAG, "BT handover already in progress, ignoring");
    797                 return true;
    798             }
    799             mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device,
    800                     handover.name, mHandoverPowerManager, mBluetoothA2dp, mBluetoothHeadset, this);
    801             mBluetoothHeadsetHandover.start();
    802         }
    803         return true;
    804     }
    805 
    806     // This starts sending an Uri over BT
    807     public void doHandoverUri(Uri[] uris, NdefMessage m) {
    808         if (mBluetoothAdapter == null) return;
    809 
    810         BluetoothHandoverData data = parse(m);
    811         if (data != null && data.valid) {
    812             // Register a new handover transfer object
    813             getOrCreateHandoverTransfer(data.device.getAddress(), false, true);
    814             BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device,
    815                 uris, mHandoverPowerManager, data.carrierActivating);
    816             handover.start();
    817         }
    818     }
    819 
    820     boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
    821         byte[] payload = handoverRec.getPayload();
    822         if (payload == null || payload.length <= 1) return false;
    823         // Skip version
    824         byte[] payloadNdef = new byte[payload.length - 1];
    825         System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1);
    826         NdefMessage msg;
    827         try {
    828             msg = new NdefMessage(payloadNdef);
    829         } catch (FormatException e) {
    830             return false;
    831         }
    832 
    833         for (NdefRecord alt : msg.getRecords()) {
    834             byte[] acPayload = alt.getPayload();
    835             if (acPayload != null) {
    836                 ByteBuffer buf = ByteBuffer.wrap(acPayload);
    837                 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits
    838                 int carrierRefLength = buf.get() & 0xFF;
    839                 if (carrierRefLength != carrierId.length) return false;
    840 
    841                 byte[] carrierRefId = new byte[carrierRefLength];
    842                 buf.get(carrierRefId);
    843                 if (Arrays.equals(carrierRefId, carrierId)) {
    844                     // Found match, returning whether power state is activating
    845                     return (cps == CARRIER_POWER_STATE_ACTIVATING);
    846                 }
    847             }
    848         }
    849 
    850         return true;
    851     }
    852 
    853     BluetoothHandoverData parseHandoverSelect(NdefMessage m) {
    854         // TODO we could parse this a lot more strictly; right now
    855         // we just search for a BT OOB record, and try to cross-reference
    856         // the carrier state inside the 'hs' payload.
    857         for (NdefRecord oob : m.getRecords()) {
    858             if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA &&
    859                     Arrays.equals(oob.getType(), TYPE_BT_OOB)) {
    860                 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload()));
    861                 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) {
    862                     data.carrierActivating = true;
    863                 }
    864                 return data;
    865             }
    866         }
    867 
    868         return null;
    869     }
    870 
    871     BluetoothHandoverData parse(NdefMessage m) {
    872         NdefRecord r = m.getRecords()[0];
    873         short tnf = r.getTnf();
    874         byte[] type = r.getType();
    875 
    876         // Check for BT OOB record
    877         if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) {
    878             return parseBtOob(ByteBuffer.wrap(r.getPayload()));
    879         }
    880 
    881         // Check for Handover Select, followed by a BT OOB record
    882         if (tnf == NdefRecord.TNF_WELL_KNOWN &&
    883                 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) {
    884             return parseHandoverSelect(m);
    885         }
    886 
    887         // Check for Nokia BT record, found on some Nokia BH-505 Headsets
    888         if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) {
    889             return parseNokia(ByteBuffer.wrap(r.getPayload()));
    890         }
    891 
    892         return null;
    893     }
    894 
    895     BluetoothHandoverData parseNokia(ByteBuffer payload) {
    896         BluetoothHandoverData result = new BluetoothHandoverData();
    897         result.valid = false;
    898 
    899         try {
    900             payload.position(1);
    901             byte[] address = new byte[6];
    902             payload.get(address);
    903             result.device = mBluetoothAdapter.getRemoteDevice(address);
    904             result.valid = true;
    905             payload.position(14);
    906             int nameLength = payload.get();
    907             byte[] nameBytes = new byte[nameLength];
    908             payload.get(nameBytes);
    909             result.name = new String(nameBytes, Charset.forName("UTF-8"));
    910         } catch (IllegalArgumentException e) {
    911             Log.i(TAG, "nokia: invalid BT address");
    912         } catch (BufferUnderflowException e) {
    913             Log.i(TAG, "nokia: payload shorter than expected");
    914         }
    915         if (result.valid && result.name == null) result.name = "";
    916         return result;
    917     }
    918 
    919     BluetoothHandoverData parseBtOob(ByteBuffer payload) {
    920         BluetoothHandoverData result = new BluetoothHandoverData();
    921         result.valid = false;
    922 
    923         try {
    924             payload.position(2);
    925             byte[] address = new byte[6];
    926             payload.get(address);
    927             // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
    928             // ByteBuffer.get(byte[]), so manually swap order
    929             for (int i = 0; i < 3; i++) {
    930                 byte temp = address[i];
    931                 address[i] = address[5 - i];
    932                 address[5 - i] = temp;
    933             }
    934             result.device = mBluetoothAdapter.getRemoteDevice(address);
    935             result.valid = true;
    936 
    937             while (payload.remaining() > 0) {
    938                 byte[] nameBytes;
    939                 int len = payload.get();
    940                 int type = payload.get();
    941                 switch (type) {
    942                     case 0x08:  // short local name
    943                         nameBytes = new byte[len - 1];
    944                         payload.get(nameBytes);
    945                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    946                         break;
    947                     case 0x09:  // long local name
    948                         if (result.name != null) break;  // prefer short name
    949                         nameBytes = new byte[len - 1];
    950                         payload.get(nameBytes);
    951                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    952                         break;
    953                     default:
    954                         payload.position(payload.position() + len - 1);
    955                         break;
    956                 }
    957             }
    958         } catch (IllegalArgumentException e) {
    959             Log.i(TAG, "BT OOB: invalid BT address");
    960         } catch (BufferUnderflowException e) {
    961             Log.i(TAG, "BT OOB: payload shorter than expected");
    962         }
    963         if (result.valid && result.name == null) result.name = "";
    964         return result;
    965     }
    966 
    967     static byte[] addressToReverseBytes(String address) {
    968         String[] split = address.split(":");
    969         byte[] result = new byte[split.length];
    970 
    971         for (int i = 0; i < split.length; i++) {
    972             // need to parse as int because parseByte() expects a signed byte
    973             result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16);
    974         }
    975 
    976         return result;
    977     }
    978 
    979     @Override
    980     public void onServiceConnected(int profile, BluetoothProfile proxy) {
    981         synchronized (HandoverManager.this) {
    982             switch (profile) {
    983                 case BluetoothProfile.HEADSET:
    984                     mBluetoothHeadset = (BluetoothHeadset) proxy;
    985                     break;
    986                 case BluetoothProfile.A2DP:
    987                     mBluetoothA2dp = (BluetoothA2dp) proxy;
    988                     break;
    989             }
    990         }
    991     }
    992 
    993     @Override
    994     public void onServiceDisconnected(int profile) {
    995         synchronized (HandoverManager.this) {
    996             switch (profile) {
    997                 case BluetoothProfile.HEADSET:
    998                     mBluetoothHeadset = null;
    999                     break;
   1000                 case BluetoothProfile.A2DP:
   1001                     mBluetoothA2dp = null;
   1002                     break;
   1003             }
   1004         }
   1005     }
   1006 
   1007     @Override
   1008     public void onBluetoothHeadsetHandoverComplete(boolean connected) {
   1009         synchronized (HandoverManager.this) {
   1010             mBluetoothHeadsetHandover = null;
   1011             mBluetoothHeadsetConnected = connected;
   1012         }
   1013     }
   1014 
   1015     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
   1016         @Override
   1017         public void onReceive(Context context, Intent intent) {
   1018             String action = intent.getAction();
   1019 
   1020             if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
   1021                 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
   1022                 if (state == BluetoothAdapter.STATE_OFF) {
   1023                     mHandoverPowerManager.stopMonitoring();
   1024                 }
   1025 
   1026                 return;
   1027             } else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) {
   1028                 String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS);
   1029                 HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, true,
   1030                         false);
   1031                 if (transfer != null) {
   1032                     transfer.cancel();
   1033                 }
   1034             } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) ||
   1035                     action.equals(ACTION_BT_OPP_TRANSFER_DONE)) {
   1036                 // Clean up old transfers no longer in progress
   1037                 cleanupTransfers();
   1038 
   1039                 int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1);
   1040                 int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1);
   1041                 String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS);
   1042 
   1043                 if (direction == -1 || id == -1 || sourceAddress == null) return;
   1044                 boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING);
   1045 
   1046                 HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, incoming,
   1047                         false);
   1048                 if (transfer == null) {
   1049                     // There is no transfer running for this source address; most likely
   1050                     // the transfer was cancelled. We need to tell BT OPP to stop transferring
   1051                     // in case this was an incoming transfer
   1052                     Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER");
   1053                     cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id);
   1054                     mContext.sendBroadcast(cancelIntent);
   1055                     return;
   1056                 }
   1057 
   1058                 if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) {
   1059                     int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS,
   1060                             HANDOVER_TRANSFER_STATUS_FAILURE);
   1061 
   1062                     if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) {
   1063                         String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI);
   1064                         String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE);
   1065                         Uri uri = Uri.parse(uriString);
   1066                         if (uri.getScheme() == null) {
   1067                             uri = Uri.fromFile(new File(uri.getPath()));
   1068                         }
   1069                         transfer.finishTransfer(true, uri, mimeType);
   1070                     } else {
   1071                         transfer.finishTransfer(false, null, null);
   1072                     }
   1073                 } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) {
   1074                     float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f);
   1075                     transfer.updateFileProgress(progress);
   1076                 }
   1077             }
   1078         }
   1079     };
   1080 
   1081 }
   1082