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.nfc.NfcAdapter;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.IBinder;
32import android.os.Message;
33import android.os.Messenger;
34import android.os.RemoteException;
35import android.util.Log;
36
37import com.android.nfc.R;
38
39public class PeripheralHandoverService extends Service implements BluetoothPeripheralHandover.Callback {
40    static final String TAG = "PeripheralHandoverService";
41    static final boolean DBG = true;
42
43    static final int MSG_PAUSE_POLLING = 0;
44
45    public static final String BUNDLE_TRANSFER = "transfer";
46    public static final String EXTRA_PERIPHERAL_DEVICE = "device";
47    public static final String EXTRA_PERIPHERAL_NAME = "headsetname";
48    public static final String EXTRA_PERIPHERAL_TRANSPORT = "transporttype";
49
50    // Amount of time to pause polling when connecting to peripherals
51    private static final int PAUSE_POLLING_TIMEOUT_MS = 35000;
52    private static final int PAUSE_DELAY_MILLIS = 300;
53
54    private static final Object sLock = new Object();
55
56    // Variables below only accessed on main thread
57    final Messenger mMessenger;
58
59    SoundPool mSoundPool;
60    int mSuccessSound;
61    int mStartId;
62
63    BluetoothAdapter mBluetoothAdapter;
64    NfcAdapter mNfcAdapter;
65    Handler mHandler;
66    BluetoothPeripheralHandover mBluetoothPeripheralHandover;
67    boolean mBluetoothHeadsetConnected;
68    boolean mBluetoothEnabledByNfc;
69
70    class MessageHandler extends Handler {
71        @Override
72        public void handleMessage(Message msg) {
73            switch (msg.what) {
74                case MSG_PAUSE_POLLING:
75                    mNfcAdapter.pausePolling(PAUSE_POLLING_TIMEOUT_MS);
76                    break;
77            }
78        }
79    }
80
81    final BroadcastReceiver mBluetoothStatusReceiver = new BroadcastReceiver() {
82        @Override
83        public void onReceive(Context context, Intent intent) {
84            String action = intent.getAction();
85            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
86                handleBluetoothStateChanged(intent);
87            }
88        }
89    };
90
91    public PeripheralHandoverService() {
92        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
93        mHandler = new MessageHandler();
94        mMessenger = new Messenger(mHandler);
95        mBluetoothHeadsetConnected = false;
96        mBluetoothEnabledByNfc = false;
97        mStartId = 0;
98    }
99
100    @Override
101    public int onStartCommand(Intent intent, int flags, int startId) {
102
103        synchronized (sLock) {
104            if (mStartId != 0) {
105                mStartId = startId;
106                // already running
107                return START_STICKY;
108            }
109            mStartId = startId;
110        }
111
112        if (intent == null) {
113            if (DBG) Log.e(TAG, "Intent is null, can't do peripheral handover.");
114            stopSelf(startId);
115            return START_NOT_STICKY;
116        }
117
118        if (doPeripheralHandover(intent.getExtras())) {
119            return START_STICKY;
120        } else {
121            stopSelf(startId);
122            return START_NOT_STICKY;
123        }
124    }
125
126    @Override
127    public void onCreate() {
128        super.onCreate();
129
130        mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
131        mSuccessSound = mSoundPool.load(this, R.raw.end, 1);
132        mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
133
134        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
135        registerReceiver(mBluetoothStatusReceiver, filter);
136    }
137
138    @Override
139    public void onDestroy() {
140        super.onDestroy();
141        if (mSoundPool != null) {
142            mSoundPool.release();
143        }
144        unregisterReceiver(mBluetoothStatusReceiver);
145    }
146
147    boolean doPeripheralHandover(Bundle msgData) {
148        if (mBluetoothPeripheralHandover != null) {
149            Log.d(TAG, "Ignoring pairing request, existing handover in progress.");
150            return true;
151        }
152
153        if (msgData == null) {
154            return false;
155        }
156
157        BluetoothDevice device = msgData.getParcelable(EXTRA_PERIPHERAL_DEVICE);
158        String name = msgData.getString(EXTRA_PERIPHERAL_NAME);
159        int transport = msgData.getInt(EXTRA_PERIPHERAL_TRANSPORT);
160
161        mBluetoothPeripheralHandover = new BluetoothPeripheralHandover(
162                this, device, name, transport, this);
163
164        if (transport == BluetoothDevice.TRANSPORT_LE) {
165            mHandler.sendMessageDelayed(
166                    mHandler.obtainMessage(MSG_PAUSE_POLLING), PAUSE_DELAY_MILLIS);
167        }
168        if (mBluetoothAdapter.isEnabled()) {
169            if (!mBluetoothPeripheralHandover.start()) {
170                mHandler.removeMessages(MSG_PAUSE_POLLING);
171                mNfcAdapter.resumePolling();
172            }
173        } else {
174            // Once BT is enabled, the headset pairing will be started
175            if (!enableBluetooth()) {
176                Log.e(TAG, "Error enabling Bluetooth.");
177                mBluetoothPeripheralHandover = null;
178                return false;
179            }
180        }
181
182        return true;
183    }
184
185    private void handleBluetoothStateChanged(Intent intent) {
186        int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
187                BluetoothAdapter.ERROR);
188        if (state == BluetoothAdapter.STATE_ON) {
189            // If there is a pending device pairing, start it
190            if (mBluetoothPeripheralHandover != null &&
191                    !mBluetoothPeripheralHandover.hasStarted()) {
192                if (!mBluetoothPeripheralHandover.start()) {
193                    mNfcAdapter.resumePolling();
194                }
195            }
196        } else if (state == BluetoothAdapter.STATE_OFF) {
197            mBluetoothEnabledByNfc = false;
198            mBluetoothHeadsetConnected = false;
199        }
200    }
201
202    @Override
203    public void onBluetoothPeripheralHandoverComplete(boolean connected) {
204        // Called on the main thread
205        int transport = mBluetoothPeripheralHandover.mTransport;
206        mBluetoothPeripheralHandover = null;
207        mBluetoothHeadsetConnected = connected;
208
209        // <hack> resume polling immediately if the connection failed,
210        // otherwise just wait for polling to come back up after the timeout
211        // This ensures we don't disconnect if the user left the volantis
212        // on the tag after pairing completed, which results in automatic
213        // disconnection </hack>
214        if (transport == BluetoothDevice.TRANSPORT_LE && !connected) {
215            if (mHandler.hasMessages(MSG_PAUSE_POLLING)) {
216                mHandler.removeMessages(MSG_PAUSE_POLLING);
217            }
218
219            // do this unconditionally as the polling could have been paused as we were removing
220            // the message in the handler. It's a no-op if polling is already enabled.
221            mNfcAdapter.resumePolling();
222        }
223        disableBluetoothIfNeeded();
224
225        synchronized (sLock) {
226            stopSelf(mStartId);
227            mStartId = 0;
228        }
229    }
230
231
232    boolean enableBluetooth() {
233        if (!mBluetoothAdapter.isEnabled()) {
234            mBluetoothEnabledByNfc = true;
235            return mBluetoothAdapter.enableNoAutoConnect();
236        }
237        return true;
238    }
239
240    void disableBluetoothIfNeeded() {
241        if (!mBluetoothEnabledByNfc) return;
242
243        if (!mBluetoothHeadsetConnected) {
244            mBluetoothAdapter.disable();
245            mBluetoothEnabledByNfc = false;
246        }
247    }
248
249    @Override
250    public IBinder onBind(Intent intent) {
251        return null;
252    }
253
254    @Override
255    public boolean onUnbind(Intent intent) {
256        // prevent any future callbacks to the client, no rebind call needed.
257        return false;
258    }
259}
260