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
17package com.android.nfc.handover;
18
19import java.io.File;
20import java.nio.BufferUnderflowException;
21import java.nio.ByteBuffer;
22import java.nio.charset.Charset;
23import java.text.SimpleDateFormat;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Date;
27import java.util.HashMap;
28import java.util.Iterator;
29import java.util.Map;
30import java.util.Random;
31
32import android.app.Notification;
33import android.app.NotificationManager;
34import android.app.PendingIntent;
35import android.app.Notification.Builder;
36import android.bluetooth.BluetoothA2dp;
37import android.bluetooth.BluetoothAdapter;
38import android.bluetooth.BluetoothDevice;
39import android.bluetooth.BluetoothHeadset;
40import android.bluetooth.BluetoothProfile;
41import android.content.BroadcastReceiver;
42import android.content.ContentResolver;
43import android.content.Context;
44import android.content.Intent;
45import android.content.IntentFilter;
46import android.media.MediaScannerConnection;
47import android.net.Uri;
48import android.nfc.FormatException;
49import android.nfc.NdefMessage;
50import android.nfc.NdefRecord;
51import android.os.Environment;
52import android.os.Handler;
53import android.os.Message;
54import android.os.SystemClock;
55import android.util.Log;
56import android.util.Pair;
57
58import com.android.nfc.NfcService;
59import com.android.nfc.R;
60
61
62/**
63 * Manages handover of NFC to other technologies.
64 */
65public 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