The Sensors API is provided to work with a live stream of fitness sensor data. It can provide data from sensors on local or connected devices. The Sensors API is a part of Google play services and can be connected using the GoogleApiClient
class. In SensorActivity
, we first add the required scopes and Sensors API and then connect to Google play services using an object of the GoogleApiClient
class. The steps for connecting to Google play services via the GoogleApiClient
class are explained in the first section of the driving event detection application of the bonus chapter, Sensor Fusion and Sensors-Based APIs – The Driving Events Detection App, during the discussion on the activity recognition API. The only difference in steps is that, instead of adding activity recognition API; we have to add Sensors API. Now let's look at the individual tasks performed by SensorActivity
:
SensorActivity
is to get authorization from the user to read live fitness data using the Sensors API. This authorization only has to be requested the first time. To get authorization, we first create an object of the GoogleApiClient
class in the activity's onCreate()
method and then add the Fitness.SENSORS_API
and relevant scopes in the object. To test all the available data sources, we add all the four possible scopes, but for a real-world application, we should only add the required scopes, as these scopes are visible to the user in the authorization system dialog. We also have to add the connection successful and failed callback listeners in the GoogleApiClient
object. After creating the GoogleApiClient
object, we connect it to the Google service library in activity's onStart()
method and disconnect the onStop()
method of the activity. If the user has already provided authorization, it will be connected successfully and will be notified through the onConnected()
method callback.But if the user has not given authorization before, then the connection will fail and will be notified through the onConnectionFailed()
method callback. If the connection failed because of non-authorization, or any other reason that can be resolved by Google play services, then the method hasResolution()
of the ConnectionResult
object inside the onConnectionFailed()
method is passed as true and we can call the startResolutionForResult()
method of the ConnectionResult
object. This will present the user with the authorization system dialog asking for relevant permissions if the user has not provided these permissions before. If there is any other reason for the connection to fail, such as the user doesn't have a fitness account or it is not configured, then it will try to resolve that. Once the user has given permission, it will notified in the onActivityResult()
method with the same request code that we requested in the startResolutionForResult()
method, and from there we can again try to connect to Google services:
public class SensorActivity extends Activity implements ConnectionCallbacks, OnConnectionFailedListener, OnItemSelectedListener, OnItemClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.livedata_layout); mLiveDataText = (TextView)findViewById(R.id.livedata); setUpSpinnerDropDown(); setUpListView(); mClient = new GoogleApiClient.Builder(this) .addApi(Fitness.SENSORS_API).addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ)) .addScope(new Scope(Scopes.FITNESS_BODY_READ)) .addScope(new Scope(Scopes.FITNESS_LOCATION_READ)) .addScope(new Scope(Scopes.FITNESS_NUTRITION_READ)) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this).build(); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { if(connectionResult.hasResolution()){ try { connectionResult.startResolutionForResult (SensorActivity.this, REQUEST_OAUTH); }catch (Exception e) { e.printStackTrace(); } } } @Override protected void onActivityResult(intrequestCode, intresultCode, Intent data) { if (requestCode == REQUEST_OAUTH&&resultCode == RESULT_OK) { if (!mClient.isConnecting() && !mClient.isConnected()) { mClient.connect(); } } }
SensorActivity
is to list all the available data sources for a selected data type. We use the spinner drop-down to let the user select a particular data type. In the setUpSpinnerDropDown()
method, we set up the spinner and set it on the selected listener. We get all the human readable string values for all the available data types from the getDataTypeReadableValues()
method of the DataHelper
utility singleton class. After the user has selected a data type from the spinner drop-down value, we find all its available data sources. In the onItemSelected()
spinner callback, we get the selected item position, and by using the getDataTypeRawValues()
method of the DataHelper
utility class, we get its corresponding DataType
object value, which is then passed to the listDataSources()
method to query the available data sources. Inside the listDataSources()
method, we use the findDataSources()
method of the Fitness.SensorsApi
class to query all the available data sources.The findDataSources()
API requires two parameters: the first is the object of GoogleApiClient
and the second is the object of DataSourcesRequest
, which has a builder syntax shown in the following code snippet. The DataSourcesRequest
API accepts two parameters: the first is the data type, which is a mandatory parameter, and second is the type of data source (TYPE_DERIVED
and TYPE_RAW
), which is an optional parameter. If we don't specify the type of data sources, then we will receive both types of data source. The result of the available data sources is received inside the result listener, which is set by passing the object of ResultCallback<DataSourcesResult>
inside the setResultCallback()
method of the findDataSources()
API. The result is received in the form of List<DataSource>
, which contains all the available DataSource
objects for that particular data type. Using this list, we populate our local mDataSourceList
, which is the ArrayList
of DataSource
. We show the entire list of available data sources in the ListView
, which is set up inside the setUpListView()
method and is called from the onCreate()
method of the activity. If no data source is found, then we display the relevant message in mLiveDataText
, which is the object of TextView
. We set the item click listener on the ListView
to receive the index of the clicked data source item for which the data listener will be added (this is explained in the next section). The implementation details of ListAdapter
can be found in the code that comes with this chapter:
public void setUpListView() { mListView = (ListView)findViewById(R.id.datasource_list); mListAdapter = new ListAdapter(); mListView.setOnItemClickListener(this); mListView.setAdapter(mListAdapter); } public void setUpSpinnerDropDown() { Spinner spinnerDropDown = (Spinner) findViewById(R.id.spinner); spinnerDropDown.setOnItemSelectedListener(this); ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, DataHelper.getInstance() .getDataTypeReadableValues()); arrayAdapter.setDropDownViewResource (android.R.layout.simple_spinner_dropdown_item); spinnerDropDown.setAdapter(arrayAdapter); } public void listDataSources(DataType mDataType) { Fitness.SensorsApi.findDataSources(mClient, new DataSourcesRequest.Builder().setDataTypes(mDataType) .setDataSourceTypes(DataSource.TYPE_DERIVED) .setDataSourceTypes(DataSource.TYPE_RAW).build()) .setResultCallback(new ResultCallback<DataSourcesResult>() { @Override public void onResult(DataSourcesResult dataSourcesResult) { mListAdapter.notifyDataSetChanged(); if (dataSourcesResult.getDataSources() .size() > 0) { mDataSourceList.addAll (dataSourcesResult.getDataSources()); mLiveDataText.setText("Please select from following data source to get the live data"); } else { mLiveDataText.setText("No data source found for selected data type"); } } }); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (mClient.isConnected() && position!=0) { listDataSources(DataHelper.getInstance() .getDataTypeRawValues().get(position)); if(mDataSourceList.size()>0) { mDataSourceList.clear(); } } }
OnDataPointListener
. In our example, we first get the clicked position of the data source item of the ListView
inside the onItemClick()
method and, using the position, we get the corresponding DataSource
object from mDataSourceList
and pass this object to the addDataListener()
method for adding the listener. Inside the addDataListener()
method, we use the add()
method of the Fitness.SensorsApi
class to add the listener and get the live sensor data. The add()
API requires three parameters: the first is the object of GoogleApiClient
and the second is the object of SensorRequest
, which has a builder syntax shown in the following code snippet. The API accepts three important parameters: the first one is the sampling rate, the second is the mandatory data type, and the third parameter is the optional data source in the SensorRequest
object. Another parameter accepted by the add()
API is the object of OnDataPointListener
, which receives the live data from sensors and returns it in the form of a single DataPoint
object. The DataPoint
object consists of multiple fields and their values. For our example, we iterate over all the fields and their values using a for
loop and show them in the mLiveDataText
label. The add()
API also allows us to set the result callback by passing the object of ResultCallback<Status>
inside the setResultCallback()
method of the API. Depending on the status received in this result callback, we set the relevant message in the mLiveDataText
label. Before adding the data point listener, we check if there is an existing listener already added by using the isDataListenerAdded
Boolean variable inside the onItemClick()
method. If the data point listener has already been added, then we remove it by calling the removeDataListener()
method. Inside the removeDataListener()
method, we remove the existing data point listener using the remove()
method of the Fitness.SensorsApi
class. It accepts two arguments: the first is the object of GoogleApiClient
and the second is the object of the existing data point listener. We set the isDataListenerAdded
Boolean variable back to false
after the successful removal of the listener:@Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //remove any existing data listener,if previously added. if(isDataListenerAdded) { removeDataListener(); } addDataListener(mDataSourceList.get(position)); } public void addDataListener(DataSource mDataSource) { Fitness.SensorsApi.add(mClient, new SensorRequest.Builder().setDataSource(mDataSource) .setDataType(mDataSource.getDataType()) .setSamplingRate(1, TimeUnit.SECONDS) .build(), mOnDataPointListener) .setResultCallback(new ResultCallback<Status>() { @Override public void onResult(Status status) { if (status.isSuccess()) { mLiveDataText.setText("Listener registered successfully, waiting for live data"); isDataListenerAdded = true; } else { mLiveDataText.setText("Listener registration failed"); } } }); } OnDataPointListener mOnDataPointListener = new OnDataPointListener() { @Override public void onDataPoint(DataPoint dataPoint) { final StringBuilder dataValue = new StringBuilder(); for (Field field : dataPoint.getDataType().getFields()) { Value val = dataPoint.getValue(field); dataValue.append("Name:" + field.getName() + " Value:" + val.toString()); } runOnUiThread(new Runnable() { @Override public void run() { mLiveDataText.setText(dataValue.toString()); } }); } }; public void removeDataListener() { Fitness.SensorsApi.remove(mClient, mOnDataPointListener).setResultCallback(new ResultCallback<Status>() { @Override public void onResult(Status status) { if (status.isSuccess()) { isDataListenerAdded = false; Log.i(TAG, "Listener was remove successfully"); } else { Log.i(TAG, "Listener was not removed"); } } }); }
We created a small utility that lists all the available data sources for a particular selected data type. For simplicity, we loaded all available names of data types in a spinner drop-down to select from. Once a data type is selected from the spinner drop-down, we load the available data sources in a list corresponding to that data type. A data type can have multiple data sources available from local or connected devices. Once any data source is clicked on from the list of available data sources, we add its listener and display the live data coming from that data source in a text field. This utility can be used in any use case where you have to process live sensor data. There are only a few data types, such as steps or location-based data types, for which you will find available data sources that provide live sensor data.
Most data sources provide data to the fitness store after processing. An important sensor that you would expect to provide live data is the heart rate BPM, especially on Android wear, but most Android wear (such as Moto 360 and LG Watch Urban), don't support the streaming of heart rate data over Bluetooth; instead they process the heart rate locally on the watch and upload it later to the Google Fitness Store. There are some chest wrap heart rate monitor devices, such as the Polar h7 Bluetooth heart rate sensor, that support the live streaming of heart rate data over Bluetooth. The following is a screenshot from a Nexus 5X device, showing the available data sources for the data type STEP COUNT DELTA
: