Android Bluetooth Terminal


The post in my another blogspot show how to "Config Raspberry Pi to use HC-06 Bluetooth Module, as Serial Terminal".

My former post show how "Android communicate with Arduino + HC-06 Bluetooth Module" to link Android with other serial devices via bluetooth. Actually, it can communicate with Raspberry Pi + HC-06 also.

This example I re-code "Android communicate with Arduino + HC-06 Bluetooth Module" to make it work like a serial terminal to log-in Raspberry Pi via bluetooth.

- Prepare on Raspberry Pi 2, to config serial terminal work on 9600 baud, refer "Config Raspberry Pi to use HC-06 Bluetooth Module, as Serial Terminal".
-  The Android and HC-06 have to pair in advance.


Create a new project of Blank Activity in Android Studio,

layout/content_main.xml, it's the main terminal screen.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:autoLink="web"
android:text="http://android-er.blogspot.com/"
android:textStyle="bold" />

<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="@android:color/white"
android:background="@android:color/black"
android:typeface="monospace"
android:gravity="bottom"/>

</LinearLayout>


Edit layout/activity_main.xml, just to change the icon of the FloatingActionButton.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:fitsSystemWindows="true"
tools:context=".MainActivity">

<android.support.design.widget.AppBarLayout android:layout_height="wrap_content"
android:layout_width="match_parent" android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_main" />

<android.support.design.widget.FloatingActionButton android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_menu_edit" />

</android.support.design.widget.CoordinatorLayout>


Create layout/setting_layout.xml, the layout of SettingDialog (OptionsMenu -> Setting), to list paired bluetooth to connect.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ListView
android:id="@+id/pairedlist"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

Create layout/typing_layout.xml, the layout of CmdLineDialog (open once FloatingActionButton is clicked), for user to enter command.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">


<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/cmdline"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />

<ImageView
android:id="@+id/clearcmd"
android:layout_width="wrap_content"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/clear"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Clear"/>
<Button
android:id="@+id/dismiss"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Dismiss"/>
<Button
android:id="@+id/enter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Enter"/>
</LinearLayout>

</LinearLayout>

MainActivity.java
[remark: a bug here, refer Update@2015-11-11on bottom of this post.]
package com.blogspot.android_er.androidbluetoothterminal;

import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.method.ScrollingMovementMethod;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Set;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {

private static final int REQUEST_ENABLE_BT = 1;

BluetoothAdapter bluetoothAdapter;

ArrayList<BluetoothDevice> pairedDeviceArrayList;
ArrayAdapter<BluetoothDevice> pairedDeviceAdapter;
private static UUID myUUID;
private final String UUID_STRING_WELL_KNOWN_SPP =
"00001101-0000-1000-8000-00805F9B34FB";

ThreadConnectBTdevice myThreadConnectBTdevice;
ThreadConnected myThreadConnected;

static TextView body;
FloatingActionButton fab;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

body = (TextView)findViewById(R.id.body);
body.setMovementMethod(new ScrollingMovementMethod());

fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showCmdLineDialog();
}
});

fab.setEnabled(false);
fab.setVisibility(FloatingActionButton.GONE);

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)){
Toast.makeText(this,
"FEATURE_BLUETOOTH NOT support",
Toast.LENGTH_LONG).show();
finish();
return;
}

//using the well-known SPP UUID
myUUID = UUID.fromString(UUID_STRING_WELL_KNOWN_SPP);

bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
Toast.makeText(this,
"Bluetooth is not supported on this hardware platform",
Toast.LENGTH_LONG).show();
finish();
return;
}

String strInfo = bluetoothAdapter.getName() + "\n" +
bluetoothAdapter.getAddress();
Toast.makeText(getApplicationContext(), strInfo, Toast.LENGTH_LONG).show();

}

@Override
protected void onStart() {
super.onStart();

//Turn ON BlueTooth if it is OFF
if (!bluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
}

@Override
protected void onDestroy() {
super.onDestroy();
closeThreads();
}

private void closeThreads(){
if(myThreadConnectBTdevice!=null){
myThreadConnectBTdevice.cancel();
myThreadConnectBTdevice = null;
}

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

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode==REQUEST_ENABLE_BT){
if(resultCode == Activity.RESULT_OK){

}else{
Toast.makeText(this,
"BlueTooth NOT enabled",
Toast.LENGTH_SHORT).show();
finish();
}
}
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
closeThreads();
fab.setEnabled(false);
fab.setVisibility(FloatingActionButton.GONE);
body.setText("");
showSettingDialog();
return true;
}

return super.onOptionsItemSelected(item);
}

void showSettingDialog() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment prev = getFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);

DialogFragment newFragment = SettingDialogFragment.newInstance(MainActivity.this);
newFragment.show(ft, "dialog");

}

void showCmdLineDialog() {

if(myThreadConnected == null){
Toast.makeText(MainActivity.this,
"myThreadConnected == null",
Toast.LENGTH_LONG).show();
return;
}

FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment prev = getFragmentManager().findFragmentByTag("cmdline");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);

DialogFragment newFragment = TypingDialogFragment.newInstance(MainActivity.this, myThreadConnected);
newFragment.show(ft, "cmdline");

}

private void setup(ListView lv, final Dialog dialog) {
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
pairedDeviceArrayList = new ArrayList<BluetoothDevice>();

for (BluetoothDevice device : pairedDevices) {
pairedDeviceArrayList.add(device);
}

pairedDeviceAdapter = new ArrayAdapter<BluetoothDevice>(this,
android.R.layout.simple_list_item_1, pairedDeviceArrayList);
lv.setAdapter(pairedDeviceAdapter);

lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
BluetoothDevice device =
(BluetoothDevice) parent.getItemAtPosition(position);
Toast.makeText(MainActivity.this,
"Name: " + device.getName() + "\n"
+ "Address: " + device.getAddress() + "\n"
+ "BondState: " + device.getBondState() + "\n"
+ "BluetoothClass: " + device.getBluetoothClass() + "\n"
+ "Class: " + device.getClass(),
Toast.LENGTH_LONG).show();

Toast.makeText(MainActivity.this, "start ThreadConnectBTdevice", Toast.LENGTH_LONG).show();
myThreadConnectBTdevice = new ThreadConnectBTdevice(device, dialog);
myThreadConnectBTdevice.start();
}
});
}
}

public static class SettingDialogFragment extends DialogFragment {

ListView listViewPairedDevice;
static MainActivity parentActivity;

static SettingDialogFragment newInstance(MainActivity parent){
parentActivity = parent;
SettingDialogFragment f = new SettingDialogFragment();
return f;
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
getDialog().setTitle("Setting");
getDialog().setCanceledOnTouchOutside(false);
View settingDialogView = inflater.inflate(R.layout.setting_layout, container, false);

listViewPairedDevice = (ListView)settingDialogView.findViewById(R.id.pairedlist);

parentActivity.setup(listViewPairedDevice, getDialog());

return settingDialogView;
}
}

public static class TypingDialogFragment extends DialogFragment {

EditText cmdLine;
static MainActivity parentActivity;
static ThreadConnected cmdThreadConnected;

static TypingDialogFragment newInstance(MainActivity parent, ThreadConnected thread){
parentActivity = parent;
cmdThreadConnected = thread;
TypingDialogFragment f = new TypingDialogFragment();
return f;
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
getDialog().setTitle("Cmd Line");
getDialog().setCanceledOnTouchOutside(false);
View typingDialogView = inflater.inflate(R.layout.typing_layout, container, false);

cmdLine = (EditText)typingDialogView.findViewById(R.id.cmdline);

ImageView imgCleaarCmd = (ImageView)typingDialogView.findViewById(R.id.clearcmd);
imgCleaarCmd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
cmdLine.setText("");
}
});

Button btnEnter = (Button)typingDialogView.findViewById(R.id.enter);
btnEnter.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(cmdThreadConnected!=null){
byte[] bytesToSend = cmdLine.getText().toString().getBytes();
cmdThreadConnected.write(bytesToSend);
byte[] NewLine = "\n".getBytes();
cmdThreadConnected.write(NewLine);
}
}
});

Button btnDismiss = (Button)typingDialogView.findViewById(R.id.dismiss);
btnDismiss.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});

Button btnClear = (Button)typingDialogView.findViewById(R.id.clear);
btnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
body.setText("");
}
});

return typingDialogView;
}
}

//Called in ThreadConnectBTdevice once connect successed
//to start ThreadConnected
private void startThreadConnected(BluetoothSocket socket){

myThreadConnected = new ThreadConnected(socket);
myThreadConnected.start();
}

/*
ThreadConnectBTdevice:
Background Thread to handle BlueTooth connecting
*/
private class ThreadConnectBTdevice extends Thread {

private BluetoothSocket bluetoothSocket = null;
private final BluetoothDevice bluetoothDevice;
Dialog dialog;

private ThreadConnectBTdevice(BluetoothDevice device, Dialog dialog) {
this.dialog = dialog;
bluetoothDevice = device;

try {
bluetoothSocket = device.createRfcommSocketToServiceRecord(myUUID);
Toast.makeText(MainActivity.this,
"bluetoothSocket: \n" + bluetoothSocket,
Toast.LENGTH_SHORT).show();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

@Override
public void run() {
boolean success = false;
try {
bluetoothSocket.connect();
success = true;
} catch (IOException e) {
e.printStackTrace();

final String eMessage = e.getMessage();
runOnUiThread(new Runnable() {

@Override
public void run() {
Toast.makeText(MainActivity.this,
"something wrong bluetoothSocket.connect(): \n" + eMessage,
Toast.LENGTH_SHORT).show();
}
});

try {
bluetoothSocket.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}

if(success){
//connect successful
final String msgconnected = "connect successful:\n"
+ "BluetoothSocket: " + bluetoothSocket + "\n"
+ "BluetoothDevice: " + bluetoothDevice;

runOnUiThread(new Runnable() {

@Override
public void run() {
fab.setEnabled(true);
fab.setVisibility(FloatingActionButton.VISIBLE);
body.setText("");
Toast.makeText(MainActivity.this, msgconnected, Toast.LENGTH_LONG).show();
dialog.dismiss();
}
});

startThreadConnected(bluetoothSocket);

}else{
//fail
}
}

public void cancel() {

Toast.makeText(getApplicationContext(),
"close bluetoothSocket",
Toast.LENGTH_LONG).show();
try {
bluetoothSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}

/*
ThreadConnected:
Background Thread to handle Bluetooth data communication
after connected
*/
private class ThreadConnected extends Thread {
private final BluetoothSocket connectedBluetoothSocket;
private final InputStream connectedInputStream;
private final OutputStream connectedOutputStream;

boolean running;

public ThreadConnected(BluetoothSocket socket) {
connectedBluetoothSocket = socket;
InputStream in = null;
OutputStream out = null;
running = true;
try {
in = socket.getInputStream();
out = socket.getOutputStream();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

connectedInputStream = in;
connectedOutputStream = out;
}

@Override
public void run() {
byte[] buffer = new byte[1024];
int bytes;

String strRx = "";

while (running) {
try {
bytes = connectedInputStream.read(buffer);
final String strReceived = new String(buffer, 0, bytes);
final String strByteCnt = String.valueOf(bytes) + " bytes received.\n";

runOnUiThread(new Runnable(){

@Override
public void run() {
body.append(strReceived);
}});

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();

final String msgConnectionLost = "Connection lost:\n"
+ e.getMessage();
runOnUiThread(new Runnable(){

@Override
public void run() {
Toast.makeText(MainActivity.this, msgConnectionLost, Toast.LENGTH_LONG).show();

}});
}
}
}

public void write(byte[] buffer) {
try {
connectedOutputStream.write(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public void cancel() {
running = false;
try {
connectedBluetoothSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}


uses-permission of "android.permission.BLUETOOTH" is needed in src/main/AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.blogspot.android_er.androidbluetoothterminal" >

<uses-permission android:name="android.permission.BLUETOOTH"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>


download filesDownload the files (Android Studio Format) .

download filesDownload APK .


Update@2015-11-11:
In the above code of MainActivity.java, in run() of ThreadConnected, if Connection lost (such as Raspberry Pi or HC-06 power OFF), the thread will still keep running.

So have to cancel the thread and disable the FloatingActionButton. Modify run() of ThreadConnected.
        @Override
public void run() {
byte[] buffer = new byte[1024];
int bytes;

String strRx = "";

while (running) {
try {
bytes = connectedInputStream.read(buffer);
final String strReceived = new String(buffer, 0, bytes);
final String strByteCnt = String.valueOf(bytes) + " bytes received.\n";

runOnUiThread(new Runnable(){

@Override
public void run() {
body.append(strReceived);
}});

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();

cancel();

final String msgConnectionLost = "Connection lost:\n"
+ e.getMessage();
runOnUiThread(new Runnable(){

@Override
public void run() {
Toast.makeText(MainActivity.this, msgConnectionLost, Toast.LENGTH_LONG).show();

fab.setEnabled(false);
fab.setVisibility(FloatingActionButton.GONE);


}});
}
}
}