People in Motion
The next task for my mileage tracking app is, The app should only track your miles when you’re driving.
For this, I’m using Google’s Activity Recognition API to detect when the user starts a drive. The first thing I did was create an ActionTransition class, which encapsulates the API setup code and starts and stops the request for transition updates. I implemented it as a singleton because I’m planning on calling it from both my OnBootReceiver and from the app on startup, and I want to get the transition data from it as a Kotlin Flow to display on the application’s screen.
Currently, I’m requesting all of the transitions as a debugging aid. The final app will not display the transitions at all, and will only use the IN_VEHICLE
ACTIVITY_TRANSITION_ENTER
and ACTIVITY_TRANSITION_EXIT
events to trigger the start and stop of the drive location tracking.
Requesting the activity transition updates is straight-forward, I followed the documentation and everything works as advertised.
My broadcast receiver is manifest-declared, which looks like this:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<application
...
<receiver
android:name=".data.services.ActionTransitionReceiver"
android:exported="true"
android:permission="android.permission.ACTIVITY_RECOGNITION" />.
</receiver>
...
</application>
</manifest>
I added the permission, com.google.android.gms.permission.ACTIVITY_RECOGNITION
, because my minimum supported SDK is 28, even though I don’t think I need it, since I’m targeting SDK 34. The docs are unclear, but most sources say it’s ok to use that permission alongside android.permission.ACTIVITY_RECOGNITION
. I don’t own a device running SDK 28, a.k.a. Android 9, a.k.a. P, a.k.a. Pie — ugh, the confusion this causes— so I can’t test that theory right now, but it works fine on my device with both declared. 😉
Also, because I don’t have any intent filters defined for my receiver, and the exported attribute is true (because it’s listening for a system broadcast), I had to add the permission attribute.
The ActionTransitionReceiver was coded as per the documentation and ended up working as expected after I integrated it into my app code, although it took me a while to figure out how to get it to work, while sitting at my favorite restaurant/office. Oh! If you’re targeting SDK 33 and above — which Google is forcing you to do at this point — don’t forget to ask the user for the ACTIVITY_RECOGNITION
permission. For Jetpack Compose, I recommend the Google Accompanist Permissions library.
Anyhow, the first time I ran the app in debug mode with activity tracking hooked up, I got a STILL
ACTIVITY_TRANSITION_ENTER
event at my breakpoint. Fantastic! So, it looked like the system would give me the last recorded action every time I started the app. Of course, it never happened again. I hadn’t changed any code and I even wrote a quick test app with only the activity transition code, but still no events on startup. I started searching and someone else had the same issue. The answer was to shake the phone for about 30 seconds to simulate walking. Sure enough, that worked! Duh! 😆
Then it came time to start the activity recognition code from my OnBootReceiver, which is triggered after a reboot. I had started off using Koin as a dependency injection framework (technically, it’s a service locator, but it serves the same purpose). I prefer it because there’s no generated code to clutter up a code search and I don’t have to periodically purge all the code caches when the build mysteriously fails, but this was the first time I’ve had to inject a class into a manifest-declared BroadcastReceiver using it, and I immediately ran into a problem.
class OnBootReceiver : BroadcastReceiver() {
val actionTransition: ActionTransition by inject()
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(
"${OnBootReceiver::class.simpleName}",
"Received broadcast: ${intent?.action ?: "null action"}"
)
actionTransition.startTracking(
onFailure = { message ->
Log.d(
"${OnBootReceiver::class.simpleName}",
"Failed to start activity tracking: $message"
)
},
)
}
}
activityTransition was null at the time of use, so the app was crashing. I did a little research and the consensus was, the BroadcastReceiver is initialized before the Application.onCreate() method is called, which is where Koin gets initialized. So, plan B was to use Hilt instead. I probably could have worked around it, but I felt the need to move on and I hadn’t used Hilt in a while, so… Why not?
@AndroidEntryPoint
class OnBootReceiver : BroadcastReceiver() {
@Inject
lateinit var actionTransition: ActivityTransition
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(
"${OnBootReceiver::class.simpleName}",
"Received broadcast: ${intent?.action ?: "null action"}"
)
actionTransition.startTracking(
onFailure = { message ->
Log.d(
"${OnBootReceiver::class.simpleName}",
"Failed to start activity tracking: $message"
)
},
)
}
}
Hilt had just released it’s KSP annotation processor version and, of course I had to try it, instead of the KAPT version. Following the docs, I added the dependencies and upgraded the plugins in the Gradle file, and when I ran the app, it crashed:
Caused by: java.lang.NoSuchMethodException:
com.wreckingballsoftware.roadruler.ui.mainscreen.MainScreenViewModel.<init> []
Ok, so it’s the view model. It probably has something to do with Jetpack Compose, which I’m using for the UI. I went back through the compose docs, which say the latest version of Hilt works with compose, and my code looked like the example, using the viewModel()
call in the compose function parameter list, but I kept getting that exception.
I finally tried using hiltViewModel()
instead of viewModel()
, even though the docs imply that the call is only for the Compose navigation module. The IDE let me know that hiltViewModel
was undefined, so I went searching for that and found that by adding
implementation(“androidx.hilt:hilt-navigation-compose:1.1.0”)
to my Gradle dependencies, everything works fine. So… Bad documentation? User error? I wouldn’t be surprised either way, but hiltViewModel()
works, so I’m good. Although, I get the following build warning now:
The following options were not recognized by any processor: '[dagger.fastInit, dagger.hilt.android.internal.disableAndroidSuperclassValidation, dagger.hilt.android.internal.projectType, dagger.hilt.internal.useAggregatingRootProcessor]'
I looked into it, and found a few different explanations: Unused annotations, conflicts with other dependencies, kapt issues, but nothing that seems relevant to my setup, so I’ll chalk it up to a new KSP library and keep an eye on it. Maybe it’ll go away in a future version. 🙂
I then set up the ActionTransitionReceiver to update a Kotlin Flow in my ActionTransition class with each new transition, so now I‘m getting a visual confirmation of the user’s actions as they happen. Perfect! Next up, the foreground service for location tracking.