/** * ************************************************************************** * 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(); } } }