Building an IoT Doorbell (Part 2)

Nick Felker
8 min readOct 28, 2015

--

Hey! If you haven’t read the first part of this tutorial, you should read that first. It discusses how to create your first activity and layout in Android and how to connect the two to create an interactive app.

The code for this tutorial is available on GitHub, and you can follow along at this commit.

Today we’ll integrate our server with the app so that we can create and respond to doorbell events. The server is hosted on Heroku with some simple Node.js code. It’s not too pretty, but it’s sufficient to do its job.

We’ll use HTTP requests to three different API endpoints to communicate with our server:

new — Creates a new event object. Returns the `event_id`

event — Queries a provided `event` and checks for the status and time

respond — Responds to a given `event` given a status code. We briefly discussed the three codes last time.

It can be a little complicated to program our own HTTP request code, and it’s not necessary. We can use a library, a set of code that somebody else already wrote, and use it in our own projects.

We’ll be using Retrofit, a very popular library for making HTTP requests because of how easy it becomes.

In order to use it, go to your app’s build.gradle. Gradle is admittedly confusing, but powerful, script that can define your builds. However, you don’t need to learn the entirety of gradle to take advantage of its capabilities.

apply plugin: 'com.android.application'

android {
compileSdkVersion 23
buildToolsVersion "23.0.1"

defaultConfig {
applicationId "com.felkertech.n.iot_doorbell_android"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.0'
}

You can modify some of your project values in the defaultConfig section. This is useful if you want to change the minSDKversion or the app version.

At the very bottom you’ll see dependencies. You can add pretty much any Android library down there. Then Android Studio will handle the downloading and compiling automatically. You don’t need to worry about it. If you want the newest version, you can just replace the version number with a +.

It’s not recommended you do this, because constantly using the newest library means that something big can change and cause a bunch of problems in your app due to compatibility. (You can opt for 23.1.+ and that will download the most recent bug fixes and probably won’t cause any major problems.)

To add Retrofit to our project, just add three simple lines:

compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
compile 'com.squareup.okhttp:okhttp:2.4.0'

Then you’ll be told to do a Gradle sync. Do that and the libraries will install and be ready to use.

Let’s create a new interface called ServerCommands. We will use this to quickly transform our HTTP requests into a standard Java interface.

package com.felkertech.n.iot_doorbell_android;

import retrofit.Call;
import retrofit.http.GET;
import retrofit.http.Path;
import retrofit.http.Query;
public interface ServerCommands {
@GET("new")
Call<NewEvent> newEvent();

@GET("event")
Call<Event> getEvent(@Query("event") int event_id);

@GET("respond")
Call<ResponseEvent> respond(@Query("event_id") int event_id,
@Query("result") int status);
}

Each of our three API endpoints is referenced in the @Get annotation. To add queries for the “event” and “respond” requests, we can add parameters to our methods. The @Query annotation states the key that we’ll add to our request, and we can use a standard variable class as the value.

These methods all return a Call object, which represents an HTTP request. You’ll see different names inside of the diamond brackets. Our data structures for each of the responses are different, so we need to create a Java class to represent each response.

Each of these classes will be very simple.

package com.felkertech.n.iot_doorbell_android;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class NewEvent {

@SerializedName("event_id")
@Expose
private Integer eventId;

/**
*
* @return
* The eventId
*/
public Integer getEventId() {
return eventId;
}

/**
*
* @param eventId
* The event_id
*/
public void setEventId(Integer eventId) {
this.eventId = eventId;
}
}

These classes use GSON annotations which help construct these objects.

package com.felkertech.n.iot_doorbell_android;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Event {

@SerializedName("result")
@Expose
private Long result;
@SerializedName("timestamp")
@Expose
private Object timestamp;
@SerializedName("event_id")
@Expose
private Long eventId;

/**
*
* @return
* The result
*/
public Long getResult() {
return result;
}

/**
*
* @param result
* The result
*/
public void setResult(Long result) {
this.result = result;
}

/**
*
* @return
* The timestamp
*/
public Object getTimestamp() {
return timestamp;
}

/**
*
* @param timestamp
* The timestamp
*/
public void setTimestamp(Object timestamp) {
this.timestamp = timestamp;
}

/**
*
* @return
* The eventId
*/
public Long getEventId() {
return eventId;
}

/**
*
* @param eventId
* The event_id
*/
public void setEventId(Long eventId) {
this.eventId = eventId;
}

}
package com.felkertech.n.iot_doorbell_android;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class ResponseEvent {
@SerializedName("resppnse")
@Expose
private Long resppnse;

/**
*
* @return
* The resppnse
*/
public Long getResppnse() {
return resppnse;
}

/**
*
* @param resppnse
* The resppnse
*/
public void setResppnse(Long resppnse) {
this.resppnse = resppnse;
}
}

These data structures are easy to create and modify for other classes in the future.

Back in our MainActivity, we need to create our ServerCommands object so that we can make our different calls.

In the onResume method, add

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://iotdoorbell.herokuapp.com/api/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build();
server = retrofit.create(ServerCommands.class);

We create a Retrofit object with a base url. This url is the same for every one of our API calls, so we can just add it once and it will be applied to all of our server calls.

We then create a new field variable,

private ServerCommands server;

and instantiate it by creating a new ServerCommands object.

When we go to our doorbell click listener, we can now add a call to create a new doorbell event.

doorbell.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
event_id.setText(R.string.searching);
status.setText(getString(R.string.status_is, Constants.STATUS_UNKNOWN));
server.newEvent().enqueue(new Callback<NewEvent>() {
@Override
public void onResponse(Response<NewEvent> response, Retrofit retrofit) {
NewEvent newEvent = response.body();
event_id.setText(newEvent.getEventId()+"");
event_id_int = newEvent.getEventId();
}

@Override
public void onFailure(Throwable t) {

}
});
}
});

We call the `newEvent` method of our server. This gives us a Call object, which is an HTTP request that hasn’t been fulfilled yet. To fulfill it, we call enqueue. This puts it on a queue of requests to execute asynchronously. We want an asynchronous request so our app isn’t waiting on a poor internet connection and prevents us from doing anything else.

Inside of the queue we add a new Callback. Note that the NewEvent object is also present in that diamond.

There are two methods we implement: onResponse and onFailure. If onResponse is called, our request was successful. We can get the body of the response. This body is a NewEvent object. As such, we can use the getEventId method to get our event id. I update the TextView and set a private field variable event_id_int to that value.

We can do the same thing with our three response buttons. Let’s create a single method that can be applied to all three buttons.

public void respond(final int response_code) {
server.respond(event_id_int, response_code).enqueue(new Callback<ResponseEvent>() {
@Override
public void onResponse(Response<ResponseEvent> response, Retrofit retrofit) {
ResponseEvent responseEvent = response.body();
if(responseEvent.getResppnse() == 200) {
status.setText(getString(R.string.status_is, response_code));
}
}

@Override
public void onFailure(Throwable t) {

}
});
}

This takes in a response code and responds to an event whose id we set above.

If the response is successful, and we get a 200, then we update our TextView.

With a single method, it’s easy to add to each of our three response buttons.

busy.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
respond(Constants.STATUS_BUSY);
}
});
free.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
respond(Constants.STATUS_FREE);
}
});
unlocked.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
respond(Constants.STATUS_UNLOCKED);
}
});

Before we test, we need to do one more thing. Go to your manifest.

This manifest is the main file for our Android app. It contains all the activities, sets the app icon, and plenty of other things. Right now we need to add a permission.

The permissions model for Android is very modular. This is meant to prevent malicious apps from running rampant. To do many things, the user will be told up-front about what the app needs to run. If it require contacts, or calendar access, the user is aware of that before installing.

Of course, with Android 6.0 Marshmallow, the permissions model has changed considerably. Now permissions aren’t checked when installing but at runtime. Users can enable or disable specific permissions whenever they want.

Either way, we do need to declare all the permissions our app will use in the manifest. Since we are making API calls to a server, we need to say that we’re using the Internet permission.

<uses-permission android:name="android.permission.INTERNET"/>

Now when you run your app you’ll see that you can request and respond to real doorbell events from a server.

As an aside, we can change some of the components of the manifest in order to target certain devices. In the Google Play Store, only apps that will run on your phone are displayed when searching. The store checks your device against certain hardware or software features that each app requires.

For example, if your app requires the user have a fingerprint reader,

<uses-feature android:name="android.hardware.fingerprint" android:required="true"/>

You specify that feature is required. Then only devices that have a fingerprint sensor will be able to install that app. If you want your app to run a TV, which does not have a touchscreen, you can add this line for a touchscreen and say that it is not required.

If you don’t respond to a doorbell press, it’s possible that you’d never know about it. We can use notifications to inform the user when they miss a doorbell.

package com.felkertech.n.iot_doorbell_android;

import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.v4.app.NotificationCompat;

public class DoorbellNotification {
public static Notification createDoorbellNotification(Context mContext) {
Notification n = new NotificationCompat.Builder(mContext)
.setContentTitle("Doorbell Event")
.setContentText("Doorbell was pressed")
.setColor(Color.CYAN)
.setPriority(Notification.PRIORITY_MAX)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
return n;
}
}

This is a simple class that just creates a notification. We can use the NotificationCompat (short for compatibility) to add a lot of different properties. Some, like color, only exist on higher versions of the platform, but the compatibility classes are designed so you don’t need version-specific code.

In this example, we add a title, body, color, priority, and small icon. A title and small icon are required for the notification to show.

Now we need to generate our notification in the activity and then send it to the notification panel.

doorbell.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
event_id.setText(R.string.searching);
status.setText(getString(R.string.status_is, Constants.STATUS_UNKNOWN));
server.newEvent().enqueue(new Callback<NewEvent>() {
@Override
public void onResponse(Response<NewEvent> response, Retrofit retrofit) {
NewEvent newEvent = response.body();
event_id.setText(newEvent.getEventId()+"");
event_id_int = newEvent.getEventId();
((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
.notify(1, DoorbellNotification.createDoorbellNotification(getApplication()));
}

@Override
public void onFailure(Throwable t) {

}
});
}
});

We can add a new notification in a single line by getting the NotificationManager and sending a new notification:

((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
.notify(1, DoorbellNotification.createDoorbellNotification(getApplication()));

We give the notification an id of 1. More notifications of the same id will overwrite the current notification instead of adding a new notification. If you want to issue several notifications from a single app, each one must have a different id.

So in this session we covered how to import a third-party library, which we used to make API calls to a server. When a call was successful, we generated a notification and were able to display it to the user.

--

--

Nick Felker

Social Media Expert -- Rowan University 2017 -- IoT & Assistant @ Google