package com.adamnickle.deck;
import android.app.Activity;
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.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.adamnickle.deck.Interfaces.ConnectionFragment;
import com.adamnickle.deck.Interfaces.ConnectionListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.UUID;
import de.keyboardsurfer.android.widget.crouton.Style;
import ru.noties.debug.Debug;
public class BluetoothConnectionFragment extends ConnectionFragment<BluetoothDevice>
{
private static final UUID MY_UUID = UUID.fromString( "e40042a0-240b-11e4-8c21-0800200c9a66" );
private static final String SERVICE_NAME = "Deck Server";
private static final int REQUEST_ENABLE_BLUETOOTH = 1;
private static final int REQUEST_FIND_DEVICE = 2;
private static final int REQUEST_MAKE_DISCOVERABLE = 3;
private static final int DISCOVERABLE_DURATION = 300;
private final BluetoothAdapter mBluetoothAdapter;
private ConnectionListener mListener;
private AcceptThread mAcceptThread;
private ConnectThread mConnectThread;
private ArrayList< ConnectedThread > mConnectedThreads;
private State mState;
private ConnectionType mConnectionType;
private boolean mAskedToDiscoverable;
private int mOldScanMode;
private boolean mRetryFindDevice;
public BluetoothConnectionFragment()
{
this.mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
this.mState = State.NONE;
this.mConnectionType = ConnectionType.NONE;
mAskedToDiscoverable = false;
mOldScanMode = -1;
mRetryFindDevice = false;
mConnectedThreads = new ArrayList< ConnectedThread >();
}
@Override
public void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
setRetainInstance( true );
setHasOptionsMenu( true );
}
@Override
public void onAttach( Activity activity )
{
super.onAttach( activity );
if( mBluetoothAdapter == null )
{
activity.setResult( RESULT_BLUETOOTH_NOT_SUPPORTED, new Intent( ConnectionFragment.class.getName() ) );
activity.finish();
}
final IntentFilter filter = new IntentFilter();
filter.addAction( BluetoothAdapter.ACTION_STATE_CHANGED );
filter.addAction( BluetoothAdapter.ACTION_SCAN_MODE_CHANGED );
activity.registerReceiver( mReceiver, filter );
}
@Override
public void onDestroy()
{
getActivity().unregisterReceiver( mReceiver );
this.stopConnection();
super.onDestroy();
}
@Override
public void onCreateOptionsMenu( Menu menu, MenuInflater inflater )
{
switch( getConnectionType() )
{
case CLIENT:
inflater.inflate( R.menu.connection_client, menu );
break;
case SERVER:
inflater.inflate( R.menu.connection_server, menu );
break;
}
}
@Override
public void onPrepareOptionsMenu( Menu menu )
{
switch( getConnectionType() )
{
case CLIENT:
{
switch( mState )
{
case CONNECTED:
menu.findItem( R.id.actionLeaveServer ).setVisible( true );
break;
default:
menu.findItem( R.id.actionLeaveServer ).setVisible( false );
break;
}
break;
}
case SERVER:
{
switch( mState )
{
case LISTENING:
menu.findItem( R.id.actionFinishConnecting ).setVisible( false );
menu.findItem( R.id.actionRestartConnecting ).setVisible( false );
break;
case CONNECTED_LISTENING:
menu.findItem( R.id.actionFinishConnecting ).setVisible( true );
menu.findItem( R.id.actionRestartConnecting ).setVisible( false );
break;
case CONNECTED:
menu.findItem( R.id.actionFinishConnecting ).setVisible( false );
menu.findItem( R.id.actionRestartConnecting ).setVisible( true );
break;
}
break;
}
}
}
@Override
public boolean onOptionsItemSelected( MenuItem item )
{
switch( item.getItemId() )
{
case R.id.actionFinishConnecting:
finishConnecting();
return true;
case R.id.actionRestartConnecting:
mAskedToDiscoverable = false;
restartConnection();
return true;
case R.id.actionCloseServer:
stopConnection();
getActivity().setResult( RESULT_SERVER_CLOSED, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
return true;
case R.id.actionLeaveServer:
stopConnection();
getActivity().finish();
return true;
default:
return super.onOptionsItemSelected( item );
}
}
@Override
public void setConnectionListener( ConnectionListener connectionListener )
{
mListener = connectionListener;
}
@Override
public synchronized ConnectionType getConnectionType()
{
return mConnectionType;
}
@Override
public synchronized void setConnectionType( ConnectionType connectionType )
{
mConnectionType = connectionType;
}
@Override
public synchronized State getState()
{
return mState;
}
private synchronized void setState( State state )
{
Debug.d( "setState() %s -> %s", mState, state );
if( state != mState )
{
mState = state;
if( mListener != null )
{
mListener.onConnectionStateChange( mState );
}
}
}
@Override
public boolean isConnected()
{
return ( mState == State.CONNECTED ) || ( mState == State.CONNECTED_LISTENING );
}
@Override
public String getLocalDeviceID()
{
return mBluetoothAdapter.getAddress();
}
@Override
public String getLocalDeviceName()
{
return mBluetoothAdapter.getName();
}
@Override
public synchronized void onActivityResult( int requestCode, int resultCode, Intent data )
{
Debug.d( "onActivityResult" );
switch( requestCode )
{
case REQUEST_ENABLE_BLUETOOTH:
if( resultCode == Activity.RESULT_OK )
{
Debug.d( "ENABLE BLUETOOTH - OK" );
if( mConnectionType == ConnectionType.SERVER )
{
startConnection();
}
else if( mConnectionType == ConnectionType.CLIENT )
{
if( mState == State.NONE )
{
findServer();
}
}
}
else
{
Debug.d( "ENABLE BLUETOOTH - CANCEL" );
getActivity().setResult( RESULT_BLUETOOTH_NOT_ENABLED, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
}
break;
case REQUEST_MAKE_DISCOVERABLE:
if( resultCode != DISCOVERABLE_DURATION )
{
mListener.onNotification( "Server was not made discoverable. Unknown devices will not be able to connect.", Style.ALERT );
}
if( getState() == ConnectionFragment.State.NONE)
{
startConnection();
}
else
{
restartConnection();
}
break;
case REQUEST_FIND_DEVICE:
switch( resultCode )
{
case Activity.RESULT_OK:
final String address = data.getStringExtra( EXTRA_DEVICE_ADDRESS );
connect( mBluetoothAdapter.getRemoteDevice( address ) );
break;
case RESULT_BLUETOOTH_NOT_ENABLED:
getActivity().setResult( RESULT_BLUETOOTH_NOT_ENABLED, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
break;
case RESULT_BLUETOOTH_DISABLED:
getActivity().setResult( RESULT_BLUETOOTH_DISABLED, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
break;
default:
getActivity().setResult( RESULT_NOT_CONNECTED_TO_DEVICE, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
break;
}
break;
}
}
private synchronized boolean ensureConnection()
{
if( getConnectionType() == ConnectionType.SERVER )
{
if( !mAskedToDiscoverable && mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE )
{
Debug.d( "enabling Discoverable" );
mAskedToDiscoverable = true;
Intent discoverableIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE );
discoverableIntent.putExtra( BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, DISCOVERABLE_DURATION );
startActivityForResult( discoverableIntent, REQUEST_MAKE_DISCOVERABLE );
return false;
}
}
if( !mBluetoothAdapter.isEnabled() )
{
Debug.d( "enabling Bluetooth" );
Intent enableIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE );
startActivityForResult( enableIntent, REQUEST_ENABLE_BLUETOOTH );
return false;
}
return true;
}
public synchronized void startConnection()
{
Debug.d( "BEGIN startConnection()" );
if( !ensureConnection() )
{
return;
}
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
for( ConnectedThread thread : mConnectedThreads )
{
thread.cancel();
}
mConnectedThreads.clear();
switch( getConnectionType() )
{
case NONE:
case CLIENT:
setState( State.NONE );
break;
case SERVER:
setState( State.LISTENING );
if( mAcceptThread == null )
{
mAcceptThread = new AcceptThread();
mAcceptThread.start();
mListener.onConnectionStarted();
}
break;
}
}
@Override
public synchronized void restartConnection()
{
Debug.d( "BEGIN restartConnection()" );
if( getConnectionType() == ConnectionType.CLIENT )
{
startConnection();
return;
}
else if( getConnectionType() == ConnectionType.SERVER )
{
if( !ensureConnection() )
{
return;
}
}
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
if( mConnectedThreads.isEmpty() )
{
setState( State.LISTENING );
}
else
{
setState( State.CONNECTED_LISTENING );
}
if( mAcceptThread == null )
{
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
}
@Override
public synchronized void finishConnecting()
{
Debug.d( "BEGIN finishedConnecting" );
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
if( mAcceptThread != null )
{
mAcceptThread.cancel();
mAcceptThread = null;
}
setState( State.CONNECTED );
}
@Override
public synchronized void stopConnection()
{
Debug.d( "BEGIN stopConnection" );
setConnectionType( ConnectionType.NONE );
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
for( ConnectedThread thread : mConnectedThreads )
{
if( thread != null )
{
thread.cancel();
}
}
mConnectedThreads.clear();
if( mAcceptThread != null )
{
mAcceptThread.cancel();
mAcceptThread = null;
}
setState( State.NONE );
}
@Override
public boolean isPlayerID( String ID )
{
return BluetoothAdapter.checkBluetoothAddress( ID );
}
@Override
public void findServer()
{
Intent intent = new Intent( getActivity(), DeviceListActivity.class );
intent.putExtra( EXTRA_RETRYING_FIND, mRetryFindDevice );
startActivityForResult( intent, REQUEST_FIND_DEVICE );
mRetryFindDevice = false;
}
@Override
public synchronized void connect( BluetoothDevice device )
{
Debug.d( "connect to: %s", device );
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
mConnectThread = new ConnectThread( device );
mConnectThread.start();
setConnectionType( ConnectionType.CLIENT );
setState( State.CONNECTING );
}
public synchronized void connected( BluetoothSocket socket, BluetoothDevice device )
{
Debug.d( "connected to: %s", device );
if( mConnectThread != null )
{
mConnectThread.cancel();
mConnectThread = null;
}
if( getConnectionType() == ConnectionType.CLIENT )
{
for( ConnectedThread thread : mConnectedThreads )
{
if( thread != null )
{
thread.cancel();
}
}
mConnectedThreads.clear();
if( mAcceptThread != null )
{
mAcceptThread.cancel();
mAcceptThread = null;
}
setState( State.CONNECTED );
mListener.onConnectionStarted();
}
else
{
setState( State.CONNECTED_LISTENING );
}
ConnectedThread connectedThread = new ConnectedThread( socket );
mConnectedThreads.add( connectedThread );
if( mListener != null )
{
mListener.onDeviceConnect( connectedThread.getID(), device.getName() );
}
connectedThread.start();
}
private void write( String deviceID, byte[] out )
{
if( isConnected() )
{
synchronized( this )
{
for( ConnectedThread connectedThread : mConnectedThreads )
{
if( connectedThread.getID().equals( deviceID ) )
{
connectedThread.write( out );
}
}
}
}
}
private void connectionFailed()
{
switch( getConnectionType() )
{
case CLIENT:
stopConnection();
mRetryFindDevice = true;
this.findServer();
break;
case SERVER:
if( mListener != null )
{
mListener.onConnectionFailed();
}
restartConnection();
break;
}
}
private void connectionLost( ConnectedThread connectedThread )
{
if( mListener != null )
{
mListener.onConnectionLost( connectedThread.getID() );
}
mConnectedThreads.remove( connectedThread );
if( getConnectionType() == ConnectionType.CLIENT )
{
stopConnection();
}
}
@Override
public void sendDataToDevice( String deviceID, byte[] data )
{
if( !isConnected() )
{
if( mListener != null )
{
mListener.onNotification( "Not connected", Style.ALERT );
}
return;
}
if( data.length > 0 )
{
write( deviceID, data );
}
}
private class AcceptThread extends Thread
{
private final BluetoothServerSocket mServerSocket;
public AcceptThread()
{
BluetoothServerSocket temp = null;
try
{
temp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord( SERVICE_NAME, MY_UUID );
}
catch( IOException io )
{
Debug.e( "Socket listen() failed", io );
}
mServerSocket = temp;
}
@Override
public void run()
{
Debug.d( "BEGIN AcceptThread" );
setName( "AcceptThread" );
BluetoothSocket socket;
while( mState != ConnectionFragment.State.CONNECTED )
{
try
{
socket = mServerSocket.accept();
}
catch( IOException io )
{
Debug.e( "Accept Thread accept() failed", io );
break;
}
if( socket != null )
{
synchronized( BluetoothConnectionFragment.this )
{
switch( mState )
{
case LISTENING:
case CONNECTING:
case CONNECTED_LISTENING:
connected( socket, socket.getRemoteDevice() );
break;
case NONE:
case CONNECTED:
try
{
socket.close();
}
catch( IOException io )
{
Debug.e( "Could not close unwanted socket", io );
}
break;
}
}
}
}
Debug.d( "END AcceptThread" );
}
public void cancel()
{
Debug.d( "AcceptThread cancel()" );
try
{
mServerSocket.close();
}
catch( IOException io )
{
Debug.e( "close() of server failed", io );
}
}
}
private class ConnectThread extends Thread
{
private final BluetoothSocket mSocket;
private final BluetoothDevice mDevice;
public ConnectThread( BluetoothDevice device )
{
mDevice = device;
BluetoothSocket temp = null;
try
{
temp = device.createRfcommSocketToServiceRecord( MY_UUID );
}
catch( IOException io )
{
Debug.e( "ConnectThread create() failed", io );
}
mSocket = temp;
}
@Override
public void run()
{
Debug.d( "BEGIN ConnectThread" );
setName( "ConnectThread" );
mBluetoothAdapter.cancelDiscovery();
try
{
mSocket.connect();
}
catch( IOException io )
{
try
{
mSocket.close();
}
catch( IOException io2 )
{
Debug.e( "unable to close()", io2 );
}
connectionFailed();
return;
}
synchronized( BluetoothConnectionFragment.this )
{
mConnectThread = null;
}
connected( mSocket, mDevice );
}
public void cancel()
{
try
{
mSocket.close();
}
catch( IOException io )
{
Debug.e( "ConnectThread close() of socket failed", io );
}
}
}
private class ConnectedThread extends Thread
{
private final BluetoothSocket mSocket;
private final InputStream mInputStream;
private final OutputStream mOutputStream;
private final String mID;
public ConnectedThread( BluetoothSocket socket )
{
Debug.d( "BEGIN create ConnectedThread" );
mSocket = socket;
synchronized( BluetoothConnectionFragment.this )
{
mID = socket.getRemoteDevice().getAddress();
}
InputStream tmpIn = null;
OutputStream tmpOut = null;
try
{
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
}
catch( IOException io )
{
Debug.e( "Failed to get input/output streams", io );
}
mInputStream = tmpIn;
mOutputStream = tmpOut;
}
public String getID()
{
return mID;
}
public void run()
{
Debug.d( "BEGIN ConnectedThread" );
byte[] buffer = new byte[ 8192 ];
int bytes;
while( true )
{
try
{
bytes = mInputStream.read( buffer );
if( bytes > 0 )
{
if( mListener != null )
{
mListener.onMessageReceive( mID, bytes, buffer );
}
}
}
catch( IOException io )
{
Debug.e( "failed to read, disconnected", io );
connectionLost( this );
break;
}
}
}
public void write( byte[] buffer )
{
try
{
mOutputStream.write( buffer );
}
catch( IOException io )
{
Debug.e( "Exception during write", io );
}
}
public void cancel()
{
try
{
mSocket.close();
}
catch( IOException io )
{
Debug.e( "close() of connected socket failed", io );
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver()
{
@Override
public void onReceive( Context context, Intent intent )
{
final String action = intent.getAction();
if( BluetoothAdapter.ACTION_STATE_CHANGED.equals( action ) )
{
final int state = intent.getIntExtra( BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR );
switch( state )
{
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
stopConnection();
getActivity().setResult( RESULT_BLUETOOTH_DISABLED, new Intent( ConnectionFragment.class.getName() ) );
getActivity().finish();
break;
}
}
else if( BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals( action ) )
{
final int newScanMode = intent.getIntExtra( BluetoothAdapter.EXTRA_SCAN_MODE, -1 );
if( mOldScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE && newScanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE )
{
BluetoothConnectionFragment.this.finishConnecting();
}
mOldScanMode = newScanMode;
}
}
};
}