Control Garage Door Openers using Android

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.

Goals

In this project we will create a simple Android app that resembles the functionality of the actual remote control.

Step 1: Creating the GUI

After creating a new "Android Application Project" named "Garage Control" in Eclipse we start with creating the GUI:

App 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);
    }
}

Step 2: Discover Bricks and Bricklets

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.

Step 3: Triggering Switches

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!

Step 4: 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!

Step 5: Error Handling and Reporting

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
}
  • SUCCESS: The connection got established and an Industrial Quad Relay Bricklet with the given UID was found.
  • NO_CONNECTION: The connection could not be established.
  • NO_DEVICE: The connection got established but there was no Industrial Quad Relay Bricklet with the given UID.

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.

Step 6: Persistent Configuration and State

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.

Step 7: Everything put together

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