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 android.app.Service;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothDevice;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.media.AudioManager;
27import android.media.SoundPool;
28import android.net.Uri;
29import android.nfc.NfcAdapter;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.Message;
34import android.os.Messenger;
35import android.os.RemoteException;
36import android.util.Log;
37import android.util.Pair;
38
39import com.android.nfc.R;
40
41import java.io.File;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.LinkedList;
45import java.util.Map;
46import java.util.Queue;
47
48public class HandoverService extends Service implements HandoverTransfer.Callback,
49        BluetoothPeripheralHandover.Callback {
50
51    static final String TAG = "HandoverService";
52    static final boolean DBG = true;
53
54    static final int MSG_REGISTER_CLIENT = 0;
55    static final int MSG_DEREGISTER_CLIENT = 1;
56    static final int MSG_START_INCOMING_TRANSFER = 2;
57    static final int MSG_START_OUTGOING_TRANSFER = 3;
58    static final int MSG_PERIPHERAL_HANDOVER = 4;
59    static final int MSG_PAUSE_POLLING = 5;
60
61
62    static final String BUNDLE_TRANSFER = "transfer";
63
64    static final String EXTRA_PERIPHERAL_DEVICE = "device";
65    static final String EXTRA_PERIPHERAL_NAME = "headsetname";
66    static final String EXTRA_PERIPHERAL_TRANSPORT = "transporttype";
67
68    public static final String ACTION_CANCEL_HANDOVER_TRANSFER =
69            "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER";
70
71    public static final String EXTRA_INCOMING =
72            "com.android.nfc.handover.extra.INCOMING";
73
74    public static final String ACTION_HANDOVER_STARTED =
75            "android.nfc.handover.intent.action.HANDOVER_STARTED";
76
77    public static final String ACTION_TRANSFER_PROGRESS =
78            "android.nfc.handover.intent.action.TRANSFER_PROGRESS";
79
80    public static final String ACTION_TRANSFER_DONE =
81            "android.nfc.handover.intent.action.TRANSFER_DONE";
82
83    public static final String EXTRA_TRANSFER_STATUS =
84            "android.nfc.handover.intent.extra.TRANSFER_STATUS";
85
86    public static final String EXTRA_TRANSFER_MIMETYPE =
87            "android.nfc.handover.intent.extra.TRANSFER_MIME_TYPE";
88
89    public static final String EXTRA_ADDRESS =
90            "android.nfc.handover.intent.extra.ADDRESS";
91
92    public static final String EXTRA_TRANSFER_DIRECTION =
93            "android.nfc.handover.intent.extra.TRANSFER_DIRECTION";
94
95    public static final String EXTRA_TRANSFER_ID =
96            "android.nfc.handover.intent.extra.TRANSFER_ID";
97
98    public static final String EXTRA_TRANSFER_PROGRESS =
99            "android.nfc.handover.intent.extra.TRANSFER_PROGRESS";
100
101    public static final String EXTRA_TRANSFER_URI =
102            "android.nfc.handover.intent.extra.TRANSFER_URI";
103
104    public static final String EXTRA_OBJECT_COUNT =
105            "android.nfc.handover.intent.extra.OBJECT_COUNT";
106
107    public static final String EXTRA_HANDOVER_DEVICE_TYPE =
108            "android.nfc.handover.intent.extra.HANDOVER_DEVICE_TYPE";
109
110    public static final int DIRECTION_INCOMING = 0;
111    public static final int DIRECTION_OUTGOING = 1;
112
113    public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0;
114    public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1;
115
116    // permission needed to be able to receive handover status requests
117    public static final String HANDOVER_STATUS_PERMISSION =
118            "android.permission.NFC_HANDOVER_STATUS";
119
120    // Amount of time to pause polling when connecting to peripherals
121    private static final int PAUSE_POLLING_TIMEOUT_MS = 35000;
122    public static final int PAUSE_DELAY_MILLIS = 300;
123
124    // Variables below only accessed on main thread
125    final Queue<BluetoothOppHandover> mPendingOutTransfers;
126    final HashMap<Pair<String, Boolean>, HandoverTransfer> mBluetoothTransfers;
127    final Messenger mMessenger;
128
129    SoundPool mSoundPool;
130    int mSuccessSound;
131
132    BluetoothAdapter mBluetoothAdapter;
133    NfcAdapter mNfcAdapter;
134    Messenger mClient;
135    Handler mHandler;
136    BluetoothPeripheralHandover mBluetoothPeripheralHandover;
137    boolean mBluetoothHeadsetConnected;
138    boolean mBluetoothEnabledByNfc;
139
140    private HandoverTransfer mWifiTransfer;
141
142    class MessageHandler extends Handler {
143        @Override
144        public void handleMessage(Message msg) {
145            switch (msg.what) {
146                case MSG_REGISTER_CLIENT:
147                    mClient = msg.replyTo;
148                    // Restore state from previous instance
149                    mBluetoothEnabledByNfc = msg.arg1 != 0;
150                    mBluetoothHeadsetConnected = msg.arg2 != 0;
151                    break;
152                case MSG_DEREGISTER_CLIENT:
153                    mClient = null;
154                    break;
155                case MSG_START_INCOMING_TRANSFER:
156                    doIncomingTransfer(msg);
157                    break;
158                case MSG_START_OUTGOING_TRANSFER:
159                    doOutgoingTransfer(msg);
160                    break;
161                case MSG_PERIPHERAL_HANDOVER:
162                    doPeripheralHandover(msg);
163                    break;
164                case MSG_PAUSE_POLLING:
165                    mNfcAdapter.pausePolling(PAUSE_POLLING_TIMEOUT_MS);
166                    break;
167            }
168        }
169
170    }
171
172    final BroadcastReceiver mHandoverStatusReceiver = new BroadcastReceiver() {
173        @Override
174        public void onReceive(Context context, Intent intent) {
175            String action = intent.getAction();
176            int deviceType = intent.getIntExtra(EXTRA_HANDOVER_DEVICE_TYPE,
177                    HandoverTransfer.DEVICE_TYPE_BLUETOOTH);
178
179            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
180                handleBluetoothStateChanged(intent);
181            } else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) {
182                handleCancelTransfer(intent, deviceType);
183            } else if (action.equals(ACTION_TRANSFER_PROGRESS) ||
184                    action.equals(ACTION_TRANSFER_DONE) ||
185                    action.equals(ACTION_HANDOVER_STARTED)) {
186                handleTransferEvent(intent, deviceType);
187            }
188        }
189    };
190
191    public HandoverService() {
192        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
193        mPendingOutTransfers = new LinkedList<BluetoothOppHandover>();
194        mBluetoothTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>();
195        mHandler = new MessageHandler();
196        mMessenger = new Messenger(mHandler);
197        mBluetoothHeadsetConnected = false;
198        mBluetoothEnabledByNfc = false;
199    }
200
201    @Override
202    public void onCreate() {
203        super.onCreate();
204
205        mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
206        mSuccessSound = mSoundPool.load(this, R.raw.end, 1);
207        mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
208
209        IntentFilter filter = new IntentFilter(ACTION_TRANSFER_DONE);
210        filter.addAction(ACTION_TRANSFER_PROGRESS);
211        filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER);
212        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
213        filter.addAction(ACTION_HANDOVER_STARTED);
214        registerReceiver(mHandoverStatusReceiver, filter, HANDOVER_STATUS_PERMISSION, mHandler);
215    }
216
217    @Override
218    public void onDestroy() {
219        super.onDestroy();
220        if (mSoundPool != null) {
221            mSoundPool.release();
222        }
223        unregisterReceiver(mHandoverStatusReceiver);
224    }
225
226    void doOutgoingTransfer(Message msg) {
227        Bundle msgData = msg.getData();
228
229        msgData.setClassLoader(getClassLoader());
230        PendingHandoverTransfer pendingTransfer = msgData.getParcelable(BUNDLE_TRANSFER);
231        createHandoverTransfer(pendingTransfer);
232
233        if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) {
234            // Create the actual bluetooth transfer
235
236            BluetoothOppHandover handover = new BluetoothOppHandover(HandoverService.this,
237                    pendingTransfer.remoteDevice, pendingTransfer.uris,
238                    pendingTransfer.remoteActivating);
239            if (mBluetoothAdapter.isEnabled()) {
240                // Start the transfer
241                handover.start();
242            } else {
243                if (!enableBluetooth()) {
244                    Log.e(TAG, "Error enabling Bluetooth.");
245                    notifyClientTransferComplete(pendingTransfer.id);
246                    return;
247                }
248                if (DBG) Log.d(TAG, "Queueing out transfer " + Integer.toString(pendingTransfer.id));
249                mPendingOutTransfers.add(handover);
250                // Queue the transfer and enable Bluetooth - when it is enabled
251                // the transfer will be started.
252            }
253        }
254    }
255
256    void doIncomingTransfer(Message msg) {
257        Bundle msgData = msg.getData();
258
259        msgData.setClassLoader(getClassLoader());
260        PendingHandoverTransfer pendingTransfer = msgData.getParcelable(BUNDLE_TRANSFER);
261        if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH &&
262                !mBluetoothAdapter.isEnabled() && !enableBluetooth()) {
263            Log.e(TAG, "Error enabling Bluetooth.");
264            notifyClientTransferComplete(pendingTransfer.id);
265            return;
266        }
267        createHandoverTransfer(pendingTransfer);
268        // Remote device will connect and finish the transfer
269    }
270
271    void doPeripheralHandover(Message msg) {
272        Bundle msgData = msg.getData();
273        BluetoothDevice device = msgData.getParcelable(EXTRA_PERIPHERAL_DEVICE);
274        String name = msgData.getString(EXTRA_PERIPHERAL_NAME);
275        int transport = msgData.getInt(EXTRA_PERIPHERAL_TRANSPORT);
276        if (mBluetoothPeripheralHandover != null) {
277           Log.d(TAG, "Ignoring pairing request, existing handover in progress.");
278           return;
279        }
280        mBluetoothPeripheralHandover = new BluetoothPeripheralHandover(HandoverService.this,
281                device, name, transport, HandoverService.this);
282        // TODO: figure out a way to disable polling without deactivating current target
283        if (transport == BluetoothDevice.TRANSPORT_LE) {
284            mHandler.sendMessageDelayed(
285                    mHandler.obtainMessage(MSG_PAUSE_POLLING), PAUSE_DELAY_MILLIS);
286        }
287        if (mBluetoothAdapter.isEnabled()) {
288            if (!mBluetoothPeripheralHandover.start()) {
289                mNfcAdapter.resumePolling();
290            }
291        } else {
292            // Once BT is enabled, the headset pairing will be started
293
294            if (!enableBluetooth()) {
295                Log.e(TAG, "Error enabling Bluetooth.");
296                mBluetoothPeripheralHandover = null;
297            }
298        }
299    }
300
301    void startPendingTransfers() {
302        while (!mPendingOutTransfers.isEmpty()) {
303             BluetoothOppHandover handover = mPendingOutTransfers.remove();
304             handover.start();
305        }
306    }
307
308    boolean enableBluetooth() {
309        if (!mBluetoothAdapter.isEnabled()) {
310            mBluetoothEnabledByNfc = true;
311            return mBluetoothAdapter.enableNoAutoConnect();
312        }
313        return true;
314    }
315
316    void disableBluetoothIfNeeded() {
317        if (!mBluetoothEnabledByNfc) return;
318
319        if (mBluetoothTransfers.size() == 0 && !mBluetoothHeadsetConnected) {
320            mBluetoothAdapter.disable();
321            mBluetoothEnabledByNfc = false;
322        }
323    }
324
325    void createHandoverTransfer(PendingHandoverTransfer pendingTransfer) {
326        HandoverTransfer transfer;
327        String macAddress;
328
329        if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) {
330            macAddress = pendingTransfer.remoteDevice.getAddress();
331            transfer = maybeCreateHandoverTransfer(macAddress,
332                    pendingTransfer.incoming, pendingTransfer);
333        } else {
334            Log.e(TAG, "Invalid device type [" + pendingTransfer.deviceType + "] received.");
335            return;
336        }
337
338        if (transfer != null) {
339            transfer.updateNotification();
340        }
341    }
342
343    HandoverTransfer maybeCreateHandoverTransfer(String address, boolean incoming,
344                                                 PendingHandoverTransfer pendingTransfer) {
345        HandoverTransfer transfer;
346        Pair<String, Boolean> key = new Pair<String, Boolean>(address, incoming);
347
348        if (mBluetoothTransfers.containsKey(key)) {
349            transfer = mBluetoothTransfers.get(key);
350            if (!transfer.isRunning()) {
351                mBluetoothTransfers.remove(key); // new one created below
352            } else {
353                // There is already a transfer running to this
354                // device - it will automatically get combined
355                // with the existing transfer.
356                notifyClientTransferComplete(pendingTransfer.id);
357                return null;
358            }
359        } else {
360            transfer = new HandoverTransfer(this, this, pendingTransfer);
361        }
362
363        mBluetoothTransfers.put(key, transfer);
364        return transfer;
365    }
366
367
368    HandoverTransfer findHandoverTransfer(String macAddress, boolean incoming) {
369        Pair<String, Boolean> key = new Pair<String, Boolean>(macAddress, incoming);
370        if (mBluetoothTransfers.containsKey(key)) {
371            HandoverTransfer transfer = mBluetoothTransfers.get(key);
372            if (transfer.isRunning()) {
373                return transfer;
374            }
375        }
376
377        return null;
378    }
379
380    @Override
381    public IBinder onBind(Intent intent) {
382       return mMessenger.getBinder();
383    }
384
385    private void handleTransferEvent(Intent intent, int deviceType) {
386        String action = intent.getAction();
387        int direction = intent.getIntExtra(EXTRA_TRANSFER_DIRECTION, -1);
388        int id = intent.getIntExtra(EXTRA_TRANSFER_ID, -1);
389        if (action.equals(ACTION_HANDOVER_STARTED)) {
390            // This is always for incoming transfers
391            direction = DIRECTION_INCOMING;
392        }
393        String sourceAddress = intent.getStringExtra(EXTRA_ADDRESS);
394
395        if (direction == -1 || sourceAddress == null) return;
396        boolean incoming = (direction == DIRECTION_INCOMING);
397
398        HandoverTransfer transfer =
399                findHandoverTransfer(sourceAddress, incoming);
400        if (transfer == null) {
401            // There is no transfer running for this source address; most likely
402            // the transfer was cancelled. We need to tell BT OPP to stop transferring.
403            if (id != -1) {
404                if (deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) {
405                    if (DBG) Log.d(TAG, "Didn't find transfer, stopping");
406                    Intent cancelIntent = new Intent(
407                            "android.btopp.intent.action.STOP_HANDOVER_TRANSFER");
408                    cancelIntent.putExtra(EXTRA_TRANSFER_ID, id);
409                    sendBroadcast(cancelIntent);
410                }
411            }
412            return;
413        }
414
415        transfer.setBluetoothTransferId(id);
416
417        if (action.equals(ACTION_TRANSFER_DONE)) {
418            int handoverStatus = intent.getIntExtra(EXTRA_TRANSFER_STATUS,
419                    HANDOVER_TRANSFER_STATUS_FAILURE);
420            if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) {
421                String uriString = intent.getStringExtra(EXTRA_TRANSFER_URI);
422                String mimeType = intent.getStringExtra(EXTRA_TRANSFER_MIMETYPE);
423                Uri uri = Uri.parse(uriString);
424                if (uri != null && uri.getScheme() == null) {
425                    uri = Uri.fromFile(new File(uri.getPath()));
426                }
427                transfer.finishTransfer(true, uri, mimeType);
428            } else {
429                transfer.finishTransfer(false, null, null);
430            }
431        } else if (action.equals(ACTION_TRANSFER_PROGRESS)) {
432            float progress = intent.getFloatExtra(EXTRA_TRANSFER_PROGRESS, 0.0f);
433            transfer.updateFileProgress(progress);
434        } else if (action.equals(ACTION_HANDOVER_STARTED)) {
435            int count = intent.getIntExtra(EXTRA_OBJECT_COUNT, 0);
436            if (count > 0) {
437                transfer.setObjectCount(count);
438            }
439        }
440    }
441
442    private void handleCancelTransfer(Intent intent, int deviceType) {
443        String sourceAddress = intent.getStringExtra(EXTRA_ADDRESS);
444        int direction = intent.getIntExtra(EXTRA_INCOMING, -1);
445
446        if (direction == -1) {
447            return;
448        }
449
450        boolean incoming = direction == DIRECTION_INCOMING;
451        HandoverTransfer transfer = findHandoverTransfer(sourceAddress, incoming);
452
453        if (transfer != null) {
454            if (DBG) Log.d(TAG, "Cancelling transfer " + Integer.toString(transfer.mTransferId));
455            transfer.cancel();
456        }
457    }
458
459    private void handleBluetoothStateChanged(Intent intent) {
460        int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
461                BluetoothAdapter.ERROR);
462        if (state == BluetoothAdapter.STATE_ON) {
463            // If there is a pending device pairing, start it
464            if (mBluetoothPeripheralHandover != null &&
465                    !mBluetoothPeripheralHandover.hasStarted()) {
466                if (!mBluetoothPeripheralHandover.start()) {
467                    mNfcAdapter.resumePolling();
468                }
469            }
470
471            // Start any pending file transfers
472            startPendingTransfers();
473        } else if (state == BluetoothAdapter.STATE_OFF) {
474            mBluetoothEnabledByNfc = false;
475            mBluetoothHeadsetConnected = false;
476        }
477    }
478
479    void notifyClientTransferComplete(int transferId) {
480        if (mClient != null) {
481            Message msg = Message.obtain(null, HandoverManager.MSG_HANDOVER_COMPLETE);
482            msg.arg1 = transferId;
483            try {
484                mClient.send(msg);
485            } catch (RemoteException e) {
486                // Ignore
487            }
488        }
489    }
490
491    @Override
492    public boolean onUnbind(Intent intent) {
493        // prevent any future callbacks to the client, no rebind call needed.
494        mClient = null;
495        return false;
496    }
497
498    @Override
499    public void onTransferComplete(HandoverTransfer transfer, boolean success) {
500        // Called on the main thread
501
502        // First, remove the transfer from our list
503        synchronized (this) {
504            if (mWifiTransfer == transfer) {
505                mWifiTransfer = null;
506            }
507        }
508
509        if (mWifiTransfer == null) {
510            Iterator it = mBluetoothTransfers.entrySet().iterator();
511            while (it.hasNext()) {
512                Map.Entry hashPair = (Map.Entry)it.next();
513                HandoverTransfer transferEntry = (HandoverTransfer) hashPair.getValue();
514                if (transferEntry == transfer) {
515                    it.remove();
516                }
517            }
518        }
519
520        // Notify any clients of the service
521        notifyClientTransferComplete(transfer.getTransferId());
522
523        // Play success sound
524        if (success) {
525            mSoundPool.play(mSuccessSound, 1.0f, 1.0f, 0, 0, 1.0f);
526        } else {
527            if (DBG) Log.d(TAG, "Transfer failed, final state: " +
528                    Integer.toString(transfer.mState));
529        }
530        disableBluetoothIfNeeded();
531    }
532
533    @Override
534    public void onBluetoothPeripheralHandoverComplete(boolean connected) {
535        // Called on the main thread
536        int transport = mBluetoothPeripheralHandover.mTransport;
537        mBluetoothPeripheralHandover = null;
538        mBluetoothHeadsetConnected = connected;
539
540        // <hack> resume polling immediately if the connection failed,
541        // otherwise just wait for polling to come back up after the timeout
542        // This ensures we don't disconnect if the user left the volantis
543        // on the tag after pairing completed, which results in automatic
544        // disconnection </hack>
545        if (transport == BluetoothDevice.TRANSPORT_LE && !connected) {
546            if (mHandler.hasMessages(MSG_PAUSE_POLLING)) {
547                mHandler.removeMessages(MSG_PAUSE_POLLING);
548            }
549
550            // do this unconditionally as the polling could have been paused as we were removing
551            // the message in the handler. It's a no-op if polling is already enabled.
552            mNfcAdapter.resumePolling();
553        }
554
555        if (mClient != null) {
556            Message msg = Message.obtain(null,
557                    connected ? HandoverManager.MSG_HEADSET_CONNECTED
558                              : HandoverManager.MSG_HEADSET_NOT_CONNECTED);
559            msg.arg1 = mBluetoothEnabledByNfc ? 1 : 0;
560            try {
561                mClient.send(msg);
562            } catch (RemoteException e) {
563                // Ignore
564            }
565        }
566        disableBluetoothIfNeeded();
567    }
568}
569