/**
 * **************************************************************************
 * Copyright © 2014 Kent Displays, Inc.
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * **************************************************************************
 */

package com.improvelectronics.sync.android;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.improvelectronics.sync.Config;
import com.improvelectronics.sync.hid.HIDMessage;
import com.improvelectronics.sync.hid.HIDSetReport;
import com.improvelectronics.sync.hid.HIDUtilities;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

/**
 * This service connects to the Boogie Board Sync devices and communicates with the Sync using a custom implementation of the HID protocol. All of the
 * connections are done automatically since this service is always running while Bluetooth is enabled. A client of this service can add a listener,
 * that implements {@link com.improvelectronics.sync.android.SyncStreamingListener #SyncStreamingListener},
 * to listen for changes of the streaming service as well as send commands to the connected Boogie Board Sync.
 * </p>
 * This service also handles all the notifications that are displayed when the Sync connects and disconnects. It is necessary to display these
 * notifications since the Android OS does not show a current Bluetooth connection with the Bluetooth icon in the status bar. Class also handles
 * the case when the user has outdated firmware and will direct them to a site with instructions on how to update the firmware.
 */
public class SyncStreamingService extends Service {

    private static final UUID LISTEN_UUID = UUID.fromString("d6a56f81-88f8-11e3-baa8-0800200c9a66");
    private static final UUID CONNECT_UUID = UUID.fromString("d6a56f80-88f8-11e3-baa8-0800200c9a66");
    private static final String TAG = SyncStreamingService.class.getSimpleName();
    private static final boolean DEBUG = Config.DEBUG;
    private BluetoothAdapter mBluetoothAdapter;
    private final IBinder mBinder = new SyncStreamingBinder();
    private List<SyncStreamingListener> mListeners;
    private int mState, mMode;
    private ConnectThread mConnectThread;
    private ConnectedThread mConnectedThread;
    private AcceptThread mAcceptThread;
    private List<BluetoothDevice> mPairedDevices;
    private List<SyncPath> mPaths;

    // Used for updating the local time of the Sync.
    private static final int YEAR_OFFSET = 1980;

    // Communication with background thread.
    private MessageHandler mMessageHandler;
    private static final int MESSAGE_DATA = 13;
    private static final int MESSAGE_CONNECTED = 14;
    private static final int MESSAGE_CONNECTION_BROKEN = 15;
    private static final int MESSAGE_BLUETOOTH_HACK = 16;

    /**
     * The Sync streaming service is in connected state.
     */
    public static final int STATE_CONNECTED = 0;

    /**
     * The Sync streaming service is in connecting state.
     */
    public static final int STATE_CONNECTING = 1;

    /**
     * The Sync streaming service is in disconnected state.
     */
    public static final int STATE_DISCONNECTED = 2;

    /**
     * The Sync streaming service is in listening state.
     */
    public static final int STATE_LISTENING = 4;

    /**
     * This mode tells the Sync to not report any information and be silent to the client. This greatly saves battery life of the Sync and the Android
     * device.
     */
    public static final int MODE_NONE = 1;

    /**
     * This mode tells the Sync to report every button push and path to the client.
     */
    public static final int MODE_CAPTURE = 4;

    /**
     * This mode tells the Sync to only inform the client when it has saved a file.
     */
    public static final int MODE_FILE = 5;

    private static final String ACTION_BASE = "com.improvelectronics.sync.android.SyncStreamingService.action";

    /**
     * Broadcast Action: Button was pushed on the Sync.
     */
    public static final String ACTION_BUTTON_PUSHED = ACTION_BASE + ".BUTTON_PUSHED";

    /**
     * Broadcast Action: The state of the Sync streaming service changed.
     */
    public static final String ACTION_STATE_CHANGED = ACTION_BASE + ".STATE_CHANGED";

    /**
     * Used as an int extra field in {@link #ACTION_BUTTON_PUSHED} intents for button push from the Sync.
     */
    public static final String EXTRA_BUTTON_PUSHED = "EXTRA_BUTTON_PUSHED";

    /**
     * Used as an int extra field in {@link #ACTION_STATE_CHANGED} intents for current state.
     */
    public static final String EXTRA_STATE = "EXTRA_STATE";

    /**
     * Used as an int extra field in {@link #ACTION_STATE_CHANGED} intents for previous state.
     */
    public static final String EXTRA_PREVIOUS_STATE = "PREVIOUS_STATE";

    /**
     * Used as an BluetoothDevice extra field in {@link #ACTION_STATE_CHANGED} intents for when streaming service reports a
     * connected state.
     */
    public static final String EXTRA_DEVICE = "EXTRA_DEVICE";

    /**
     * Used as an int extra for when the save button is pushed.
     */
    public static final int SAVE_BUTTON = 13;

    @Override
    public void onCreate() {
        super.onCreate();
        if (DEBUG) Log.d(TAG, "onCreate");

        // Set the default properties.
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mMessageHandler = new MessageHandler(Looper.getMainLooper());
        mPairedDevices = new ArrayList<BluetoothDevice>();
        mPaths = new ArrayList<SyncPath>();
        mListeners = new ArrayList<SyncStreamingListener>();
        mState = STATE_DISCONNECTED;
        mMode = MODE_NONE;
        setupIntentFilter();

        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            // Bluetooth adapter isn't available.  The client of the service is supposed to
            // verify that it is available and activate before invoking this service.
            Log.e(TAG, "stopping sync streaming service, device does not have Bluetooth or Bluetooth is turned off");
            stopSelf();
        } else {
            updatePairedDevices();
            start();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return Service.START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (DEBUG) Log.d(TAG, "onDestroy");

        // Stop all running threads.
        stop();

        // Clean up receivers.
        unregisterReceiver(mMessageReceiver);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private void broadcastStateChange(int state, int previousState) {
        Intent intent = new Intent(ACTION_STATE_CHANGED);
        intent.putExtra(EXTRA_STATE, state);
        intent.putExtra(EXTRA_PREVIOUS_STATE, previousState);
        if (mPairedDevices.size() > 0) {
            intent.putExtra(EXTRA_DEVICE, mPairedDevices.get(0));
        }
        sendBroadcast(intent);
    }

    private void broadcastButtonPush(int button) {
        Intent intent = new Intent(ACTION_BUTTON_PUSHED);
        intent.putExtra(EXTRA_BUTTON_PUSHED, button);
        sendBroadcast(intent);
    }

    /**
     * Returns the current state of the Sync streaming service.
     *
     * @return state
     */
    public int getState() {
        return mState;
    }

    /**
     * Returns the currently connected {@link BluetoothDevice}.
     *
     * @return device that is connected, returns null if there is no device connected
     */
    public BluetoothDevice getConnectedDevice() {
        if (mState != STATE_CONNECTED) return null;
        else return mPairedDevices.get(0);
    }

    /**
     * Start the streaming service. Check to see if we have paired devices and connect if necessary.
     */
    private synchronized void start() {
        if (DEBUG) Log.d(TAG, "start");

        if (mPairedDevices.size() > 0) {
            // Start the thread to listen on a BluetoothServerSocket
            if (mAcceptThread == null) {
                mAcceptThread = new AcceptThread();
                mAcceptThread.start();
            }

            // Only change state to listening if we are disconnected.
            if (mState == STATE_DISCONNECTED) updateDeviceState(STATE_LISTENING);

            if (mState != STATE_CONNECTED && mState != STATE_CONNECTING) {
                connect(mPairedDevices.get(0));
            }
        } else {
            stop();
        }
    }

    /**
     * Start the ConnectThread to initiate a connection to a remote device.
     *
     * @param device The BluetoothDevice to connect
     */
    private synchronized void connect(BluetoothDevice device) {
        if (DEBUG) Log.d(TAG, "connect to: " + device);

        // Cancel any thread attempting to make a connection
        if (mState == STATE_CONNECTING) {
            if (mConnectThread != null) {
                mConnectThread.cancel();
                mConnectThread = null;
            }
        }

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        // Start the thread to connect with the given device
        mConnectThread = new ConnectThread(device);
        mConnectThread.start();
        updateDeviceState(STATE_CONNECTING);
    }

    /**
     * Start the ConnectedThread to begin managing a Bluetooth connection
     *
     * @param socket The BluetoothSocket on which the connection was made
     */
    private synchronized void connected(BluetoothSocket socket) {
        if (DEBUG) Log.d(TAG, "connected");

        // Cancel the thread that completed the connection.
        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        // Cancel any thread currently running a connection.
        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        // Start listening thread if there is no one already running.
        if (mAcceptThread == null) {
            mAcceptThread = new AcceptThread();
            mAcceptThread.start();
        }

        // Start the thread to manage the connection and perform transmissions.
        mConnectedThread = new ConnectedThread(socket);
        mConnectedThread.start();

        startBluetoothHack();

        updateDeviceState(STATE_CONNECTED);
    }

    /**
     * Stop all threads.
     */
    private synchronized void stop() {
        if (DEBUG) Log.d(TAG, "stop");

        stopBluetoothHack();

        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        if (mAcceptThread != null) {
            mAcceptThread.cancel();
            mAcceptThread = null;
        }

        updateDeviceState(STATE_DISCONNECTED);
    }

    /**
     * Write to the ConnectedThread in an unsynchronized manner
     *
     * @param out The bytes to write
     * @see ConnectedThread#write(byte[])
     */
    private boolean write(byte[] out) {
        // Create temporary object
        ConnectedThread r;
        // Synchronize a copy of the ConnectedThread
        synchronized (this) {
            if (mState != STATE_CONNECTED) return false;
            r = mConnectedThread;
        }
        // Perform the write unsynchronized
        r.write(out);
        return true;
    }

    /**
     * Erases the Boogie Board Sync's screen.
     *
     * @return an immediate check if the message could be sent.
     */
    public boolean eraseSync() {
        if (mState != STATE_CONNECTED) return false;

        if (DEBUG) Log.d(TAG, "writing message to erase Boogie Board Sync's screen");

        // Clean up paths.
        mPaths.clear();

        // Create the HID message to be sent to the Sync to erase the screen.
        byte ERASE_MODE = 0x01;
        HIDSetReport setReport = new HIDSetReport(HIDSetReport.TYPE_FEATURE, HIDSetReport.ID_OPERATION_REQUEST, new byte[]{ERASE_MODE});

        return write(setReport.getPacketBytes());
    }

    /**
     * Updates the Boogie Board Sync's local time with the time of the device currently connected to it.
     *
     * @return an immediate check if the message could be sent.
     */
    private boolean updateSyncTimeWithLocalTime() {
        if (mState != STATE_CONNECTED) return false;

        // Construct the byte array for the time.
        Calendar calendar = Calendar.getInstance();
        int second = calendar.get(Calendar.SECOND) / 2;
        int minute = calendar.get(Calendar.MINUTE);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int month = calendar.get(Calendar.MONTH) + 1;
        int year = calendar.get(Calendar.YEAR) - YEAR_OFFSET;

        byte byte1 = (byte) ((minute << 5) | second);
        byte byte2 = (byte) ((hour << 3) | (minute >> 3));
        byte byte3 = (byte) ((month << 5) | day);
        byte byte4 = (byte) ((year << 1) | (month >> 3));

        // Create the HID message to be sent to the Sync to set the time.
        HIDSetReport setReport = new HIDSetReport(HIDSetReport.TYPE_FEATURE, HIDSetReport.ID_DATE, new byte[]{byte1, byte2, byte3,
                byte4});
        if (DEBUG) Log.d(TAG, "writing message to update Boogie Board Sync's time");
        return write(setReport.getPacketBytes());
    }

    /**
     * Sets the Boogie Board Sync into the specified mode.
     *
     * @param mode to put the Boogie Board Sync in.
     * @return an immediate check if the message could be sent.
     */
    public boolean setSyncMode(int mode) {
        // Check to see if a valid mode was sent.
        if (mMode == mode || mode < MODE_NONE || mode > MODE_FILE || mState != STATE_CONNECTED)
            return false;

        // Create the HID message to be sent to the Sync to change its mode.
        HIDSetReport setReport = new HIDSetReport(HIDSetReport.TYPE_FEATURE, HIDSetReport.ID_MODE, new byte[]{(byte) mode});
        if (DEBUG) Log.d(TAG, "writing message to set Boogie Board Sync into different mode");
        if (write(setReport.getPacketBytes())) {
            mMode = mode;
            return true;
        } else {
            return false;
        }
    }

    public List<BluetoothDevice> getPairedDevices() {
        return mPairedDevices;
    }

    /**
     * Returns a list of paths that the Sync currently have drawn on it.
     *
     * @return paths
     */
    public List<SyncPath> getPaths() {
        return mPaths;
    }

    /**
     * Tells the Boogie Board Sync what device is currently connected to it.
     *
     * @return an immediate check if the message could be sent.
     */
    private boolean informSyncOfDevice() {
        if (mState != STATE_CONNECTED) return false;

        // Create the HID message to be sent to the Sync to tell the Sync what device this is.
        byte ANDROID_DEVICE = 8;
        HIDSetReport setReport = new HIDSetReport(HIDSetReport.TYPE_FEATURE, HIDSetReport.ID_DEVICE, new byte[]{ANDROID_DEVICE, 0x00,
                0x00, 0x00});
        if (DEBUG) Log.d(TAG, "writing message to inform Boogie Board Sync what device we are");
        return write(setReport.getPacketBytes());
    }

    private void updatePairedDevices() {
        Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
        if (pairedDevices == null || pairedDevices.size() == 0) return;

        if (DEBUG) Log.d(TAG, "searching for paired Syncs");

        mPairedDevices.clear();
        for (BluetoothDevice device : pairedDevices) {
            if (device.getName() != null && device.getName().equals("Sync")) {
                if (DEBUG) Log.d(TAG, "found a Boogie Board Sync");
                mPairedDevices.add(device);
            }
        }
    }

    /**
     * Adds a listener to the Sync streaming service. Listener is used for state changes and asynchronous callbacks from streaming commands.
     * Remember to remove
     * the listener with {@link #removeListener(SyncStreamingListener)} when finished.
     *
     * @param listener Class that implements SyncStreamingListener for asynchronous callbacks.
     * @return false indicates listener has already been added
     */
    public boolean addListener(SyncStreamingListener listener) {
        if (mListeners.contains(listener)) return false;
        else mListeners.add(listener);
        return true;
    }

    /**
     * Removes a listener that was previously added with {@link #addListener(SyncStreamingListener)}.
     *
     * @param listener Class that implements SyncStreamingListener for asynchronous callbacks.
     * @return false indicates listener was not originally added
     */
    public boolean removeListener(SyncStreamingListener listener) {
        if (!mListeners.contains(listener)) return false;
        else mListeners.remove(listener);
        return true;
    }

    private void setupIntentFilter() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
        intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        registerReceiver(mMessageReceiver, intentFilter);
    }

    public class SyncStreamingBinder extends Binder {
        public SyncStreamingService getService() {
            // Return this instance of LocalService so clients can call public methods.
            return SyncStreamingService.this;
        }
    }

    private void updateDeviceState(int newState) {
        if (newState == mState) return;
        if (DEBUG) Log.d(TAG, "device state changed from " + mState + " to " + newState);

        int oldState = mState;
        mState = newState;

        // Clean up objects when there is a disconnection.
        if (newState == STATE_DISCONNECTED) {
            // Reset the mode of the Boogie Board Sync.
            mMode = MODE_NONE;
            mPaths.clear();
        } else if (newState == STATE_CONNECTED) {
            setSyncMode(MODE_FILE);
            updateSyncTimeWithLocalTime();
            informSyncOfDevice();
        }

        broadcastStateChange(mState, oldState);

        for (SyncStreamingListener listener : mListeners) {
            listener.onStreamingStateChange(oldState, newState);
        }
    }

    private class MessageHandler extends Handler {

        public MessageHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message message) {
            // Parse the message that was returned from the background thread.
            if (message.what == MESSAGE_DATA) {
                byte[] buffer = (byte[]) message.obj;
                int numBytes = message.arg1;

                List<HIDMessage> hidMessages = HIDUtilities.parseBuffer(buffer, numBytes);

                if (hidMessages == null) return;

                // Received a capture report.
                for (HIDMessage hidMessage : hidMessages) {
                    if (hidMessage == null) {
                        Log.e(TAG, "was unable to parse the returned message from the Sync");
                    } else if (hidMessage instanceof SyncCaptureReport) {
                        SyncCaptureReport captureReport = (SyncCaptureReport) hidMessage;
                        for (SyncStreamingListener listener : mListeners)
                            listener.onCaptureReport(captureReport);

                        // Filter the paths that are returned from the Boogie Board Sync.
                        List<SyncPath> paths = Filtering.filterSyncCaptureReport(captureReport);
                        if (paths.size() > 0) {
                            for (SyncStreamingListener listener : mListeners)
                                listener.onDrawnPaths(paths);
                            mPaths.addAll(paths);
                        }

                        // Erase button was pushed.
                        if (captureReport.hasEraseSwitchFlag()) {
                            mPaths.clear();
                            for (SyncStreamingListener listener : mListeners) listener.onErase();
                        }

                        // Save button was pushed.
                        if (captureReport.hasSaveFlag()) {
                            for (SyncStreamingListener listener : mListeners) listener.onSave();

                            // Dispatch a broadcast.
                            broadcastButtonPush(SAVE_BUTTON);
                        }
                    }
                }
            }

            // Connected to a device from the accept or connect thread.
            // Passed object will be a socket.
            else if (message.what == MESSAGE_CONNECTED) {
                connected((BluetoothSocket) message.obj);
            }

            // Disconnected from the device on a worker thread.
            else if (message.what == MESSAGE_CONNECTION_BROKEN) {
                // Update the state of the device, want to show the disconnection notification and then pop into listening mode since the accept
                // thread should still be running.
                updateDeviceState(STATE_DISCONNECTED);
                updateDeviceState(STATE_LISTENING);

                stopBluetoothHack(); // Don't need to keep transmitting hack.
            }

            // Bluetooth hack, see reference below.
            else if (message.what == MESSAGE_BLUETOOTH_HACK) {
                // Only transmit, if we are in capture mode.
                if (mMode != MODE_CAPTURE) return;

                if (DEBUG) Log.d(TAG, "transmitting bluetooth hack");

                if (!write(DUMMY_PACKET)) stopBluetoothHack();
            }
        }
    }

    private final BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction() == null) return;

            if (intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                int prevState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.ERROR);

                if (prevState == BluetoothAdapter.STATE_ON && newState == BluetoothAdapter.STATE_TURNING_OFF) {
                    stopSelf();
                }
            } else if (intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
                int newState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
                int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR);
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                // Bonded to new device. Check to see if it is a Sync device.
                if (prevState == BluetoothDevice.BOND_BONDING && newState == BluetoothDevice.BOND_BONDED) {
                    if (device != null && device.getName() != null && device.getName().equals("Sync")) {
                        updatePairedDevices();
                        start();
                    }
                } else if (prevState == BluetoothDevice.BOND_BONDED && newState == BluetoothDevice.BOND_NONE) {
                    if (device != null && device.getName() != null && device.getName().equals("Sync")) {
                        updatePairedDevices();
                        start();
                    }
                }
            }
        }
    };

    /**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until cancelled).
     */
    private class AcceptThread extends Thread {
        // The local server socket
        private final BluetoothServerSocket mServerSocket;

        public AcceptThread() {
            BluetoothServerSocket tmp = null;

            // Create a new listening server socket.
            try {
                tmp = mBluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord("Sync Streaming Profile", LISTEN_UUID);
            } catch (IOException e) {
                Log.e(TAG, "listen() failed", e);
            }
            mServerSocket = tmp;
        }

        public void run() {
            // Server socket could be null if Bluetooth was turned off and it threw an IOException
            if (mServerSocket == null) {
                Log.e(TAG, "server socket is null, finish the accept thread");
                return;
            }

            if (DEBUG) Log.d(TAG, "BEGIN mAcceptThread" + this);
            setName("AcceptThread");

            BluetoothSocket socket;
            while (true) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = mServerSocket.accept();
                } catch (IOException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket != null) {
                    synchronized (SyncStreamingService.this) {
                        // Normal operation.
                        if (mState == STATE_LISTENING || mState == STATE_DISCONNECTED) {
                            mMessageHandler.obtainMessage(MESSAGE_CONNECTED, socket).sendToTarget();
                        }

                        // Either not ready or already connected. Terminate new socket.
                        else if (mState == STATE_CONNECTED) {
                            try {
                                socket.close();
                            } catch (IOException e) {
                                Log.e(TAG, "Could not close unwanted socket", e);
                            }
                        }
                    }
                }
            }
            if (DEBUG) Log.i(TAG, "END mAcceptThread");
        }

        public void cancel() {
            if (DEBUG) Log.d(TAG, "cancel " + this);
            try {
                if (mServerSocket != null) mServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of server failed", e);
            }
        }
    }

    /**
     * This thread runs while attempting to make an outgoing connection
     * with a device. It runs straight through; the connection either
     * succeeds or fails.
     */
    private class ConnectThread extends Thread {
        private final BluetoothSocket mSocket;

        public ConnectThread(BluetoothDevice device) {
            BluetoothSocket tmp = null;

            // Get a BluetoothSocket for a connection with the given BluetoothDevice
            try {
                tmp = device.createInsecureRfcommSocketToServiceRecord(CONNECT_UUID);
            } catch (IOException e) {
                Log.e(TAG, "create() failed", e);
            }
            mSocket = tmp;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectThread");
            setName("ConnectThread");

            // Always cancel discovery because it will slow down a connection
            mBluetoothAdapter.cancelDiscovery();

            // Make a connection to the BluetoothSocket
            try {
                // This is a blocking call and will only return on a
                // successful connection or an exception
                mSocket.connect();
            } catch (IOException e) {
                // Close the socket
                try {
                    mSocket.close();
                } catch (IOException e2) {
                    Log.e(TAG, "unable to close() socket during connection failure", e2);
                }
                mMessageHandler.obtainMessage(MESSAGE_CONNECTION_BROKEN).sendToTarget();
                return;
            }

            // Reset the ConnectThread because we're done
            synchronized (SyncStreamingService.this) {
                mConnectThread = null;
            }

            // Start the connected thread
            mMessageHandler.obtainMessage(MESSAGE_CONNECTED, mSocket).sendToTarget();
        }

        public void cancel() {
            try {
                mSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }

    /**
     * This thread runs during a connection with a remote device.
     * It handles all incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final BluetoothSocket mSocket;
        private final InputStream mInputStream;
        private final OutputStream mOutputStream;

        public ConnectedThread(BluetoothSocket socket) {
            Log.d(TAG, "create ConnectedThread: ");
            mSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mInputStream = tmpIn;
            mOutputStream = tmpOut;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            byte[] buffer = new byte[1024];
            int bytes;

            // Keep listening to the InputStream while connected
            while (true) {
                try {
                    // Read from the InputStream
                    bytes = mInputStream.read(buffer);

                    // Send the obtained bytes to the main thread to be processed.
                    mMessageHandler.obtainMessage(MESSAGE_DATA, bytes, -1, buffer).sendToTarget();

                    // Reset buffer.
                    buffer = new byte[1024];
                } catch (IOException e) {
                    mMessageHandler.obtainMessage(MESSAGE_CONNECTION_BROKEN).sendToTarget();
                    if (DEBUG) Log.d(TAG, "disconnected", e);
                    break;
                }
            }
        }

        /**
         * Write to the connected OutputStream.
         *
         * @param buffer The bytes to write
         */
        public void write(byte[] buffer) {
            try {
                mOutputStream.write(buffer);
            } catch (IOException e) {
                Log.e(TAG, "Exception during write", e);
            }
        }

        public void cancel() {
            try {
                mSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }

    /**
     * On Android v0.4.3+, if you are constantly reading data from an input stream (using Bluetooth API) and are never sending any data over
     * the output stream this shows up in the console log.
     * <p/>
     * W/bt-btif﹕ dm_pm_timer expires
     * W/bt-btif﹕ dm_pm_timer expires 0
     * W/bt-btif﹕ proc dm_pm_timer expires
     * <p/>
     * One can assume that there is a timer set to ensure there is back and forth communication between a Bluetooth device. Once it is hit, the
     * input stream drops a lot of frames and some of the frames read are even corrupted.
     * <p/>
     * To combat this, every few seconds a FEND is sent to keep this timer alive and to ensure it does not expire. A.K.A. Bluetooth Hack
     * <p/>
     * Similar problem: http://stackoverflow.com/a/18508694
     */

    private Timer mBluetoothHackTimer;
    private TimerTask mBluetoothHackTimerTask;
    private final byte[] DUMMY_PACKET = new byte[]{(byte) 0xC0}; // Dummy packet just contains a frame end.

    private void startBluetoothHack() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return;

        mBluetoothHackTimer = new Timer();
        mBluetoothHackTimerTask = new TimerTask() {
            public void run() {
                mMessageHandler.obtainMessage(MESSAGE_BLUETOOTH_HACK).sendToTarget();
            }
        };

        int DELAY = 3000;
        mBluetoothHackTimer.scheduleAtFixedRate(mBluetoothHackTimerTask, DELAY, DELAY);
    }

    private void stopBluetoothHack() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return;

        if (mBluetoothHackTimer != null) {
            mBluetoothHackTimer.cancel();
            mBluetoothHackTimer.purge();
        }
    }
}