Android Activity Recognition
Activity Recognition gives our Android device the ability to detect a number of our physical activities like walking, riding a bicycle, driving a car or standing idle. All that by simply using an API to access Google Play Services, an increasingly crucial piece of software available to all Android versions.
As in the article on geofencing, we will download the sample app (ActivityRecognition.zip) at the Android developer’s site and start playing with it, eventually modifying parts of it to fit our purposes. We will show here only the most relevant code sections.
The first thing to note is that we need a specific permission to use Activity Recognition:
<!-- Inside manifest file --> <uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>
As with geofencing or location updates, we use the API to request Google Play Services to analyse our data and provide us with the results. The chain of method calls for requesting updates is similar to that of geofencing:
- Make sure that Google Play Services is available
- As an Activity Recognition client, request a connection
- Once connected, Location Services calls back the onConnected() method in our app
- Proceed with the updates request via a Pending Intent pointing to an IntentService we have written
- Google Location Services sends out its activity recognition updates as Intent objects, using the PendingIntent we provided. Get & process the updates in our IntentService’s onHandleIntent() method.
The sample app writes all the updates in a log file, and that is OK if we like that sort of thing…though a closer look at the data makes us realize that most of it is garbage. Do we really need to know that we have a 27% chance of being driving a vehicle and a 7% chance of riding a bicycle when we are in fact sitting idle at our desk? Not really. What we want is the most significant data, and in this case, that would be the most probable activity:
//.. import com.google.android.gms.location.ActivityRecognitionResult; import com.google.android.gms.location.DetectedActivity; /** * Service that receives ActivityRecognition updates. It receives updates * in the background, even if the main Activity is not visible. */ public class ActivityRecognitionIntentService extends IntentService { //.. /** * Called when a new activity detection update is available. */ @Override protected void onHandleIntent(Intent intent) { //... // If the intent contains an update if (ActivityRecognitionResult.hasResult(intent)) { // Get the update ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent); DetectedActivity mostProbableActivity = result.getMostProbableActivity(); // Get the confidence % (probability) int confidence = mostProbableActivity.getConfidence(); // Get the type int activityType = mostProbableActivity.getType(); /* types: * DetectedActivity.IN_VEHICLE * DetectedActivity.ON_BICYCLE * DetectedActivity.ON_FOOT * DetectedActivity.STILL * DetectedActivity.UNKNOWN * DetectedActivity.TILTING */ // process } } }
Instead of writing the updates to a log file, it is simpler to just store them in memory (e.g. in a static List in a dedicated class) and display them to the user of our app. One way to do this would be by using a Fragment to display the updates on top of a Google map. As commented in previous articles, Fragments were introduced in Honeycomb but are also available to older Android versions through the Support Library.
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/lay_map" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <fragment android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" class="com.google.android.gms.maps.SupportMapFragment"/> <!-- activity recognition --> <fragment android:id="@+id/ar_fragment" android:name="package.to.our.fragment.ActReconFragment" android:layout_width="match_parent" android:layout_height="wrap_content"/> </FrameLayout>
Once we define our own XML layout for the ActReconFragment and give it a transparent background (left to the reader as an exercise), we will get a nice overlaid display like this:
Since we have chosen to show the most probable activity to the users of our app, we need the display to be dynamic, like a live feed. For that, we can add a local broadcast in our service:
//inside ActivityRecognitionIntentService 's onHandleIntent Intent broadcastIntent = new Intent(); // Give it the category for all intents sent by the Intent Service broadcastIntent.addCategory(ActivityUtils.CATEGORY_LOCATION_SERVICES); // Set the action and content for the broadcast intent broadcastIntent.setAction(ActivityUtils.ACTION_REFRESH_STATUS_LIST); // Broadcast *locally* to other components in this app LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent);
We are using a LocalBroadcastManager (included in android 3.0 and above and in the support library v4 for early releases). Apart from providing our own layout to position the Activity Detection panel on top of a map, the only new code snippet we wrote is the above local broadcast. For the remainder below, we have simply re-positioned the sample app’s code in a fragment and use in-memory storage of the activity updates instead of using a log file. The receiver on that local broadcast is in our Fragment:
//... public class ActReconFragment extends Fragment{ // Intent filter for incoming broadcasts from the IntentService IntentFilter mBroadcastFilter; // Instance of a local broadcast manager private LocalBroadcastManager mBroadcastManager; //... /** * Called when the corresponding Map Activity's * onCreate() method has completed. */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Set the broadcast receiver intent filer mBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); // Create a new Intent filter for the broadcast receiver mBroadcastFilter = new IntentFilter(ActivityUtils.ACTION_REFRESH_STATUS_LIST); mBroadcastFilter.addCategory(ActivityUtils.CATEGORY_LOCATION_SERVICES); //... } /** * Broadcast receiver that receives activity update intents * This receiver is local only. It can't read broadcast Intents from other apps. */ BroadcastReceiver updateListReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // When an Intent is received from the update listener IntentService, // update the display. updateActivityHistory(); } }; //... }
Once we have taken care of the display, we need to move on to other important aspects like what to do with those activity updates. The sample app gives us one example of that in the ActivityRecognitionIntentService:
if( // If the current type is "moving" i.e on foot, bicycle or vehicle isMoving(activityType) && // The activity has changed from the previous activity activityChanged(activityType) // The confidence level for the current activity is >= 50% && (confidence >= 50)) { // do something useful }
Simply getting the most probable activity might be OK for displaying purposes, but might not be enough for an app to act on it and do something useful. We need to make sure that the type of activity and the corresponding confidence level (i.e. probability) are adequate for our purposes. While a detected activity type of “Unknown” with a confidence level of 52% is next to useless, knowing that the user is moving in a vehicle as opposed to walking can be put to good use: increase the frequency of location updates, enlarge the map area of available points of interest, etc…
Activity Recognition has been added as an experimental feature to this Geofencing app. Check it out and feel free to post any feedback.
thank you very much, that was good example
also I think it will be nice to have the source code for the projects in this cool site :)