Flutter Fall Event

Join 20,000+ other developers enjoying 3 months of free access to Flutter Apprentice, courtesy of the Flutter team at Google !

Home Android & Kotlin Tutorials

Activity Recognition API Tutorial for Android: Getting Started

Learn to track your activities in your Android app by creating a fitness app that uses the Activity Recognition API.

5/5 2 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

Activity tracking is nothing new in the world of apps. While Google created an Activity Recognition API a long time ago, they recently improved its performance with a series of APIs, grouped into the ActivityRecognitionClient.

The APIs contained within ActivityRecognitionClient include:

  1. Activity Recognition Transition API for tracking transitions between activities.
  2. Activity Recognition Sampling API to get more frequent results for potential activities.
  3. Sleep API for detecting a user’s sleep time.

In this tutorial, you’ll learn how to use the Transition and Sampling APIs by creating Pet Buddy, an app that tracks physical activity changes.

Note: This tutorial assumes you know the basics of Android development with Kotlin. If you’re new to Kotlin, check out our Kotlin introduction tutorial. If you’re completely new to Android development, read through our Beginning Android Development tutorials to familiarize yourself with the basics.

Why Use the Activity Recognition Client

Mobile devices have become an indispensable part of life. Your users have their phones with them all day. That helps your app become smarter and better at helping your users in their daily routines.

The Activity Recognition Client collects data from the sensors available in mobile devices. Those sensors help your app better understand a user.

Improved accuracy, lower battery consumption and enhanced engineering productivity are some of the Transition API’s benefits. It tells the app when a user starts or stops an activity, whether it be walking, running or riding in a vehicle.

For more frequent requests or raw activity data, you’ll use the Sampling API. Of course, more frequent requests mean more battery consumption, which, depending on the use case, you’re responsible for managing. While the Transition API manages battery consumption for you, the Sampling API gives you more control over the data.

Last but not least, the Sleep API is a separate API for detecting a user’s sleep routine. To compute sleep confidence, it uses the accelerometer sensor and the ambient light sensor data. It also provides sleep time ranges each day once it detects a high-confidence awakening.

Note: If you want to learn how to use the Sleep API, check out Android Sleep API Tutorial: Getting Started or the official documentation.

Now that you’re familiar with the APIs, you can start working on your first activity recognition app.

Getting Started

Download the starter project by clicking Download Materials at the top or bottom of the tutorial. Then, open the starter project in Android Studio 3.5 or later.

Look at the code. You’ll find the starter project with activities, a layout, a permission helper and a lot of to-dos.

Build and run the starter project. You’ll see buttons to start and stop activity tracking and a label that says tracking hasn’t started yet. There’s also an ImageView holder for an image that represents a current activity.

Starting activity in Pet Buddy

Try using the app. Tap Start and you’ll get a message that tracking has started.

But when you start walking, nothing happens because activity tracking isn’t implemented yet. Don’t worry: It’s easier than you think!

If you’ve developed an app that uses sensors, you know you need to request permission to use them. In the next section, you’ll learn how to request the permission needed to start tracking your activities.

Unleashing the Power of Sensors

All Android developers need to know how to handle permissions. To use Android features like storage, location and camera, you need to request permission.

Yet, not all permissions need explicit requests for all Android versions. Sometimes, all you need to do is add it to the manifest.

You need to request permission to use the Activity Recognition Client on devices running on Android Q, API level 29, or later. For earlier versions, a one-liner in the manifest will do the magic for you.

First, open AndroidManifest.xml. Replace TODO 1 with:

<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

Devices running below Android Q use the first permission. To enable the Activity Recognition Client for Android Q or later, you’ll need the second permission. But, there’s more work left to enable sensors on devices with Android Q or later.

Note: Use the key combination Option-Return on Mac Alt-Enter on PC to resolve any missing dependencies as you work through your project.

Open the ActivityPermissionHelper.kt class. Then replace TODO 2 and with the next block of code:

val isAndroidQOrLater: Boolean =
    android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q

return if (isAndroidQOrLater.not()) {
  true
} else {
  PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
      this,
      Manifest.permission.ACTIVITY_RECOGNITION
  )
}

Here’s a step-by-step breakdown of this logic:

  1. isAndroidQOrLater gives you the information if you need to request permission.
  2. For devices with a version below Android Q, you don’t have to check permission status. It’s enough to return true.
  3. For devices running on Android Q or later, you’ll need to check the permission.

Now Pet Buddy can check permission status. Next, you’ll request permission if it’s not granted.

In ActivityPermissionHelper.kt, find TODO 3 and replace it with:

if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACTIVITY_RECOGNITION).not()) {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACTIVITY_RECOGNITION), PERMISSION_REQUEST_ACTIVITY_RECOGNITION)
  } else {
    showRationalDialog(this)
  }

The Permission rationale check returns on the next check after denying permission. Note that the condition is a negative value from the result.

If that condition is true, the native permission request dialog will show. When the result is false, the user sees a custom rationale dialog.

Build and run. Tap Start and deny the permission. Then tap Start again and your screen will look like this:

Rationale dialog for requesting permission

In the rationale dialog, you explain why the app needs this permission. The Permission rationale check returns false after two permission requests. Next, you need to consume the permission request result.

Open MainActivity.kt. Then find TODO 4 and replace it with:

if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.ACTIVITY_RECOGNITION).not() &&
        grantResults.size == 1 &&
        grantResults[0] == PackageManager.PERMISSION_DENIED) {
      showSettingsDialog(this)
    } else if (requestCode == PERMISSION_REQUEST_ACTIVITY_RECOGNITION &&
        permissions.contains(Manifest.permission.ACTIVITY_RECOGNITION) &&
        grantResults.size == 1 &&
        grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      Log.d("permission_result", "permission granted")
      isTrackingStarted = true
    }

The code above grants permission when the following conditions are true:

  • The request code matches one from the request.
  • The permissions contain Manifest.permission.ACTIVITY_RECOGNITION.
  • grantResults contains PackageManager.PERMISSION_GRANTED.

Build and run. First, tap Start. Then deny permission two times and take note of the screen:

Transition between the standing still and walking

After denying permission for the second time, a dialog appears and asks the user to grant permission in the settings.

Now, reinstall the app. Tap Start again, but this time allow the permission. Take a look at the logcat and you’ll see:

com.raywenderlich.android.petbuddy D/permission_result: permission granted

Congratulations! You’re ready to start implementing Activity Recognition Client functionalities.

In the next section, you’ll learn more about the activity types supported by the Activity Recognition Client.

Exploring Activities

Before you continue developing Pet Buddy, check out the list of supported activities in the Transition API:

  • STILL: When device sensors don’t detect any motion, such as when the device sits still on a table.
  • WALKING: A sub-activity of ON_FOOT detected when the user walks with the device.
  • RUNNING: A sub-activity of ON_FOOT detected when the user runs with the device.
  • ON_BICYCLE: Activity detected when a user rides a bicycle.
  • IN_VEHICLE: Activity when device sensors detect moving at a higher speed, such as when the user is in a car.

The Sampling API also supports these activities:

  • ON_FOOT: The device’s sensors detect movement at a normal speed when the user is walking or running.
  • TILTING: The device’s angle relative to gravity changed.
  • UNKNOWN: When the device can’t detect any of the activities.

But there’s another difference between these two APIs besides the activities they support. While the Transition API returns a list of events, for example, WALKINGEXIT and STILLENTER, the Sampling API returns the list of all activities sorted by the most confident detection.

In the next section, you’ll learn how to engage all sensors and listen for transitions. Get your shoes and your pet ready for a walk!

Detecting the Start and End of an Activity

Your pet is excited, but tell him to wait a little longer. You’re about to start working with the Transition API.

Before you can use the Activity Recognition Client components, you have to add the dependency. Open the app build.gradle, and replace TODO 5 with:

implementation 'com.google.android.gms:play-services-location:18.0.0'

Now sync gradle. Voila, now your app can access Activity Recognition Client components.

In the Transition API, you decide which transitions you want your app to detect. For example, say you’re building a messenger app that sets your status to busy when you start driving. In this case, you only need to subscribe to the ENTER and EXIT transitions of IN_VEHICLE activities by creating an instance of ActivityTransitionRequest.

But your pet isn’t excited about car rides. So, Pet Buddy will only track:

  • STILL
  • WALKING
  • RUNNING

Now that you have access to the Activity Recognition Client components, it’s time to start working.

Open SupportedActivity.kt. Then find TODO 6 and uncomment the function in the companion object. Now, the companion object looks like this:

companion object {

 fun fromActivityType(type: Int): SupportedActivity = when (type) {
   DetectedActivity.STILL -> STILL
   DetectedActivity.WALKING -> WALKING
   DetectedActivity.RUNNING -> RUNNING
   else -> throw IllegalArgumentException("activity $type not supported")
 }
}

The enum representation of DetectedActivity contains the predefined text and image resources.

Next, you’ll create the ActivityTransitionRequest for supported activities. Open TransitionHelper.kt and replace the TODO 7 with:

private fun getActivitiesToTrack(): List<ActivityTransition> =
   // 1
   mutableListOf<ActivityTransition>()
       .apply {
         // 2
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.STILL)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
             .build())
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.STILL)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
             .build())
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.WALKING)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
             .build())
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.WALKING)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
             .build())
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.RUNNING)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
             .build())
         add(ActivityTransition.Builder()
             .setActivityType(DetectedActivity.RUNNING)
             .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
             .build())
       }

This function:

  1. Creates a list of ActivityTransition types.
  2. Then adds all activity types you want to track with ENTER and EXIT transition types.

Remember to use the key combination Option-Return on Mac or Alt-Enter on PC to resolve any missing dependencies.

Your ActivityTransitionRequest is now ready. Next, you’ll make a request.

To request Transition updates, you need to use the ActivityRecognitionClient. In TransitionHelper.kt, find TODO 8 and replace it with:

val request = ActivityTransitionRequest(getActivitiesToTrack())
val task = ActivityRecognitionClient(this).requestActivityTransitionUpdates(request,
   TransitionsReceiver.getPendingIntent(this))

task.run {
 addOnSuccessListener {
   Log.d("TransitionUpdate", getString(R.string.transition_update_request_success))
 }
 addOnFailureListener {
   Log.d("TransitionUpdate", getString(R.string.transition_update_request_failed))
 }
}

Then, add the next import:

import com.google.android.gms.location.ActivityRecognitionClient

To start the transition updates, you’ll need ActivityTransitionRequest and PendingIntent.
In this case, PendingIntent points to TransitionReceiver, the implementation of BroadcastReceiver.

To save the user’s battery, you need to stop tracking the updates. In the same class, find TODO 9 and replace it with:

val task = ActivityRecognitionClient(this).removeActivityTransitionUpdates(
   TransitionsReceiver.getPendingIntent(this))

task.run {
 addOnSuccessListener {
   Log.d("TransitionUpdate", getString(R.string.transition_update_remove_success))
 }
 addOnFailureListener {
   Log.d("TransitionUpdate", getString(R.string.transition_update_remove_failed))
 }
}

This code stops listening for transition updates.

Build and run. Lift the logcat and tap Start. You’ll see the next log:

com.raywenderlich.android.petbuddy D/TransitionUpdate: Transition updates requested

Now, tap Stop. Listening stops, and you’ll see the next log:

com.raywenderlich.android.petbuddy D/TransitionUpdate: Transition updates removed

Now Pet Buddy is ready to receive updates on your activities. Next, you’ll extract those updates and show them in the MainActivity.kt

Keeping Track of Activity Transitions

First, open TransitionsReceiver.kt and replace TODO 10 with:

    // 1
    if (ActivityTransitionResult.hasResult(intent)) {
      // 2
      val result = ActivityTransitionResult.extractResult(intent)
      result?.let { handleTransitionEvents(it.transitionEvents) }
    }

There will be an error in the code that you’ll clear up in the next step. Now, replace TODO 11 with:

private fun handleTransitionEvents(transitionEvents: List<ActivityTransitionEvent>) {
    transitionEvents
        // 3
        .filter { it.transitionType == ActivityTransition.ACTIVITY_TRANSITION_ENTER }
        // 4
        .forEach { action?.invoke(SupportedActivity.fromActivityType(it.activityType)) }
  }

Use the quick keys mentioned above to resolve any missing imports or dependencies.

Here’s a code breakdown:

  1. ActivityRecognitionClient provides ActivityTransitionResult. Result verifies that the intent is of the right type.
  2. It also exposes a function to extract ActivityTransitionResult from the intent.
  3. In handleTransitionsEvents, you filter only ACTIVITY_TRANSITION_ENTER events.
  4. Then you invoke the action with the activity enter transition mapped to SupportedActivity.

You’re close to showing the transition updates on the main screen.

In MainActivity.kt, find TODO 12 and replace it with:

registerReceiver(transitionBroadcastReceiver, IntentFilter(TRANSITIONS_RECEIVER_ACTION))

Then replace TODO 13 with:

unregisterReceiver(transitionBroadcastReceiver)

Now MainActivity.kt is ready to receive transitions. Find TODO 14 and replace it with:

activityImage.setImageDrawable(ContextCompat.getDrawable(this, supportedActivity.activityImage))
activityTitle.text = getString(supportedActivity.activityText)

Your app is now ready to use! Is your pet still ready for a walk?

Build and run. Take your pet for a walk. Start tracking the transition updates and you’ll see:

Transition from standing still to walking

Now you can track activity transition changes. Before starting the Sampling API implementation, you’ll learn more about confidence grades.

Note: If you’re not getting any results, be patient. Try shaking your phone or go outside and walk for a few minutes to wake up the sensors.

Confidence Grades

As you read before, the Transition API always does calculations under the hood. Once the system is confident you’re doing some activity, you’ll get a message.

The Sampling API is a little different. Here, you get a list of probable activities with confidence grades. Confidence grades for all the probable activities range from zero to 100.

You need to set the level of confidence that will suit you. For example, you could consider the activity as confirmed if the confidence grade is above 75. But there are some cases where many activities could have high confidence.

In that case, sensors can’t distinguish between two close activities. For example, if you run slowly enough, WALKING and RUNNING will both have high confidence. But, if you’re driving a car, there’s no chance that WALKING will be a probable activity.

In the next section, you’ll learn how to implement the Sampling API and use it while your app is in the background.

Tracking Activities in the Background

Apps that track your activity should work in the background so you can still use other apps on your mobile device. In this section, you’ll learn how to use the Sampling API and run it in the background service.

Remember how you requested transition updates from the ActivityRecognitionClient? Requesting activity sampling updates is almost the same process.

Find DetectedActivityService.kt. Then replace TODO 15 with:

val task = ActivityRecognitionClient(this).requestActivityUpdates(ACTIVITY_UPDATES_INTERVAL,
        DetectedActivityReceiver.getPendingIntent(this))

    task.run {
      addOnSuccessListener {
        Log.d("ActivityUpdate", getString(R.string.activity_update_request_success))
      }
      addOnFailureListener {
        Log.d("ActivityUpdate", getString(R.string.activity_update_request_failed))
      }
    }

In the above code, you can see the process is pretty much the same as requesting transition updates. You create a task and apply success and failure listeners to that task. The difference is that now you call requestActivityUpdates which takes detectionInterval and the activity’s pending intent as parameters and updates the receiver’s PendingIntent.

Now, ActivityRecognitionClient will send periodic updates to your app.

Next, add code to stop the updates in case a user closes the app. Find TODO 16 and replace it with:

val task = ActivityRecognitionClient(this).removeActivityUpdates(
        DetectedActivityReceiver.getPendingIntent(this))

    task.run {
      addOnSuccessListener {
        Log.d("ActivityUpdate", getString(R.string.activity_update_remove_success))
      }
      addOnFailureListener {
        Log.d("ActivityUpdate", getString(R.string.activity_update_remove_failed))
      }
    }

This code removes all activity updates specified for PendingIntent. It also helps you control battery consumption.

Now, you’ll register the service and broadcast receiver in AndroidManifes.xml.

Open AndroidManifes.xml. Find TODO 17 and replace it with:

<service android:name=".detectedactivity.DetectedActivityService" />
<receiver android:name=".detectedactivity.DetectedActivityReceiver"/>

This code lets your service and receiver work in the background.

Now, switch to DetectedActivityReceiver.kt. Here, you’ll handle messages received from the ActivityRecognitionClient.

First, find TODO 18 and uncomment handleDetectedActivities and showNotification. Next, add the imports:

import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.android.gms.location.DetectedActivity
import com.raywenderlich.android.petbuddy.MainActivity
import com.raywenderlich.android.petbuddy.R
import com.raywenderlich.android.petbuddy.SUPPORTED_ACTIVITY_KEY
import com.raywenderlich.android.petbuddy.SupportedActivity

Now, you’ll handle the Intent received from the ActivityRecognitionClient with the same steps you used for transition updates. Find TODO 19, and replace it with:

// 1
if (ActivityRecognitionResult.hasResult(intent)) {
 // 2
 val result = ActivityRecognitionResult.extractResult(intent)
 // 3
 result?.let { handleDetectedActivities(it.probableActivities, context) }
}

Here, the logic:

  1. Checks if the received intent contains a wanted result.
  2. Then, extracts ActivityRecognitionResult from the intent.
  3. If the result is not null, it handles probable activities.

probableActivites is a sorted list of activities based on confidence grades. Since your pet likes to walk and run, you don’t want to track any other activities. Okay, you’ll take standing still into consideration, too.

In DetectedActivityReceiver.kt, find TODO 20. Replace it with:

detectedActivities
        .filter {
          it.type == DetectedActivity.STILL ||
              it.type == DetectedActivity.WALKING ||
              it.type == DetectedActivity.RUNNING
        }
        .filter { it.confidence > RELIABLE_CONFIDENCE }
        .run {
          if (isNotEmpty()) {
            showNotification(this[0], context)
          }
        }

First, you filter activities you’re interested in. Then, you filter activities with a confidence grade over 75% because you’re only interested in the most probable activity.

Voila! The notification is here.

When you requested permission, you logged a message on permission granted. In the MainActivity.kt, find TODO 21. In the else branch above it, add:

  startService(Intent(this, DetectedActivityService::class.java))
  requestActivityTransitionUpdates()

When a user taps Start on a fresh install of the app, the permission dialog shows. When a user allows the permission, it’ll start tracking activity.

Great job! You’re ready to use the Sampling API.

Build and run. Go outside, start tracking and pay attention to the status bar. You’ll see a notification like this:

Notification with standing still status

Congratulations! You created your first activity tracker app. Take your dog for a walk and enjoy nature.

Where to Go From Here?

In this tutorial, you learned why and how to use the Activity Recognition API. You used the Activity Recognition Client to detect the start and end of an activity and how to keep track of activity transitions. You also learned about confidence grades and tracking activities in the background.

You can download the complete project by clicking Download Materials at the top or bottom of this tutorial.

If you want to know more about this topic, check out the official documentation for building apps with Android Recognition Client.

There’s one API left in the Android Recognition Client. Take a look at the official documentation for Sleep API.

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments