For this project we are assuming, that you have the Android SDK set up and that you have a rudimentary understanding of the Java language.
If you are totally new to Java itself you should start here. If you are new to the Tinkerforge API, you should start here.
We are also assuming that you have a remote control connected to an Industrial Quad Relay Bricklet as described here.
The complete Eclipse project can be downloaded here.
In this project we will create a simple Android app that resembles the functionality of the actual remote control.
After creating a new "Android Application Project" named "Garage Control" in Eclipse we start with creating the GUI:
The basis is a "Linear Layout (Vertical)". Three text fields allow to enter the host, port and UID of the Industrial Quad Relay Bricklet. For the port a "Number" text field is used, so Android will restrict the content of this text field to numbers. The final two elements are one "Button" to connect and disconnect and another one to trigger the remote control.
Now the GUI layout is finished. To access the GUI components in the Java code
we use the findViewById()
method and store the references to member
variables of the MainActivity
class:
public class MainActivity extends Activity {
private EditText host;
private EditText port;
private EditText uid;
private Button connect;
private Button trigger;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
host = (EditText)findViewById(R.id.host);
port = (EditText)findViewById(R.id.port);
uid = (EditText)findViewById(R.id.uid);
connect = (Button)findViewById(R.id.connect);
trigger = (Button)findViewById(R.id.trigger);
}
}
This step is similar to step 1 in the
Read out Smoke Detectors using Java project.
We apply some changes to make it work in a GUI program and instead of using the
EnumerateListener
to discover the Industrial Quad Relay Bricklet its UID
has to be specified. This approach allows to pick the correct Industrial Quad
Relay Bricklet even if multiple are connected to the same host at once.
We don't want to call the connect()
method directly, because it might take
a moment and block the GUI during that period of time. Instead connect()
will
be called from an AsyncTask
, so it will run in the background and the GUI
stays responsive:
public class MainActivity extends Activity {
// [...]
private IPConnection ipcon;
private BrickletIndustrialQuadRelay relay;
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
private String currentHost;
private String currentPort;
private String currentUID;
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
}
protected Void doInBackground(Void... params) {
ipcon = new IPConnection();
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
ipcon.connect(currentHost, Integer.parseInt(currentPort));
return null;
}
}
}
The ConnectAsyncTask
is implemented as an inner class. This allows for
direct access to the member variables of the MainActivity
class.
The onPreExecute()
method is called before doInBackground()
and stores
the host, port and UID configuration from the GUI elements. This is necessary,
because the doInBackground()
method needs this information, but is not
allowed to access the GUI elements. Now the doInBackground()
method can
create an IPConnection
and BrickletIndustrialQuadRelay
object and call
the connect()
method.
Finally, a ConnectAsyncTask
should be created and executed when the connect
button is clicked. A OnClickListener
added to the connect button does this:
public class MainActivity extends Activity {
// [...]
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// [...]
connect.setOnClickListener(new ConnectClickListener());
}
}
Host, port and UID can now be configured and a click on the connect button establishes the connection.
The connection is established and the Industrial Quad Relay Bricklet is found but there is no logic yet to trigger the switch on the remote control if the trigger button is clicked.
An OnClickListener
is added to the trigger button that creates and executes
an AsyncTask
that in turn calls the setMonoflop()
method of the
Industrial Quad Relay Bricklet to trigger the switch on the remote control:
public class MainActivity extends Activity {
// [...]
private BrickletIndustrialQuadRelay relay;
@Override
protected void onCreate(Bundle savedInstanceState) {
// [...]
trigger.setOnClickListener(new TriggerClickListener());
}
class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
relay.setMonoflop(1 << 0, 15, 1500);
return null;
}
}
class TriggerClickListener implements OnClickListener {
public void onClick(View v) {
new TriggerAsyncTask().execute();
}
}
}
The call to setMonoflop(1 << 0, 15, 1500)
closes the first relay for 1.5s then opens it again.
That's it. If we would copy these three steps together in one project, we would have a working app that allows a smart phone to control a garage door opener using its hacked remote control!
We don't have a disconnect button yet and the trigger button can be clicked before the connection is established. We need some more GUI logic!
There is no button to close the connection again after it got established. The connect button could do this. When the connection is established it should allow to disconnect it again:
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPostExecute(Void result) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
}
}
The onPostExecute()
method is called after doInBackground()
. It changes
the text on the button and sets a new OnClickListener
that will closes the
connection if the button is clicked:
class DisconnectClickListener implements OnClickListener {
public void onClick(View v) {
new DisconnectAsyncTask().execute();
}
}
We don't want to call the disconnect()
method directly, because it might take a moment and block the GUI during that
period of time. Instead disconnect()
will be called from an AsyncTask
,
so it will run in the background and the GUI stays responsive:
class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
ipcon.disconnect();
return null;
}
@Override
protected void onPostExecute(Void result) {
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
}
}
Once the connection is closed a new OnClickListener
is set that will
execute ConnectAsyncTask
to establish the connection if the connect button
is clicked:
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
Finally, the user should not be able to change the content of the text fields during the time the connection gets established and the trigger button should not be clickable if there is no connection.
The ConnectAsyncTask
and the DisconnectAsyncTask
are extended to
disable and enable the GUI elements according to the current connection state:
class ConnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPreExecute() {
// [...]
host.setEnabled(false);
port.setEnabled(false);
uid.setEnabled(false);
connect.setEnabled(false);
trigger.setEnabled(false);
}
// [...]
@Override
protected void onPostExecute(Void result) {
// [...]
connect.setEnabled(true);
trigger.setEnabled(true);
}
}
class DisconnectAsyncTask extends AsyncTask<Void, Void, Void> {
// [...]
@Override
protected void onPreExecute() {
connect.setEnabled(false);
trigger.setEnabled(false);
}
// [...]
@Override
protected void onPostExecute(Void result) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setEnabled(true);
// [...]
}
}
But the program is not yet robust enough. What happens if can't connect? What happens if there is no Industrial Quad Relay Bricklet with the given UID?
What we need is error handling!
We will use similar principals as in step 4 of the Read out Smoke Detectors using Java project, but with some changes to make it work in a GUI program.
We can't just use System.out.println()
for error reporting because there
is no console window in an app. Instead dialog boxes are used.
The ConnectAsyncTask
has to validate the user input before using it. An
AlertDialog
is used to report possible problems:
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
// [...]
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage("Host/Port/UID cannot be empty");
builder.create().show();
cancel(true);
return;
}
}
// [...]
}
The ConnectAsyncTask
also gains a ProgressDialog
to show the connection
process:
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
// [...]
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
// [...]
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
progressDialog.setCancelable(false);
progressDialog.show();
}
// [...]
}
Then the doInBackground()
method needs to be able to report its result. But
it is not allowed to interact with the GUI, but it can return a value that is
then passed to the onPostExecute()
method. We use this enum
that
represents the three possible outcomes of a connection attempt:
enum ConnectResult {
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
The getIdentity()
method is used to check that the device for the given
UID really is an Industrial Quad Relay Bricklet. If this is not the case then
the connection gets closed:
protected ConnectResult doInBackground(Void... params) {
ipcon = new IPConnection();
try {
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
} catch(IllegalArgumentException e) {
return ConnectResult.NO_DEVICE;
}
try {
ipcon.connect(currentHost, currentPort);
} catch(java.net.UnknownHostException e) {
return ConnectResult.NO_CONNECTION;
} catch(IllegalArgumentException e) {
return ConnectResult.NO_CONNECTION;
} catch(java.io.IOException e) {
return ConnectResult.NO_CONNECTION;
} catch(com.tinkerforge.AlreadyConnectedException e) {
return ConnectResult.NO_CONNECTION;
}
try {
if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
ipcon.disconnect();
return ConnectResult.NO_DEVICE;
}
} catch (com.tinkerforge.TinkerforgeException e1) {
try {
ipcon.disconnect();
} catch (com.tinkerforge.NotConnectedException e2) {
}
return ConnectResult.NO_DEVICE;
}
return ConnectResult.SUCCESS;
}
Now the onPostExecute()
method has to handle this three outcomes. First the
progress dialog is dismissed:
@Override
protected void onPostExecute(ConnectResult result) {
progressDialog.dismiss();
In case the connection attempt was successful the original logic stays the same:
if (result == ConnectResult.SUCCESS) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
connect.setEnabled(true);
trigger.setEnabled(true);
}
In the error case we use an AlertDialog
and set the error message according
to the connection result:
else {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (result == ConnectResult.NO_CONNECTION) {
builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
} else { // ConnectResult.NO_DEVICE
builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
}
builder.setCancelable(false);
Then retry and cancel buttons are added with the respective logic to either retry to connect or to cancel the connection attempt:
builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
new ConnectAsyncTask().execute();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
connect.setEnabled(true);
dialog.dismiss();
}
});
Finally, the dialog gets created and shown:
builder.create().show();
}
}
Now the app can connect to an configurable host and port and trigger a button on the remote control of your garage door opener using an Industrial Quad Relay Bricklet.
The app doesn't store its configuration yet. Android provides the
SharedPreferences
class to take care of this. In onCreate()
the
configuration is restored:
protected void onCreate(Bundle savedInstanceState) {
// [...]
SharedPreferences settings = getPreferences(0);
host.setText(settings.getString("host", host.getText().toString()));
port.setText(settings.getString("port", port.getText().toString()));
uid.setText(settings.getString("uid", uid.getText().toString()));
}
In onStop()
the configuration is then stored again:
protected void onStop() {
super.onStop();
SharedPreferences settings = getPreferences(0);
SharedPreferences.Editor editor = settings.edit();
editor.putString("host", host.getText().toString());
editor.putString("port", port.getText().toString());
editor.putString("uid", uid.getText().toString());
editor.commit();
}
If the orientation is changed Android basically restarts the app for the
new orientation. This makes our app loss the connection. Therefore, the state
of the connection is stored when onSaveInstanceState()
is called:
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("connected", ipcon != null &&
ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED);
}
And is restored when onCreate()
is called:
protected void onCreate(Bundle savedInstanceState) {
// [...]
if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
new ConnectAsyncTask().execute();
}
}
Now the configuration and state is stored persistent across a restart of the app.
That's it! We are done with the app for our hacked garage door opener remote control.
Now all of the above put together (Download):
package com.tinkerforge.garagecontrol;
import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import com.tinkerforge.BrickletIndustrialQuadRelay;
import com.tinkerforge.IPConnection;
import com.tinkerforge.TinkerforgeException;
import com.tinkerforge.NotConnectedException;
import com.tinkerforge.AlreadyConnectedException;
public class MainActivity extends Activity {
final Context context = this;
private IPConnection ipcon = null;
private BrickletIndustrialQuadRelay relay = null;
private EditText host;
private EditText port;
private EditText uid;
private Button connect;
private Button trigger;
enum ConnectResult {
SUCCESS,
NO_CONNECTION,
NO_DEVICE
}
class ConnectAsyncTask extends AsyncTask<Void, Void, ConnectResult> {
private ProgressDialog progressDialog;
private String currentHost;
private String currentPort;
private String currentUID;
@Override
protected void onPreExecute() {
currentHost = host.getText().toString();
currentPort = port.getText().toString();
currentUID = uid.getText().toString();
if (currentHost.length() == 0 || currentPort.length() == 0 || currentUID.length() == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage("Host/Port/UID cannot be empty");
builder.create().show();
cancel(true);
return;
}
host.setEnabled(false);
port.setEnabled(false);
uid.setEnabled(false);
connect.setEnabled(false);
trigger.setEnabled(false);
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Connecting to " + currentHost + ":" + currentPort);
progressDialog.setCancelable(false);
progressDialog.show();
}
protected ConnectResult doInBackground(Void... params) {
ipcon = new IPConnection();
try {
relay = new BrickletIndustrialQuadRelay(currentUID, ipcon);
} catch(IllegalArgumentException e) {
return ConnectResult.NO_DEVICE;
}
try {
ipcon.connect(currentHost, Integer.parseInt(currentPort));
} catch(java.net.UnknownHostException e) {
return ConnectResult.NO_CONNECTION;
} catch(IllegalArgumentException e) {
return ConnectResult.NO_CONNECTION;
} catch(java.io.IOException e) {
return ConnectResult.NO_CONNECTION;
} catch(AlreadyConnectedException e) {
return ConnectResult.NO_CONNECTION;
}
try {
if (relay.getIdentity().deviceIdentifier != BrickletIndustrialQuadRelay.DEVICE_IDENTIFIER) {
ipcon.disconnect();
return ConnectResult.NO_DEVICE;
}
} catch (TinkerforgeException e1) {
try {
ipcon.disconnect();
} catch (NotConnectedException e2) {
}
return ConnectResult.NO_DEVICE;
}
return ConnectResult.SUCCESS;
}
@Override
protected void onPostExecute(ConnectResult result) {
progressDialog.dismiss();
if (result == ConnectResult.SUCCESS) {
connect.setText("Disconnect");
connect.setOnClickListener(new DisconnectClickListener());
connect.setEnabled(true);
trigger.setEnabled(true);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (result == ConnectResult.NO_CONNECTION) {
builder.setMessage("Could not connect to " + currentHost + ":" + currentPort);
} else { // ConnectResult.NO_DEVICE
builder.setMessage("Could not find Industrial Quad Relay Bricklet [" + currentUID + "]");
}
builder.setCancelable(false);
builder.setPositiveButton("Retry", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
new ConnectAsyncTask().execute();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
connect.setEnabled(true);
dialog.dismiss();
}
});
builder.create().show();
}
}
}
class DisconnectAsyncTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected void onPreExecute() {
connect.setEnabled(false);
trigger.setEnabled(false);
}
protected Boolean doInBackground(Void... params) {
try {
ipcon.disconnect();
return true;
} catch(TinkerforgeException e) {
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
host.setEnabled(true);
port.setEnabled(true);
uid.setEnabled(true);
connect.setText("Connect");
connect.setOnClickListener(new ConnectClickListener());
}
connect.setEnabled(true);
}
}
class TriggerAsyncTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... params) {
try {
relay.setMonoflop(1 << 0, 15, 1500);
} catch (TinkerforgeException e) {
}
return null;
}
}
class ConnectClickListener implements OnClickListener {
public void onClick(View v) {
new ConnectAsyncTask().execute();
}
}
class DisconnectClickListener implements OnClickListener {
public void onClick(View v) {
new DisconnectAsyncTask().execute();
}
}
class TriggerClickListener implements OnClickListener {
public void onClick(View v) {
new TriggerAsyncTask().execute();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
host = (EditText)findViewById(R.id.host);
port = (EditText)findViewById(R.id.port);
uid = (EditText)findViewById(R.id.uid);
connect = (Button)findViewById(R.id.connect);
trigger = (Button)findViewById(R.id.trigger);
SharedPreferences settings = getPreferences(0);
host.setText(settings.getString("host", host.getText().toString()));
port.setText(settings.getString("port", port.getText().toString()));
uid.setText(settings.getString("uid", uid.getText().toString()));
connect.setOnClickListener(new ConnectClickListener());
trigger.setOnClickListener(new TriggerClickListener());
trigger.setEnabled(false);
if (savedInstanceState != null && savedInstanceState.getBoolean("connected", false)) {
new ConnectAsyncTask().execute();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean connected = false;
if (ipcon != null) {
connected = ipcon.getConnectionState() == IPConnection.CONNECTION_STATE_CONNECTED;
}
outState.putBoolean("connected", connected);
}
@Override
protected void onStop() {
super.onStop();
SharedPreferences settings = getPreferences(0);
SharedPreferences.Editor editor = settings.edit();
editor.putString("host", host.getText().toString());
editor.putString("port", port.getText().toString());
editor.putString("uid", uid.getText().toString());
editor.commit();
}
}