Skip to content

Commit

Permalink
Merge pull request #424 from this-Aditya/google-activity
Browse files Browse the repository at this point in the history
Implemented plugin for Google Activity Recognition API
  • Loading branch information
blootsvoets committed Aug 21, 2023
2 parents 077d77b + af6c5c5 commit b13601b
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 0 deletions.
27 changes: 27 additions & 0 deletions plugins/radar-android-google-activity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Google Activity plugin of RADAR-pRMT

Plugin to monitor the user activity transition data. The user activity data is tracked via Google Activity Recognition API. The API automatically detects activities by periodically reading short bursts of sensor data and processing them using machine learning models. This API provides the activity and transition of it.

This plugin requires the Android `ACTIVITY_RECOGNITION` permission for devices with API level 29 or higher, and the `com.google.android.gms.permission.ACTIVITY_RECOGNITION` permission for devices with API level 28 or lower.

## Installation

To add the plugin to your app, add the following snippet to your app's `build.gradle` file.

```gradle
dependencies {
implementation "org.radarbase:radar-android-google-activity:$radarCommonsAndroidVersion"
}
```

Add `org.radarbase.passive.google.activity.GoogleActivityProvider` to the `plugins` variable of the `RadarService` instance in your app.

## Configuration

To enable this plugin, add the provider `google_activity` to `plugins` property of the configuration.

This plugin produces data for the following topics: (types starts with `org.radarcns.passive.google` namespace)

| Topic | Type | Description |
|--------------------------------------------|---------------------------------|---------------------------------------------------------------------------------------|
| `android_google_activity_transition_event` | `GoogleActivityTransitionEvent` | Represents an activity transition event, for example start to walk, stop running etc. |
21 changes: 21 additions & 0 deletions plugins/radar-android-google-activity/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply from: "$rootDir/gradle/android.gradle"

android {
namespace "org.radarbase.passive.google.activity"
}

//---------------------------------------------------------------------------//
// Configuration //
//---------------------------------------------------------------------------//

description = 'Google Activity Recognition plugin for RADAR passive remote monitoring app.'

//---------------------------------------------------------------------------//
// Sources and classpath configurations //
//---------------------------------------------------------------------------//
dependencies {
api project(':radar-commons-android')
implementation 'com.google.android.gms:play-services-location:21.0.1'
}

apply from: "$rootDir/gradle/publishing.gradle"
13 changes: 13 additions & 0 deletions plugins/radar-android-google-activity/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">

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

<application android:allowBackup="true">
<service android:name=".GoogleActivityService"
android:exported="false"
android:description="@string/google_activity_description" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017 The Hyve
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.radarbase.passive.google.activity

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.android.gms.location.ActivityTransitionResult

class ActivityTransitionReceiver(private val googleActivityManager: GoogleActivityManager) : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
if (ActivityTransitionResult.hasResult(intent)) googleActivityManager.sendActivityTransitionUpdates(intent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2017 The Hyve
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.radarbase.passive.google.activity

import com.google.android.gms.location.ActivityTransition
import com.google.android.gms.location.ActivityTransitionRequest
import com.google.android.gms.location.DetectedActivity

object ActivityTransitionUtil {

private fun getTransitions(): MutableList<ActivityTransition> {
val transitions = mutableListOf<ActivityTransition>()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.IN_VEHICLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.IN_VEHICLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_BICYCLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_BICYCLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_FOOT)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_FOOT)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.RUNNING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()

transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.RUNNING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()

return transitions
}

fun getActivityTransitionRequest() = ActivityTransitionRequest(getTransitions())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2017 The Hyve
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.radarbase.passive.google.activity

import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Process
import android.os.SystemClock
import androidx.core.content.ContextCompat
import com.google.android.gms.location.ActivityRecognition
import com.google.android.gms.location.ActivityTransition
import com.google.android.gms.location.ActivityTransitionResult
import com.google.android.gms.location.DetectedActivity
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
import org.radarbase.android.util.SafeHandler
import org.radarbase.android.util.toPendingIntentFlag
import org.radarbase.passive.google.activity.GoogleActivityProvider.Companion.ACTIVITY_RECOGNITION_COMPAT
import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.google.ActivityType
import org.radarcns.passive.google.GoogleActivityTransitionEvent
import org.radarcns.passive.google.TransitionType
import org.slf4j.LoggerFactory

class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceManager<GoogleActivityService, BaseSourceState>(context) {
private val activityTransitionEventTopic: DataCache<ObservationKey, GoogleActivityTransitionEvent> = createCache("android_google_activity_transition_event", GoogleActivityTransitionEvent())

private val activityHandler = SafeHandler.getInstance("Google Activity", Process.THREAD_PRIORITY_BACKGROUND)
private val activityPendingIntent: PendingIntent
private val activityTransitionReceiver: ActivityTransitionReceiver

private val isPermissionGranted: Boolean
get() = ContextCompat.checkSelfPermission(service, ACTIVITY_RECOGNITION_COMPAT) == PackageManager.PERMISSION_GRANTED

init {
name = service.getString(R.string.google_activity_display_name)
activityTransitionReceiver = ActivityTransitionReceiver(this)
activityPendingIntent = createActivityPendingIntent()
}

override fun start(acceptableIds: Set<String>) {
register()
activityHandler.start()
status = SourceStatusListener.Status.READY
activityHandler.execute {
registerActivityTransitionReceiver()
registerForActivityUpdates()
}
}

private fun registerActivityTransitionReceiver() {
val filter = IntentFilter(ACTION_ACTIVITY_UPDATE)
service.registerReceiver(activityTransitionReceiver, filter)
logger.info("Registered activity transition receiver.")
}

@SuppressLint("MissingPermission")
private fun registerForActivityUpdates() {
if (isPermissionGranted) {
status = SourceStatusListener.Status.CONNECTING

ActivityRecognition.getClient(service)
.requestActivityTransitionUpdates(
ActivityTransitionUtil.getActivityTransitionRequest(),
activityPendingIntent
)
.addOnSuccessListener {
status = SourceStatusListener.Status.CONNECTED
logger.info("Successfully subscribed to activity transition updates")
}
.addOnFailureListener { exception ->
disconnect()
logger.error("Exception while subscribing for activity transition updates: $exception") }
}
else {
logger.warn("Permission not granted for ACTIVITY_RECOGNITION")
disconnect()
}
}

fun sendActivityTransitionUpdates(activityIntent: Intent) {
logger.info("Activity transition event data received")
val activityTransitionResult: ActivityTransitionResult = ActivityTransitionResult.extractResult(activityIntent) ?: return

// Accepting the events only if the activity happened in last 30 seconds
val now = SystemClock.elapsedRealtimeNanos()
activityTransitionResult.transitionEvents.asSequence()
.filter { now - it.elapsedRealTimeNanos < 30_000_000_000 }
.forEach { event ->
val time = event.elapsedRealTimeNanos.toActivityTime() / 1000.0
val activity = event.activityType.toActivityType()
val transition = event.transitionType.toTransitionType()
send(
activityTransitionEventTopic,
GoogleActivityTransitionEvent(time, currentTime, activity, transition)
)
}
}

private fun createActivityPendingIntent(): PendingIntent {
val intent = Intent(ACTION_ACTIVITY_UPDATE)
logger.info("Activity pending intent created")
return PendingIntent.getBroadcast(service, ACTIVITY_UPDATE_REQUEST_CODE, intent,
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentFlag(true)
)
}

private fun unRegisterFromActivityReceiver() {
try {
service.unregisterReceiver(activityTransitionReceiver)
logger.info("Unregistered from activity transition receiver ")
} catch (ex: IllegalArgumentException) {
logger.error("Exception when unregistering from activity transition receiver", ex)
}
}

@SuppressLint("MissingPermission")
private fun unRegisterFromActivityUpdates() {
if (isPermissionGranted) {
ActivityRecognition.getClient(service)
.removeActivityTransitionUpdates(activityPendingIntent)
.addOnSuccessListener {
logger.info("Successfully unsubscribed from activity transition updates")
}
.addOnFailureListener { exception ->
logger.error("Exception while unsubscribing from activity transition updates: $exception")
}
}
}

override fun onClose() {
activityHandler.stop {
unRegisterFromActivityUpdates()
unRegisterFromActivityReceiver()
}
}

companion object {
private val logger = LoggerFactory.getLogger(GoogleActivityManager::class.java)

const val ACTION_ACTIVITY_UPDATE = "org.radarbase.passive.google.activity.ACTION_ACTIVITY_UPDATE"
private const val ACTIVITY_UPDATE_REQUEST_CODE = 534351
}

private fun Int.toTransitionType(): TransitionType = when (this) {
ActivityTransition.ACTIVITY_TRANSITION_ENTER -> TransitionType.ENTER
ActivityTransition.ACTIVITY_TRANSITION_EXIT -> TransitionType.EXIT
else -> TransitionType.UNKNOWN
}

private fun Int.toActivityType(): ActivityType = when (this) {
DetectedActivity.IN_VEHICLE -> ActivityType.IN_VEHICLE
DetectedActivity.ON_BICYCLE -> ActivityType.ON_BICYCLE
DetectedActivity.ON_FOOT -> ActivityType.ON_FOOT
DetectedActivity.STILL -> ActivityType.STILL
DetectedActivity.WALKING -> ActivityType.WALKING
DetectedActivity.RUNNING -> ActivityType.RUNNING
else -> ActivityType.UNKNOWN
}

/** Returns epoch time (ms) for the transition events from the time after last device boot when this transition happened. */
private fun Long.toActivityTime(): Long = getLastBootTime() + this / 1000000

private fun getLastBootTime(): Long = System.currentTimeMillis() - SystemClock.elapsedRealtime()
}




Loading

0 comments on commit b13601b

Please sign in to comment.