top of page
Rechercher
Photo du rédacteurRuben Mim

Activity Recognition Tutorial



Activity recognition API is a fun way to identify what's the device owner is currently doing, if it's walking,running, driving ( maybe soon skying ? ). It's not really hard to incorporate in an app and it can be useful for different project like sports apps, driving time recorder, pedometer as examples.

The process will be :

  1. Add the activity recognition permission in the manifest, and we'll pay attention to the required Android API version.

  2. Create a Service to keep your recognizer tool running .

  3. Updating UI accordingly to the current activity is user is doing, practicing or whatever.



Permission


Graddle

implementation "com.google.gms:google-services:4.3.3"
implementation "com.google.android.gms:play-services-location:17.0.0"


Manifest

// if your app is targeting Api level 29 and above
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>

// if your app is targeting Api level 28 and below
<uses-permission android:name="android.gms.permission.ACTIVITY_RECOGNITION"/>

The difference ( except the code ) is the security level required from the Api level raised from the 28 to the 29, meaning to get the the user permission in Api level 29 and above, you will pop a request dialog to get the authorization whereas when targeting Api level 28 and below ( until Api level 11, when it got created ) the permission if written in the manifest is automatically granted, like the Internet one for example.






Main Activity


companion object {
    const val MY_PERMISSIONS= 101

    const val BROADCAST_DETECTED_ACTIVITY = "activity_intent"
}
    
private var broadcastReceiver: BroadcastReceiver? = null    

fun checkForPermissions(){
    val arrayOfPermission: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        arrayOf(Manifest.permission.ACTIVITY_RECOGNITION)
    }else{
        arrayOf("com.android.gms.permission.ACTIVITY_RECOGNITION")
    }
    ActivityCompat.requestPermissions(this, arrayOfPermission, MY_PERMISSIONS)

}

The constant value in the companion object is the request code we'll need to request the permission.


Have you noticed the way arrayOfPermission is built ? It basically checks first what Api level the device is on and sets its value according to it. We then call the static method requestPermission() from the ActivityCompat and give as parameters the activity, the arrayOfPermission and the permission code.

Call checkForPermission() in OnCreate().


Add to your class this method :

fun isActivityRecognitionGranted() : Boolean{

    if (ContextCompat.checkSelfPermission(this,
    Manifest.permission.ACTIVITY_RECOGNITION)
        != PackageManager.PERMISSION_GRANTED) {
        
        Toast.makeText(activity,"Activity Recognition is not granted", Toast.LENGTH_SHORT).show()
        
   checkForPermissions()

            return false

    }
Toast.makeText(activity,"Activity Recognition is granted", Toast.LENGTH_SHORT).show()
    return true
}

For the case we need a permission from the user (from Api 29 ) , we need to check if the permission has been granted. If not we request the permission again of course ! It will cause a loop of request until our dear user gives us his permission ( and a StackOverFlow error). It's a rude user experience, but we need it so we'll force him to give it MWAHAHAHA .




Anyway, override the onRequestPermissionResult() method in your main activity and add this:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {           
 if(grantResults.isEmpty() || requestCode != MY_PERMISSIONS ||         !permissions.contains(Manifest.permission.ACTIVITY_RECOGNITION)){
       isActivityRecognitionGranted(this)

     }
  }
}

Ok I stop kidding, basically we see a check of the grantResults array, the request code and the permissions right ? If you do add this check you'll avoid the StackOverFlow error. However if the request was not finished because of X or Y reason or is denied, the user will be asked again next time the app is calling onCreate().






Yeeaaah ! First step went well we can now go forward on our Activity recognition quest











Service


"The service is an application component used to run longer-running operation while not interacting with the user". Because we will use one quiet heavy, we are going to use a IntentService class to move it the a background thread and not affect the UI.


First things first and it's a big one !


// No worries we'll explain step by step 

class BackgroundDetectedActivitiesService : Service() {

    private val tag = "BackgroundActivitieService"
    private val  DETECTION_INTERVAL_IN_MILLISECONDS = 30*1000.toLong()
    private var mIntent  : Intent? = null
    private var mPendingIntent : PendingIntent?  = null
    private var mActivityRecognitionClient : ActivityRecognitionClient? = null

    private val mBinder = LocalBinder()

    private val transitions = mutableListOf<ActivityTransition>()

    private var request : ActivityTransitionRequest? =null


 
//1
    class LocalBinder : Binder() {
        fun getServerInstance(): BackgroundDetectedActivitiesService? {
            return BackgroundDetectedActivitiesService()
        }
    }


    override fun onBind(p0: Intent?): IBinder? {
        return mBinder
    }

    override fun onCreate() {
        super.onCreate()

        setUpTransitions()
//3
        request = ActivityTransitionRequest(transitions)



    }
//6
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        mActivityRecognitionClient = ActivityRecognitionClient(this)
        mIntent  = Intent(this, DetectedIntentService::class.java)
        mPendingIntent = PendingIntent.getService(this, 1,   mIntent!!,PendingIntent.FLAG_CANCEL_CURRENT)
        requestActivityUpdatesHandler()


        return START_STICKY
    }

//2
    private fun setUpTransitions(){

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

        transitions +=            ActivityTransition.Builder().setActivityType(DetectedActivity.RUNNING)                .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()
    }

//4
    private fun requestActivityUpdatesHandler(){
        val task =  mActivityRecognitionClient!!.requestActivityUpdates(DETECTION_INTERVAL_IN_MILLISECONDS,mPendingIntent)
        task.addOnSuccessListener {
            Toast.makeText(
                applicationContext,
                "Successfully requested activity updates",
                Toast.LENGTH_SHORT
            )
                .show()
        }

        task.addOnFailureListener {
            Toast.makeText(
                applicationContext,
                "Requesting activity updates failed to start",
                Toast.LENGTH_SHORT
            )
                .show()
        }
    }

//5
    private fun removeActivityUpdatesButtonHandler() {
        val task =
            mActivityRecognitionClient!!.removeActivityUpdates(
                mPendingIntent
            )
        task.addOnSuccessListener {
            Toast.makeText(
                applicationContext,
                "Removed activity updates successfully!",
                Toast.LENGTH_SHORT
            )
                .show()
        }
        task.addOnFailureListener {
            Toast.makeText(
                applicationContext, "Failed to remove activity updates!",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        removeActivityUpdatesButtonHandler()
    }

}

// 1 Local bind service is needed to handle client-server case. Suppose you have a case when you need to do some long operations many times , need results from it and use the results in your Activity.In that case you bind service and use its method and also can use callback to get results and do operations in activity.


// 2 In this method we add to our mutableList of ActivityTransitions objects new transitions according to what activity types we are planning to get our users to do. Notice the transition , it records when if you want updates when you exit or enter the activity.


// 3 After activity transitions are set up we simply insert them as a parameter to our ActivityTransitionRequest object.


// 4 Here first we create an instance of the task created by the ActivityRequestClient's method requestActivityUpdates(). We use to the task to update the user ( it's more a developping/debugging message now ) of the status of the updates when


// 5 Same as above, the difference is that it will be called once, to remove all activity request updates and free our soul.


//6 onStartCommand() is called every time a client starts the service using startService(Intent intent). If you don't implement onStartCommand() then you won't be able to get any information from the Intent that the client passes to onStartCommand() and your service might not be able to do any useful work. You use a pending intent to get the service given by the DetectedIntentService class. START_STICKY means, if the service is detroyed because the OS is in lack of memory when memory is freed onStartCommand() will be called again.


PS : There are a lot more activity types than you think, I recommend you to check it out !



Create another class extending IntentService

class DetectedIntentService : IntentService("INTENT ACTIVITY"){


    private val tag = "DETECTEDINTENTSERVICE"


 
    override fun onHandleIntent(p0: Intent?) {
       val result = ActivityRecognitionResult.extractResult(p0)

        val detectedActivities: MutableList<DetectedActivity> = result.probableActivities
        
        for (activity in detectedActivities) {
            broadcastActivity(activity)
        }
    }




    private fun broadcastActivity(activity: DetectedActivity) {
        val intent = Intent(BROADCAST_DETECTED_ACTIVITY)
        intent.putExtra("type", activity.type)
        intent.putExtra("confidence", activity.confidence)

        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
    }

}


On HandlingIntent() is called each time onStartCommand() sends a new intent, and process each intent one by one. That's why from there we call broadCastActivity() wich contains a LocalBroadcastManager which basically send data update only within the app.


Don't forget to add services to your manifest in the <application> tag, or it will not run :

<application>

<service
    android:name=".ui.home.service.DetectedIntentService"
    android:exported="false" />
<service
    android:name=".ui.home.service.BackgroundDetectedActivitiesService"
    android:enabled="true" />
    
</application>

UUUF it was a lot for once and we are almost done chickaas !



Update UI


Now, go back to the MainActivity and add these methods :


fun stopTracking() {
    if (intentTracking == null) return
   .stopService(intentTracking)
}


fun startTracking() {
    try {
        intentTracking =
            Intent(companionActivity, BackgroundDetectedActivitiesService::class.java)
        companionActivity?.startService(intentTracking)
    } catch (e: IllegalStateException) {
        Log.e("RUN", "$e : couldnt not start service")
    }

val intentFilter = IntentFilter(BROADCAST_DETECTED_ACTIVITY)
intentFilter.addCategory(Intent.CATEGORY_DEFAULT)
registerReceiver(broadcastReceiver, intentFilter)

}


private fun handleUserActivity(type: Int, confidence: Int) {
    if (isAdded) {

        var label = ""
       
        when (type) {

            DetectedActivity.RUNNING -> {
                label = "Running"
                Log.i("MAIN",label)
              
            }
            DetectedActivity.STILL -> {          
                label = "STILL"
              Log.i("MAIN",label)
            }
            DetectedActivity.WALKING -> {  
                label = "WALKING"
             Log.i("MAIN",label)
            }


        }

Call startTracking() and instance the broacastReceiver in OnCreate() :


broadcastReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == BROADCAST_DETECTED_ACTIVITY) {
            //   Log.e("RUN DETAILS","RECEIVE")
            val type = intent.getIntExtra("type", -1)
            val confidence = intent.getIntExtra("confidence", 0)

            handleUserActivity(activity, confidence)
   
        }
    }
}
    startTracking()

Use stopStracking() or in the onDestroy() method or with a button and onClickListener.





WAS IT LONG AND TOUGH ? WAS IT COMPLICATED ? I hope not and I guess it wasn't. Hope you enjoyed and I helped somehow, I mean if you didn't know how to use it. See you soon for new android adventures !



816 vues0 commentaire

Comentários


bottom of page