We have previously demonstrated how to use a web API from your Android app, and we have also discussed how to get and use location data in your Android app. In this tutorial, we will¬†utilize both of these lessons, and show how you can track a device’s location using a remote web API. The remote web API we selected is the Thingspeak API.

aa_thingspeak_logo

For this article, we will develop an Android application that gets the device’s location information, and sends the latitude/longitude data to a ThingSpeak channel. We will then fetch the last location data from the ThingSpeak channel, and compute the distance from the last updated location to a location entered by the user. A simple (and obvious) use of this is parents who wish to track their children, or employers tracking employees. The ThingSpeak channel can also be viewed using a browser.

Getting Started

First thing you should do is to register for a ThingSpeak account. It is, at the time of writing, completely free. After registration, you will need to create a channel. You can have multiple channels, but each channel has a unique ID, and a special Channel write API_KEY, that can be used to write or read data to the channel. For a private channel, you can generate read API_KEYs, that can only read data from the channel.

While creating your channel, you might notice a Latitude and Longitude field. These are used for static devices, and are not suitable for our use case. ThingSpeak allows for the tracking of up to eight different fields, named field1 through field8. We add latitude as field1, and longitude as field2. You can access the ThingSpeak documentation online on their Support page

aa_thingspeak_fields

App Layout

aa_thingspeak_layout

Our app layout displays the current device latitude and longitude. Beneath these is a button, enabling us begin and pause location tracking. Whenever the app receives a location update, the values of the TextViews are updated, and the new location data is immediately sent to the ThingSpeak channel. There are two EditTexts, into which the user can enter latitude and longitude values to compare against the device’s location. Clicking on the “Get Distance” button will fetch the last updated location from the thingspeak channel, and perform a Location.distanceTo() to compute the distance between both Locations.
Our activity_main.xml is a simple RelativeLayout.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/latitudeText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/latitude"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/latitudeUpdate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/latitudeText"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:text="0.00"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/longitudeText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/latitudeText"
        android:text="@string/longitude"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/longitudeUpdate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/longitudeText"
        android:layout_below="@id/latitudeUpdate"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:text="0.00"
        android:textSize="20sp"/>

    <Button
        android:id="@+id/locationController"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/longitudeUpdate"
        android:text="@string/resume"
        android:onClick="toggleLocationUpdates" />

    <EditText
        android:id="@+id/latitudeTarget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/locationController"
        android:hint="@string/target_latitude"/>

    <EditText
        android:id="@+id/longitudeTarget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/latitudeTarget"
        android:hint="@string/target_longitude"/>

    <Button
        android:id="@+id/notificationController"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/longitudeTarget"
        android:layout_centerHorizontal="true"
        android:text="@string/get_distance"
        android:onClick="compareLocation" />

    <TextView
        android:id="@+id/distanceText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/notificationController"
        android:gravity="center"/>

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/notificationController"
        android:layout_centerHorizontal="true"
        android:visibility="gone"/>

</RelativeLayout>

Updating your channel

Recall our previous article on fetching location data? For this app, we are using the GPS_PROVIDER. If your application runs in the background (as a service), consider using the PASSIVE_PROVIDER, or use a longer refresh time.

In the code snippet below, when the “Begin Tracking” button is clicked, we request location updates from the GPS_PROVIDER every two minutes (2 * 60 * 1000). However, if the button reads “Pause Tracking”, we ask the LocationManager to stop sending updates.

    public void toggleLocationUpdates(View view) {
        if(!checkLocation())
            return;

        Button button = (Button) view;
        if(button.getText().equals(getResources().getString(R.string.pause))) {
            locationManager.removeUpdates(locationListener);
            button.setText(R.string.resume);
        }
        else {
            locationManager.requestLocationUpdates(
                    LocationManager.GPS_PROVIDER, 2 * 60 * 1000, 10, locationListener);
            button.setText(R.string.pause);
        }
    }

This snippet is our implementation of the LocationListener interface. When we receive a new location update, the onLocationChanged() method is called, with the new Location information as the method arguments. On extracting the latitude and longitude values, we execute the AsyncTask that updates the thingspeak API.

    private final LocationListener locationListener = new LocationListener() {
        public void onLocationChanged(final Location location) {
            longitude = location.getLongitude();
            latitude = location.getLatitude();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    longitudeUpdate.setText(longitude + "");
                    latitudeUpdate.setText(latitude + "");
                }
            });

            new UpdateThingspeakTask().execute();
        }
    };

Android prevents developers from performing potentially long running tasks in the UI thread (the thread that controls the responsiveness of your application’s widgets). As such, we must perform network tasks in another thread, and the AsyncTask is a great class that handles all the thread complexity behind the scenes. Instructions in both onPreExecute() and onPostExecute() are run on the UI thread before and after the long running task respectively, while the long running task is placed in the doInBackground() method.

    class UpdateThingspeakTask extends AsyncTask<Void, Void, String> {

        private Exception exception;

        protected void onPreExecute() {
        }

        protected String doInBackground(Void... urls) {
            try {
                URL url = new URL(THINGSPEAK_UPDATE_URL + THINGSPEAK_API_KEY_STRING + "=" +
                        THINGSPEAK_API_KEY + "&" + THINGSPEAK_FIELD1 + "=" + latitude +
                        "&" + THINGSPEAK_FIELD2 + "=" + longitude);
                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                try {
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    StringBuilder stringBuilder = new StringBuilder();
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        stringBuilder.append(line).append("\n");
                    }
                    bufferedReader.close();
                    return stringBuilder.toString();
                }
                finally{
                    urlConnection.disconnect();
                }
            }
            catch(Exception e) {
                Log.e("ERROR", e.getMessage(), e);
                return null;
            }
        }

        protected void onPostExecute(String response) {
            // We completely ignore the response
            // Ideally we should confirm that our update was successful
        }
    }

Fetching channel data

There are many different ways data can be read from a ThingSpeak channel. This includes

  • The entire channel feed
  • Individual fields
  • A specific update
  • Updates within a given time frame and more

Consult the documentation to learn more.

We, however, are interested in retrieving the last update only. This is implemented using an AsyncTask.

In the onPreExecute() method, we fetch the latitude/longitude values, disable further editing of the fields, and display the ProgressBar.

In the onPostExecute() method, we parse the received to a JSONObject, fetch the returned latitude/longitude values, and finally compute the distance between both locations.

    class FetchThingspeakTask extends AsyncTask<Void, Void, String> {

        Location target;

        protected void onPreExecute() {
            double latitude = Double.parseDouble(latitudeEdit.getText().toString());
            double longitude = Double.parseDouble(longitudeEdit.getText().toString());
            target = new Location("");
            target.setLatitude(latitude);
            target.setLongitude(longitude);

            latitudeEdit.setEnabled(false);
            longitudeEdit.setEnabled(false);
            distanceText.setText("");
            distanceText.setVisibility(View.GONE);
            progressBar.setVisibility(View.VISIBLE);
        }

        protected String doInBackground(Void... urls) {
            try {
                URL url = new URL(THINGSPEAK_CHANNEL_URL + THINGSPEAK_CHANNEL_ID +
                        THINGSPEAK_FEEDS_LAST + THINGSPEAK_API_KEY_STRING + "=" +
                        THINGSPEAK_API_KEY + "");
                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                try {
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    StringBuilder stringBuilder = new StringBuilder();
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        stringBuilder.append(line).append("\n");
                    }
                    bufferedReader.close();
                    return stringBuilder.toString();
                }
                finally{
                    urlConnection.disconnect();
                }
            }
            catch(Exception e) {
                Log.e("ERROR", e.getMessage(), e);
                return null;
            }
        }

        protected void onPostExecute(String response) {
            if(response == null) {
                Toast.makeText(MainActivity.this, "There was an error", Toast.LENGTH_SHORT).show();
                return;
            }
            latitudeEdit.setEnabled(true);
            longitudeEdit.setEnabled(true);

            distanceText.setVisibility(View.VISIBLE);
            progressBar.setVisibility(View.GONE);

            try {
                JSONObject channel = (JSONObject) new JSONTokener(response).nextValue();
                double latitude = channel.getDouble(THINGSPEAK_FIELD1);
                double longitude = channel.getDouble(THINGSPEAK_FIELD2);
                Location location = new Location("");
                location.setLatitude(latitude);
                location.setLongitude(longitude);
                float distance = location.distanceTo(target);
                distanceText.setText("The distance between both Locations is \n" +
                        distance + " meters");
                Log.e(TAG, "distance == " + distance);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

aa_thingspeak_newyork

Round up

The complete source is available on Github, for use as you see fit. If building from scratch, you would need to request for both the INTERNET and LOCATION permissions by adding the following to your AndroidManifest

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

Using the GPS_PROVIDER is a battery hog, so if your app is going to run in the background, consider using the PASSIVE_PROVIDER to reduce battery usage.

aa_thingspeak_high_battery

The ThingSpeak API allows channel updates every 15 seconds, and fields can contain either numeric data or alphanumeric strings. There are tons of interesting public channels, including a hamster on Twitter. Have you used ThingSpeak or a similar service? What was your experience? What was/is your use case? Share in the comments below.


Android Developer Newsletter

Do you want to know more? Subscribe to our Android Developer Newsletter. Just type in your email address below to get all the top developer news, tips & links once a week in your inbox:

PS. No spam, ever. Your email address will only ever be used for Android Dev Weekly.


Comments
Read comments