Time for action – working with live fitness data using the Sensors API

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:

  1. The first task performed by 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(); 
                } 
            } 
          } 
    
  2. The second important task performed by 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(); 
                } 
              } 
            } 
    
  3. After getting the available data sources for a particular data type, we can get the live data from the data source. To get the live data from the data source, we have to add the object of 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"); 
              } 
            } 
          }); 
        } 

What just happened?

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:

What just happened?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset