Continuous background operation in Android: Services

Prerequisites: Some knowledge of Android activities. Android smartphone (though this one can run in simulator). You can find the gist for this post here.

For some Android applications, there could be tasks that must be performed over long periods of time, whether sporadically (once each couple of hours, at the end of the day) or continuously. For most of these tasks, an Activity might not be required to be shown at all times in the smartphone’s screen. As an example, network transmission for downloading songs (as in Spotify, Deezer, and the like) or continuous sensing can be happening in the background while the user is playing a video game, sending an email, or not interacting with the smartphone at all.

For these cases, Android provides a special component known as Service, which resembles the concept of services in desktop or server operating system: a piece of software that is run in the background and that exists independently of a graphical interface. Think of a service as an element that will be interacting with other running software pieces to carry out a task.

Here, I aim at explaining the different ways you can create services in Android. There is a great guide at https://developer.android.com/guide/components/services, which by the way should be your starting point for getting into the best and updated practices for Android programming (whether Java or Kotlin). Remember that mobile devices, from the perspective of both research and technology, are an evolving field so you will notice improvements in the hardware and software platforms of devices. So, here I provide a different perspective of Android services that might help you to grasp concepts quicker.

Types of services

Although at this moment the Android guide specifies three types of services, I would say there are only two types of services:

  • “Started services”: That is, services that are explicitly started by other components by calling the startService() method. This type of services will be running until the stopMethod() is called. Such method can be called by the service itself or by the caller. It is noteworthy that the service will have an independent life cycle, and it can keep running even if the caller is not running anymore. Note also, that the caller does not have any object reference for the service, so the caller can’t invoke service’s methods. This means that doing service.myCoolMethod() is not possible for started services.
  • Bound services: These services are created when another component “binds” to them. Roughly speaking, this happens by calling the bindService() method from the caller. Now, by default the bound service will be running as long as there are bound “clients” to it. See that I use the word “client”; bound services feels like implementing a client-server paradigm, as the client (caller) will be performing requests to the service (server). This means that the caller has an actual object reference to the service, so a call like service.myCoolMethod() is perfectly valid. Think of this as if you are delegating some heavy processing to the service but at the same time you can obtain the outcome of the processing.

See this figure from the official Android documentation:

On the left there are the started services, and on the right the bound services. All services have a life cycle (just like Activities and other Android components), but the path for services have slight variations depending on their type.

Now, the crazy/interesting part is that… your service can be of the bound or started class at the same time! This is (in my opinion) something good, as you can personalize your service according to your needs. Do you need a service that runs even if your app is not running (started service) but at the same time, whenever you start the app you want to interact with it and consume its services (bound services)? You can! This combination is the one I use when I need to sense data and process it continuously in the background.

Something to notice is that, by default, a Service are run on the same thread of the caller, so depending on the complexity of the task, you might want to start a thread to avoid affecting the caller. If the caller is an Activity you might see that the GUI is unresponsive, and remember the shameful Activity Not Responding message, you definitely don’t want that.

Services in practice

Let’s try to use some very basic services of each type to get our hands dirty. We will create a project in Android Studio using Java. If you want to copy/paste code seamlessly, name the project HelloWorldServices with the package com.s0lver. Let’s specify an empty Activity (I will use 23 as the minimum API, my phone is rather old).

Started services

Let’s start with the started type of services. As services are another basic element in Android programming (remember: Activity, BroadcastReceiver, Service, Intent, ContentProvider, see https://developer.android.com/guide/components/fundamentals), they must be defined on the Android Manifest. So let’s update our AndroidManifest.xml including the name of the service we will be creating.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.s0lver">

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

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

</manifest>

Now, let’s create the MyStartedService, by adding a class named MyStartedService, and we will extend the Service class. This will force us to implement the onBind() method, we will return null here (recall, currently we are exploring the started service type and not the bound service type). Right now, our service looks like this:

package com.s0lver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import androidx.annotation.Nullable;

public class MyStartedService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

And virtually does nothing. Now let’s make our service do something as soon as it is started. We will simply output a string in the logger periodically. Let’s do this as follows:

package com.s0lver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;

import java.util.Timer;
import java.util.TimerTask;

public class MyStartedService extends Service {
    private Timer myTimer;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(this.getClass().getSimpleName(), "onStartCommand()");
        String name = intent.getStringExtra("name");
        doTask(name);

        return START_STICKY; // START_NOT_STICKY
    }

    private void doTask(final String name) {
        myTimer = new Timer();
        myTimer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                Log.i("MyServiceStarted", String.format("Hello %s", name));

            }
        }, 0, 3 * 1000);

        // TODO Use this code rather than previous one to see a Toast shown every 3 seconds.
        //  you might find handler and runnable weirds, ignore them for now, they are needed if
        //  you want to show a Toast from a Service...
        // final Handler handler = new Handler();
        // myTimer.scheduleAtFixedRate(new TimerTask() {
        //     @Override
        //     public void run() {
        //         handler.post(new Runnable() {
        //             @Override
        //             public void run() {
        //                 Toast.makeText(MyStartedService.this, String.format("Hello %s", name), Toast.LENGTH_SHORT).show();
        //                 Log.i("Timer", String.format("Hello %s", name));
        //            }
        //         });
        //     }
        // }, 0, 3 * 1000);
    }

    @Override
    public void onCreate() {
        Log.i(this.getClass().getSimpleName(), "onCreate()");
    }


    @Override
    public void onDestroy() {
        Log.i(this.getClass().getSimpleName(), "onDestroyService()");
        myTimer.cancel();
        super.onDestroy();
    }
}

I also included some Logging for the life cycle of this service. See the onStartCommand() method, it is invoked when the service is started. It will extract a parameter from the intent that just was received and will call a dummy method that will run a TimerTask each 3 seconds, outputting the name in the Logcat. See the return value for the onStartCommand(), it refers to how would Android treat the service if it needs to be killed (sticky means the service would be restarted, so you should prepare your code for such restarting). To see the service running, let’s update the code of our empty Activity as follows:

package com.s0lver;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private Intent serviceIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        serviceIntent = new Intent(this, MyStartedService.class);
        serviceIntent.putExtra("name", "Rafael");
        startService(serviceIntent);
    }

    @Override
    protected void onDestroy() {
        Log.i(this.getClass().getSimpleName(), "onDestroy()");
        stopService(serviceIntent);
        super.onDestroy();
    }
}

See how we call startService() and stopService() using an Intent that links the Activity with the class type of our service. Think of an Intent as the message that travels within Android components and that enables to perform tasks (Intents have types, or identifiers for that matter; here the id/type is our class, but there are other types like Intent.ACTION_CALL, MediaStore.ACTION_IMAGE_CAPTURE, etc.).

Now, run the app. You will see in the Logcat that messages are displayed and you will see

04-19 16:01:41.126 14652-14652/com.s0lver I/MyStartedService: onCreate()
04-19 16:01:41.126 14652-14652/com.s0lver I/MyStartedService: onStartCommand()
04-19 16:01:41.128 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael
04-19 16:01:44.127 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael
04-19 16:01:47.127 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael
04-19 16:01:50.128 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael
04-19 16:01:53.127 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael
04-19 16:01:56.127.e., top 14652-14694/com.s0lver I/MyServiceStarted: Hello Rafael

If you navigate out of the app (don’t close it, i.e., don’t select back on the Activity) by pressing your phone home menu and doing something in another app, you will see that messages are still displayed. This means your service is still active regardless of your app life cycle: remember, your service will be active even if the app is not. Navigate back to your app (using the overview button, i.e., the most right button) and now end it using the back button. You will see in the Logcat the onDestroy() messages for the Activity and the Service.

Bound Services

Now let’s have a quick look to the bound services. The Android guide currently states three ways to bind to a service: extending the Binder class, using a Messenger, or using Android Interface Definition Language (AIDL). Here we will cover the first way, as it allows to obtain a plain reference to the service and invoke its methods. So let’s add a new class for the bound service as follows:

package com.s0lver;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;

public class MyBoundService extends Service {
    public IBinder theBinder = new TheBinder();

    public class TheBinder extends Binder {
        MyBoundService getService() {
            return MyBoundService.this;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(this.getClass().getSimpleName(), "onBind()");
        return theBinder;
    }

    public double sum(double a, double b) {
        return a + b;
    }

    public double divide(double a, double b) {
        return a / b;
    }

    @Override
    public void onDestroy() {
        Log.i(this.getClass().getSimpleName(), "onDestroy()");
        super.onDestroy();
    }
}

Don’t forget to add MyBoundService to the AndroidManifest.xml file. Now, have a look to the TheBinder class, which extends the Binder class and will be providing the actual reference to our bound service. See the onBind() service, which is invoked whenever a client binds to the service. Finally see the sample add and divide methods, which help to demonstrate how we can explicitly call public methods of our service.

In order to use the sum and divide methods of our service, let’s update the GUI (activity_main.xml) of our Activity as follows (this will create two buttons):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnSum"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickSum"
        android:text="Sum"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnDivide"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickDivide"
        android:text="Divide"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now, let’s see the changes in MainActivity, which will be the caller (client) of the bound service:

package com.s0lver;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Random;

public class MainActivity extends AppCompatActivity {
    private MyBoundService myBoundService;
    private Random random;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        random = new Random(0);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, MyBoundService.class);
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }

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

    private ServiceConnection connection = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName className,
                                       IBinder service) {
            MyBoundService.TheBinder binder = (MyBoundService.TheBinder) service;
            myBoundService = binder.getService();
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
        }
    };

    public void onClickSum(View view) {
        double a = random.nextDouble();
        double b = random.nextDouble();
        double sum = myBoundService.sum(a, b);
        Toast.makeText(this, String.format("%s + %s = %s", a, b, sum), Toast.LENGTH_SHORT).show();
    }

    public void onClickDivide(View view) {
        double a = random.nextDouble();
        double b = random.nextDouble();
        double division = myBoundService.divide(a, b);
        Toast.makeText(this, String.format("%s / %s = %s", a, b, division), Toast.LENGTH_SHORT).show();
    }
}

Notice the onStart() method of this Activity, the call to bind to the service is there. Such call will require a connection ServiceConnection parameter, which is a field of our Activity. In this connection object we have two callbacks, one for when we are bound to the service and one for when we unbind. For the first callback, see that we can extract an object reference to our service from the TheBinder object.

From here, we are free to use a reference to the bound service as if it was a regular Java object. See how the onClickSum() and onClickDivide() are consuming this “API” provided by the service.

Finally, see how we request the unbinding from the service on the onDestroy() method of our Activity. This means that the service will be alive as long as we don’t finish the Activity. Make the same exercise as previously, navigate out of the activity (don’t close it) and you won’t see the onDestroy() message in the Logcat. Go back to the app and now actually close it and you will see such message. The message appears as our activity was the only client bound to the service, and according to the documentation, a bound service is automatically destroyed when all clients unbind from it.

Issues with services

If the tasks to be executed by services are short or with a low frequency, you shouldn’t have any issue or being worried about anything else. However, if your application requires consistent execution of tasks during long periods of time some complications might appear. You must remember that mobile development targets devices with computing (memory) and energy restrictions. Because of this, Android uses some aggressive energy savings methods that can impact on services’ execution. As a consequence, services can be stopped or paused and then relaunched by Android under certain conditions like low memory or idleness. You have two options here:

  1. You implement your service and elements in such a way they are ready to be paused and then reactivated by Android. You should look into the life cycle of a service to know which methods will capture the re-launching of your service to continue its processing. This is similar to handling the life cycle of an Activity, which can be paused and restarted when the user navigates through apps, after receiving a phone call, etc.
  2. Let Android know that your service should be treated with high priority, in such a way it is less likely to be stopped-restarted. Be mindful here, if your service is not performing something vital for your app, you definitively should go with option 1, so Android will make the best of the smartphone’s resources; in other words, don’t go with option 2 just to avoid the work of handling the service’s life cycle: be responsible and aware of smartphone’s constrained resources. You let a service receive this treatment by setting it as a foreground service

One note on the foreground service concept. Android documentation states that there are background, foreground and bound services. Here I just want to clarify that when a service is said to be of background type (yeah, background service is a pleonasm) it means the user is completely unaware of its execution. Instead, in a foreground service the user will know there is something being executed because a notification will be shown in the taskbar.

In fact, the process to mark the service as a foreground service involves putting a notification for the user. As stated, the idea behind this is that the user will know that something is being executed, so Android will not touch such operation. If you have used Spotify, you will see that when you are playing a song, a notification is always shown and you can not swipe it, so Android will never interrupt the song playing.

The following code would mark the StartedService as a foreground service:

package com.s0lver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class MyStartedService extends Service {
    private Timer myTimer;
    private static final int ID = new Random().nextInt(99999);

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(this.getClass().getSimpleName(), "onStartCommand()");
        String name = intent.getStringExtra("name");
        doTask(name);

        NotificationCompat.Builder b = new NotificationCompat.Builder(this, "channel_id_value");
        b.setSmallIcon(R.drawable.ic_launcher_background)
                .setContentTitle("MyStartedService")
                .setContentText("MyStartedService has been, well, started...");
        startForeground(ID, b.build());

        return START_STICKY; // START_NOT_STICKY
    }

    private void doTask(final String name) {
        myTimer = new Timer();
        myTimer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                Log.i("MyServiceStarted", String.format("Hello %s", name));

            }
        }, 0, 3 * 1000);
    }

    @Override
    public void onCreate() {
        Log.i(this.getClass().getSimpleName(), "onCreate()");
    }


    @Override
    public void onDestroy() {
        Log.i(this.getClass().getSimpleName(), "onDestroyService()");
        myTimer.cancel();
        stopForeground(true);
        super.onDestroy();
    }
}

Replace the code on MainActivity with the corresponding version for the StartedService interaction. You can keep the GUI. Now, when you run the app you will see that the notification is shown and you can’t remove it from the taskbar as the service is in foreground and users must always know that there is something running permanently there.

When you want to stop a foreground service, you should call stopForeground(). From this moment, Android can treat your service as a regular one.

Using a foreground service in combination with a started + bound service will allow you to have a service running indefinitely, not killed by Android, and with which you can interact through an API that you can freely define. Think of the possibilities you have by having an app running all the time (for research on smartphone-based sensing this is a must, and probably one of the few cases you would want to do something like this), but also think of the impact on resources your app will be producing.

Summary

We have presented the concepts on Android Services here. Android is an evolving platform, so be sure to check the official documentation as more elements are added that make it simpler to develop your apps. The lesson about services is to evaluate your requirements and then select the type/configuration of services you need. Hopefully, by reading this short tutorial now you have the basic knowledge to go and investigate deeper on the Android documentation and develop something exciting!

You can find the gist for this post here.

[I hope soon to document a code for continuous GPS tracking, which was the key concept in my PhD thesis.]

A not-so-short tutorial on inertial sensors sampling in Android

Prerequisites: A running installation of Android Studio or JetBrains IntelliJ configured with the Android SDK. An Android phone for the actual sampling.

A terrible introduction

There are multiple reasons why mobile devices like smartphones are so popular these days. In my opinion the following aspects can explain such popularity:

  • Technological advances on hardware:
    • Computing: smaller, faster and more energy efficient processors. Even now we have “neuro-processors” tailored for on-device machine learning as in newest Google and iPhone phones for face recognition, as well as FPGAs! to reconfigure the operation of SoCs (Systems on a Chip) after devices have been already deployed.
    • Communication: Faster and more energy efficient transmission devices for 4G and now 5G, which leave the problems of video stalling in the past (unless you reach the data cap…).
    • Sensing: A plethora of embedded sensors that enable smartphones with environmental information and the possibility to become “context-aware”.
  • Technological advances on software platforms:
    • With robust and flexible Operating Systems that (mostly) make accessible the hardware functionalities to build mobile apps that go beyond simple internet connectivity and information retrieval, creating a paradigm that goes beyond desktop applications programming.
    • An ever increasing market with official ways to reach users and deploy created mobile apps. This helps to sanitize or filter applications, bringing the user only high-quality software pieces (ideally).

So, nowadays mobile devices are the ideal artifacts to obtain first-hand information from users and to establish a prompt notification (feedback) channel with them. As sensors are embedded on devices, we now have the possibility to gather “physical” data that promote the development of mobile health, location tracking, activity recognition, voice commands, and many more services.

Here, I provide a very short tutorial for the collection of accelerometer or other inertial sensors data. Accelerometer data has been widely studied to detect the transportation mode employed by users (i.e., walking, cycling, vehicle, running), their activity (cooking, brushing teeth, etc.) and even to detect health-risks, like Parkinson’s disease, elderly people falls, and more.

I think this basic tutorial could be of help for researchers or new mobile developers to know how to develop basic insights for data collection, as sometimes datasets are not available or because the problem at hand requires specific data collection requirements (sampling frequency, duty cycle, etc.). Please note that although here I cover accelerometer data collection, this procedure also applies for other inertial sensors like gyroscope, and environment sensors like light, pressure, compass and more.

Delimiting our basic app

A good idea is to state what are the objectives of our Android sample app. In short, we will create an app that will:

  • Collect accelerometer samples for as long the user wants (by clicking on a button to stop sampling).
  • Store the collected data into a file.
  • Introduce a small delay before starting readings (optional). This will help users to click a button and have time to put the accelerometer on their pockets, purse, etc.

Notice that here we are talking about short data collection periods (seconds, a couple of minutes maybe), longer intervals will certainly require more dirty Android tricks (but of course it is possible). Finally, the architecture of this app is just a starting point, feel free to optimize it, perform refactoring, encapsulate behavior, make new classes, etc.

Creating the project

Open Android Studio and create a project with an empty activity, we will only need one. I left the default name MainActivity. If you want to copy and paste code seamlessly, you can use the package name com.s0lver.samplingtutorial.simpleaccelerometersampling.

A shameful GUI

You will excuse my lack of design abilities, we will only prepare an Android Activity with two buttons, one for starting and another to stopping accelerometer sampling. So this would be the layout for this activity (activity_main.xml if you left the default name).

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnStartSampling"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickStartSampling"
        android:text="Start sampling"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnStopSampling"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickStopSampling"
        android:text="Stop sampling"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Pretty much everything here (except for some layout constraints) is self-explanatory. However have a look at highlighted lines, they represent methods in our apps that will be called (callback) when a click event on those buttons is generated. This is the very first time you might have heard the word event; in Android (or mobile platforms) almost everything is triggered by an event, so you will be dealing a lot with event-driven programming. In fact, access to sensors is also event-driven, so from now you can imagine that you will need to provide a method that will be called whenever sensors produce some data, more details on this in next sections.

Initial app code

Now lets attach the GUI with our application. If you are very new to Android, in this first step we will just produce some Toast (not joking, it is a small notification) when we click a button, but it will serve you to understand how the GUI and app code are linked, and when are the events generated and processed.

So you can simply paste this code on the MainActivity.java file.

package com.s0lver.samplingtutorial.simpleaccelerometersampling;

import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onClickStartSampling(View view) {
        Toast.makeText(this, "Clicked on start sampling", Toast.LENGTH_SHORT).show();
    }

    public void onClickStopSampling(View view) {
        Toast.makeText(this, "Clicked on stop sampling", Toast.LENGTH_SHORT).show();
    }
}

Some details on the activity (for beginners)

Here, you can see the methods that link the buttons with our app’s code. Maybe you have already seen some apps generating Toasts. Now you know how they are created. Take some time to understand what is going on with this Activity. In fact, if you are a complete beginner, you might want to review the life cycle of Android activities.

At first, there is no Activity running, and the Android virtual machine should look in the AndroidManifest.xml file for the Activity that has the intent android.intent.action.MAIN in its intent-filter section, as such activity is the one to be loaded and shown initially to the users. Verify in the manifest file that the MainActivity is the only one, and the one with such intent value. When the activity is identified, then it will execute a number of methods in sequence. Suffice to say by now, is that the onCreate() method is only invoked once, and it is the preferred point to load the GUI and make some preparations for your app (preparations that are done only once, remember).

Connecting the app with the hardware

Now we can deal with connecting our app with the hardware (sensors). Remember, Android is after all an operating system, so it must have defined somewhere how to access the hardware facilities of the device, maybe as very low level API calls, or as high level services. In the case of Android, you have high level services to register to and access a wide range of hardware functionalities: recording sound data from microphone, collecting intertial sensors samples, accessing location information, sending information via Wi-Fi, etc.

We will be using a separate Java class to isolate all hardware access from our MainActivity class. It is a good idea to separate pieces of code according to their scope. The system service we will use is SensorManager. You can use the same system service to access all other sensors in the device (GPS is an exception though, it uses LOCATION_SERVICE instead). The following snippet shows what we should have by now on our new SensorAccess class (SensorAccess.java):

package com.s0lver.samplingtutorial.simpleaccelerometersampling;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;

public class SensorAccess {
    private final Context context;
    private final SensorManager sensorManager;
    private final Sensor accelerometer;

    public SensorAccess(Context context) {
        this.context = context;
        this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }
}

Notice line 15 (highlighted). Here we have access to the sensor manager (service) and even access to our sensor. However, we are not reading anything yet. To do so, we need to provide a listener (callback), which we will register with the sensor manager to receive event updates about a sensor, in this case, the accelerometer. So, we need first to create a class that defines that listener mechanism. The following class will be our first attempt to receive accelerometer event notifications:

class AccelerometerListener implements SensorEventListener {
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            float[] acc = sensorEvent.values;
            long timestampMilliseconds = sensorEvent.timestamp / 1000;
            Log.i("SensorAccess", String.format("ACC %s,%s,%s @%s", acc[0], acc[1], acc[2], timestampMilliseconds));
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {

        }
    }

Notice that the callback onSensorChanged will be the one invoked whenever the sensor produces data. At the moment we just put the received accelerometer data on the Android Log. In the following updated SensorAccess class we will see how we can register and unregister the listener class, which will start or stop the sampling, respectively.

package com.s0lver.samplingtutorial.simpleaccelerometersampling;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;

public class SensorAccess {
    private final Context context;
    private final SensorManager sensorManager;
    private final Sensor accelerometer;
    private final AccelerometerListener accelerometerListener;

    public SensorAccess(Context context) {
        this.context = context;
        this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        this.accelerometerListener = new AccelerometerListener();
    }

    public void startSampling() {
        this.sensorManager.registerListener(this.accelerometerListener,
                this.accelerometer,
                SensorManager.SENSOR_DELAY_NORMAL);
    }

    public void stopSampling() {
        this.sensorManager.unregisterListener(this.accelerometerListener);
    }

    class AccelerometerListener implements SensorEventListener {
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            float[] acc = sensorEvent.values;
            long timestampMilliseconds = sensorEvent.timestamp / 1000;
            Log.i("SensorAccess", String.format("ACC %s,%s,%s @%s", acc[0], acc[1], acc[2], timestampMilliseconds));
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {

        }
    }

}

Check the methods startSampling() and stopSampling(), which will register and unregister the listener. Notice the third parameter for the registerListener call (line 24), which refers to a sampling frequency. Depending to the sensor at hand, there could be different values for this. Android provides support for other values here (like SENSOR_DELAY_FASTEST, SENSOR_DELAY_UI, SENSOR_DELAY_GAME, etc). However, the actual frequency will depend on the manufacturer’s implementation.

Linking the MainActivity code with SensorAccess class

Up to here we have our sampling “backend” almost ready. Lets use its functionality from the MainActivity code. We can do this by simply creating an instance of SensorAccess and calling the appropriate methods when we click one of the buttons. Let’s update the main activity’s code as follows:

package com.s0lver.samplingtutorial.simpleaccelerometersampling;

import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private SensorAccess sensorAccess;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.sensorAccess = new SensorAccess(this);
    }

    public void onClickStartSampling(View view) {
        sensorAccess.startSampling();
    }

    public void onClickStopSampling(View view) {
        sensorAccess.stopSampling();
    }
}

If you run your app now, you will see on the “Run” window of Android Studio some output as follows:

I/SensorAccess: ACC 4.951209,7.680599,3.2848446 @1632445224306
I/SensorAccess: ACC 4.7309427,7.6614456,3.1316159 @1632445234057
I/SensorAccess: ACC 4.596867,7.5944076,2.9592333 @1632445243802
I/SensorAccess: ACC 4.6160207,7.6231384,2.825158 @1632445253566
I/SensorAccess: ACC 5.104438,7.7572136,2.0494366 @1632445263312
I/SensorAccess: ACC 5.793968,7.642292,1.8674773 @1632445273060
I/SensorAccess: ACC 5.592855,7.632715,2.432509 @1632445282812
I/SensorAccess: ACC 5.2768207,7.6231384,2.4420857 @1632445292565
I/SensorAccess: ACC 5.171476,7.651869,2.777274 @1632445302324
...

Pretty nice! We achieved objective number one: collecting data from accelerometer.

Storing the records into a file is not a hard task once that you have the actual information. As this is simple, let’s try to work a little bit on the third goal: introducing a delay.

Introducing a small delay before sampling

Let’s try to delay the start of accelerometer sampling once we clicked the start sampling button. This can help the user to get ready to put the smartphone in the correct position or to fulfill other data collection requirements. There are many ways to introduce delay, but here we will use a combination of Timer and TimerTask objects. Think of a Timer as that, a tool that will notify you that some time has elapsed and TimerTask will the something (a task) that can be executed when the timer goes off.

Although the previously mentioned might sound intimidating, it is ridiculously easy to be implemented. Going back to the SensorAccess class you should only update the startSampling() method as follows:

    public void startSampling() {
        long delayInMilliseconds = 5 * 1000;
        Log.i("SensorAccess", String.format("I will start reading in %s seconds", delayInMilliseconds / 1000));
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                sensorManager.registerListener(
                        accelerometerListener,
                        accelerometer,
                        SensorManager.SENSOR_DELAY_FASTEST);
            }
        }, delayInMilliseconds);
    }

As you can see, the delayInMilliseconds variable will help you to adjust the delay time. Maybe you want to let it be user-specified, or maybe you don’t want a delay at all (but if that is the case, I won’t recommend setting delay as 0, you better delete the Time and TimerTask to reduce complexity.

If you run your app with the updated code, you will see how the sampling starts after the specified delay.

Saving accelerometer data to a file

So let’s go back to the saving-data-in-a-file objective. To make the handling of each accelerometer sample as a proper entity, lets introduce a helper class called AccelerometerSample, which will comprise the values of x, y, and z axis, as well as the timestamp.

class AccelerometerSample {
        float x, y, z;
        long timestampMilliseconds;

        AccelerometerSample(float x, float y, float z, long timestampMilliseconds) {
            this.x = x;
            this.y = y;
            this.z = z;
            this.timestampMilliseconds = timestampMilliseconds;
        }

        @NonNull
        @Override
        public String toString() {
            return String.format("%s,%s,%s,%s", x, y, z, timestampMilliseconds);
        }
    }

yep, this looks ok. So now after each sensor event is generated, we will create an AccelerometerSample object, and we will add it to a list of them. The idea is that when the user clicks on the stop sampling button, we simply write everything on the list to a file, in a CSV (comma separated value) format. CSV is very popular, if you work in Python you can find support within it or also in Pandas, etc.

So, we can create handle the file writing in a new class (you know, to keep code separated by functionality). Let’s name this class AccelerometerSamplesWriter:

package com.s0lver.samplingtutorial.simpleaccelerometersampling;

import android.os.Environment;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

public class AccelerometerSamplesWriter {
    private final String fileName;

    public AccelerometerSamplesWriter(String fileName) {
        this.fileName = fileName;
    }

    public void writeSamples(List<SensorAccess.AccelerometerSample> samples) {
        String filepath = Environment.getExternalStorageDirectory().toString() + "/" + fileName;
        try {
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(filepath, false)));
            pw.println("x,y,z,t");
            for (SensorAccess.AccelerometerSample sample : samples) {
                pw.println(sample.toString());
            }
            pw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

A couple of details here. See the call to Environment.getExternalStorageDirectory().toString(); this represents the external storage of your device, typically an sdcard, which will be the place were your file will be saved. Also notice that we add a header, that includes simply x, y, z, and t (for the timestamp). This will help you to load your file in Pandas by having the column names ready.

To use this new functionality, lets update our SensorAccess by defining a new attribute and modifying the constructor as follows:

private List<AccelerometerSample> samples;

    public SensorAccess(Context context) {
        this.context = context;
        this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        this.accelerometerListener = new AccelerometerListener();
    }

We have to initialize that samples list whenever we start sampling, so we update the startSampling() as follows:

public void startSampling() {
        samples = new ArrayList<>();
        long delayInMilliseconds = 5 * 1000;
        Log.i("SensorAccess", String.format("I will start reading in %s seconds", delayInMilliseconds / 1000));
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                sensorManager.registerListener(
                        accelerometerListener,
                        accelerometer,
                        SensorManager.SENSOR_DELAY_FASTEST);
            }
        }, delayInMilliseconds);
    }

Also, lets update our listener, as it should use the samples list now:

class AccelerometerListener implements SensorEventListener {
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            float[] acc = sensorEvent.values;
            long timestampMilliseconds = sensorEvent.timestamp / 1000;
            // Log.i("SensorAccess", String.format("ACC %s,%s,%s @%s", acc[0], acc[1], acc[2], timestampMilliseconds));
            samples.add(new AccelerometerSample(acc[0], acc[1], acc[2], timestampMilliseconds));
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {

        }
    }

So, now we have this list, we initialize it when we start sampling, and we add a new record whenever an event is produced. Now we have to write it to a file when we stop sampling, so lets update the stopSampling() method of our SensorAccess class:

public void stopSampling() {
        this.sensorManager.unregisterListener(this.accelerometerListener);
        AccelerometerSamplesWriter writer = new AccelerometerSamplesWriter("samples.csv");
        writer.writeSamples(samples);
    }

See how there we create the samples writer with the filename samples.csv. You can update it, maybe attach the current time to avoid rewriting the file, or better yet, asking the user through an UI widget for the filename.

Before running your app, notice that Android requires permission to access the file system of the smart device. You have to register this permission in the Android manifest file, so you can add the line <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> Your AndroidManifest.xml file should now look as:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.s0lver.samplingtutorial.simpleaccelerometersampling">

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

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

</manifest>

However, this is not enough for your app to actually store data. In fact you have to ask dynamically the user for such permission, as now users can selectively turn off permissions that you required in the manifest (you can further investigate on the issue, which for security issues totally makes sense). To avoid further extending this post with more code, you can do the following:

  • Run / deploy the app from Android Studio.
  • Before start sampling, go to Android System settings and select apps.
  • Navigate to your app (if you did not change the name, it should be SimpleAccelerometerSampling).
  • Click on Permissions and activate the Storage permission.

Now go back to your app (or relaunch it from Android Studio or from your phone home menu) and start sampling for some seconds and then stop. In the Device File Explorer window of Android Studio you can navigate to the sdcard and see that the samples.csv file is there!

Recap

Thats all from me. In this quick tutorial you have learned a lot about sensor management, data collection, and file writing. You even managed to include a nice feature of sampling delay to let the user position the device properly. In this gist I include the whole source code of the tutorial, including some simple validation (you know, to avoid the user clicking twice on the start sampling button and stuff like that).

You can work on the structure of the classes in this project, which can be greatly improved. You can also add features, for instance removing the last n seconds of samplings before saving the file, as it will delete the movement caused by the user when clicking on the stop sampling button. Perhaps you want to add a GUI to customize all values, or you might even want to delete the stop sampling button and just capture for a number of seconds and stopping readings automatically (hint: you can use Timer and TimerTask objects again).

Later on, I hope to prepare a tutorial for location sampling and for long-sampling of location and sensors, as it could be handy to have more data (from weeks or months) to make interesting research.

Complete source code: public gist here.