GaitAuth™ is an SDK offered by UnifyID that allows you to authenticate your users based on how they move. This post demonstrates the process of adding GaitAuth to a simple Android app.
The UnifyID Developer Portal has high-level documentation that covers the basics of the GaitAuth SDK integration process. There are also auto-generated JavaDocs available for the specific details. The goal of this post is to meet the two in the middle and give some more color to the integration process. By the end of the post we’ll have built a sample Android app that uses GaitAuth to train and test a GaitModel. Along the way, we’ll also explore relevant best practices and important implementation details.
The Sample App
All of the sample app code is available on GitHub. To get a feel for the training and testing process you can build the app on your phone and try it for yourself. If you want to get right to building your own app, you can treat the repository as a collection of helpful code snippets. You can also use the code to follow along with this post in depth. Not all of the code is shown in this post, the code that is shown has been abbreviated for simplicity’s sake. Links to the original code on GitHub are provided at the top of the snippets.
The sample app closely mirrors the functionality of the GaitAuth SDK. On the starting screen you are presented with the option to create or load a model. You’ll want to create a new model when you first use the app. In future uses, you can use the load option to skip training. After creating a model you’re ready to collect data and train the model. First, turn on collection (it will continue to collect even if the phone is locked or the app is backgrounded or closed). Once you’ve collected some features you can add them to the model. After you’ve collected enough data you can train the model. While you wait for the model to train (which may take a few hours), you can manually refresh its status.

If the model fails to train you’ll need to start over by pressing the trashcan icon in the top left corner. When the model successfully trains you’re ready to test it. Turn on feature collection and walk around for a while. When you are ready, stop collecting features and score them. This will graph the scores and show you an authentication decision.

Now that we know what the sample app does, let’s go build it.
Configuration & Initialization
Adding GaitAuth to our Gradle configuration is a good starting point. We’ll also need to request some permissions in the Android Manifest. We can follow the steps outlined in the Developer Portal documentation to take care of that.
Before the GaitAuth SDK can be used anywhere in the app it must be initialized. The initialization is trivial; always initializing it before using it is harder. In something straightforward like this sample app we can simply initialize it in the onCreate
method of the MainActivity
.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L144 if (GaitAuth.getInstance() == null) { // First initialize the UnifyID Core SDK UnifyID.initialize(getApplicationContext, SDK_KEY, USER, new CompletionHandler() { @Override public void onCompletion(UnifyIDConfig config) { // Second initialize the GaitAuth SDK GaitAuth.initialize(context, config); // Save these values for debugging purposes Preferences.put(Preferences.CLIENT_ID, config.getClientId()); Preferences.put(Preferences.INSTALL_ID, config.getInstallId()); Preferences.put(Preferences.CUSTOMER_ID, config.getCustomerId()); startGaitAuthService(context); route(); } }); }
When initialization succeeds we squirrel away a couple of relevant details in the Shared Preferences (the Preferences class hides the idiosyncrasies of the Shared Preferences API). These will come in handy for debugging. You can view them by tapping on the info icon in the top right corner of the sample app. We also start a foreground service — more on this later. Finally, we call route
which determines what the app does next.
In an app with a more complex workflow, initialization of the GaitAuth SDK may not be so simple. In these scenarios we recommend you wrap the SDK in a class that protects its usage. With some simple synchronization this wrapper can ensure that the SDK is never used uninitialized.
Two important points remain. First, remember that the SDK key is a sensitive value and should be protected. In the sample app we ask the user to enter the SDK key the first time they use the app. For an app in production, something like Android Keystore can be used. Second, the user value has a few important stipulations on it. It must be unique, immutable, and should be unrelated to any personally identifiable information. We don’t recommend using things like email addresses or phone numbers for the user value.
The GaitModel
Now that we’ve gotten through the drudgery of initialization it’s time to talk about the star of the show — the GaitModel. In a sense the sample app can be viewed as a tool for managing the lifecycle of a GaitModel. It creates a new model, loads training data into it, initiates training, and then tests the model. For this reason, the sample app’s routing is based on the status of the current GaitModel.
The sample app uses a single-activity/multiple-fragment architecture where each fragment is a different screen. In the MainActivity
after the initialization of the GaitAuth SDK the following routing code is run.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L185 private void route() { // Load the id of the current GaitModel String modelId = Preferences.getString(Preferences.MODEL_ID); if (Strings.isNullOrEmpty(modelId)) { showFragment(new SelectModelFragment()); } else { loadModel(modelId); } } // https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L287 private void loadModel(String modelId) { AsyncTask.execute(() -> { model = GaitAuth.getInstance().loadModel(modelId); Preferences.put(Preferences.MODEL_ID, modelId); renderFragmentBasedOnModelStatus(model.getStatus()); }); } // https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L255 private void renderFragmentBasedOnModelStatus(GaitModel.Status status) { switch (status) { case CREATED: showFragment(FeatureCollectionFragment.build(gaitAuthService.isTraining())); break; case TRAINING: showFragment(new ModelPendingFragment()); break; case FAILED: showFragment(ModelErrorFragment.newInstance(model.getReason())); break; case READY: showFragment(TestingFragment.build(gaitAuthService.isTesting())); break; default: // treat it as a failure showFragment(ModelErrorFragment.newInstance("unknown model status")); break; } }
First the method route
looks for the id of the current GaitModel and asynchronously loads it with the help of loadModel
. After the model is loaded, renderFragmentBasedOnModelStatus
is called. A simple switch statement then sends the user to the screen matching the current state of the model.
The method route
will not find a model id on a user’s first time through the app or if the reset button was pressed. In these cases the user is immediately sent to the SelectModelFragment
. From here, when the user clicks on the “Create Model” button, onCreateModelPressed
is executed. It builds a new GaitModel, saves the model id in the shared preferences, and sends the user to the FeatureCollectionFragment
. Alternatively, the user can opt to load a pre-existing model which leverages the same loadModel
method.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L315 public void onCreateModelPressed() { AsyncTask.execute(() -> { model = GaitAuth.getInstance().createModel(); Preferences.put(Preferences.MODEL_ID, model.getId()); showFragment(new FeatureCollectionFragment()); }); }

Feature Collection
So we have a GaitModel locked and loaded, but now what? The model is useless without training data so let’s start there. A GaitModel is trained on a collection of GaitFeatures which are data points collected from the user’s movement. These GaitFeatures are given to the model via the add
method which has the signature void GaitModel.add(Collection<GaitFeature> features)
. Later during training, only the features explicitly added to the model will be used.

Now that we know how to use the features, how do we actually collect them? This is achieved by registering a FeatureEventListener
that will fire the onNewFeature
callback for every feature collected. Correctly managing feature collection requires tackling two key issues: feature storage and backgrounding.
Storing Features
In a naive implementation of feature collection, every time a new feature was received it would be immediately added to the GaitModel. There are two problems with this. First, adding features to the GaitModel may use network resources or trigger other expensive operations and thus is inefficient to call frequently. Second, if the model fails when training you will need to recollect all new data for the new model since you didn’t persist it anywhere.
The sample app solves the feature storage problem with a thread-safe FeatureStore
class. This class exposes a method with the signature void add(GaitFeature feature)
, which serializes the feature and appends it to a file on disk. At a future time (when the user clicks the “Add Feature” button) all of the features can be loaded from disk, deserialized and returned via the method getAll
with the signature List<List<GaitFeature>> getAll()
. Note that it returns the features partitioned into multiple lists. This is because adding thousands of features to a model at once should be avoided. Finally, there is an empty
method to clear the file after adding features. This prevents adding features twice. Deleting the features after using them reintroduces the persistence problem though. The sample app does it anyways to avoid re-adding features to the model in a simple manner.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/GaitAuthService.java#L163 public void onNewFeature(GaitFeature feature) { featureStore.add(feature); int count = Preferences.getInt(Preferences.FEATURE_COLLECTED_COUNT) + 1; Preferences.put(Preferences.FEATURE_COLLECTED_COUNT, count); } // https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L363 public int onAddFeaturesPressed() { FeatureStore featureStore = FeatureStore.getInstance(this); List<List<GaitFeature>> chunks = featureStore.getAll(); int uploadedCounter = 0; for (List<GaitFeature> chunk: chunks) { model.add(chunk); uploadedCounter += chunk.size(); } if (uploadedCounter > 0) { // only truncate file after we uploaded something featureStore.empty(); } return uploadedCounter; }
In a production application there are many improvements you would want to make to FeatureStore
. First and foremost, it should implement some form of in-memory buffering. Writing to disk for every feature you collect is slow and can lead to poor battery-life. Second, it should rotate files to avoid size limitations and corruption. Third, it should not clear the file after adding the features to the model. Rather, it should keep track of what features have been added and only add the new ones.
Backgrounding
The second key issue to solve for feature collection is backgrounding. It would not be wise to register the FeatureEventListener
on the app’s main thread. As soon as the user closed the app, GaitFeatures would no longer be collected. Android services can help us solve this problem. Services provide a way to execute a long-running operation in the background. There are two relevant types of Android services: background and foreground. A background service needn’t provide any indication to the user that it is running, but is more likely to be shut down by the OS if resources are scarce. A foreground service must present a notification to the user indicating its presence the entire time it is alive. But, it is unlikely to be killed by the OS.
The sample app takes the most straightforward approach. In the onCreate
method of the MainActivity
a foreground service is created regardless of whether or not the service will be used. This avoids complex state management and synchronization issues that arise when dynamically building a service. In the onCreate
method the activity binds to the service. And in the onStop
method it unbinds from the service. This gives the activity direct access to the feature collection service.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L104 protected void onStop() { super.onStop(); if (gaitAuthServiceBound.get()) { unbindService(gaitAuthServiceConnection); gaitAuthServiceBound.set(false); } }
You may want to handle services differently in a production app. For example, you may only want to start the service when you are actually collecting features. If you go this route make sure to take special care in synchronizing the usage of the GaitAuth SDK.
Final Considerations
That was a lot of details. Let’s take a step back for a moment and consider the entire training process. Feature collection for training is the most critical stage for the GaitAuth SDK. To get good authentication results you need to have a well trained model. The Developer Portal documentation has a number of suggestions on how to best do this.
Training the Model
The hard work of feature collection was well worth the effort. We now have a plethora of GaitFeatures to train with. We’re nearly ready to use our GaitModel, but first we need to train it. Thankfully, training is easy. When a user clicks on the “Train Model” button the method below is called. In a background thread it kicks off the training process for the model and sends the user to the ModelPendingFragment
.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L397 public void onTrainModelPressed() { AsyncTask.execute(() -> { try { model.train(); showFragment(new ModelPendingFragment()); } catch (GaitModelException e) { showToast("Failed to start training model."); } }); }

At the pending model screen a user can manually refresh the status of a model while they wait for it to train.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L436 public void onRefreshPressed() { AsyncTask.execute(() -> { try { model.refresh(); renderFragmentBasedOnModelStatus(model.getStatus()); } catch (GaitModelException e) { showToast("Failed to refresh model status."); } }); }
After refreshing the status of the model, our old friend renderFragmentBasedOnModelStatus
is called to bring the user to the right screen given the model’s status. If the training failed for some reason the ModelErrorFragment
will load. From there the user has no choice other than to reset and start over. After a successful training, the user will be presented with the TestingFragment
. And of course, if the model status is still TRAINING
after a refresh then no navigation will occur.

Testing the Model
The hard work is over now — we’ve trained a GaitModel and can now reap the benefits. All of the testing functionality is managed by the Authenticator
interface. When you instantiate an Authenticator
you pass it a GaitModel and a scoring policy. Once created, it automatically starts collecting features. You can ask it for an authentication decision at any time and it will say if the user is authenticated
or unauthenticated
. In the scenario where this is not enough data to make a decision it will return inconclusive
. When the user presses the “Start Collection” button onStartCollectionBtnPressed
is called. It in turn tells the foreground service to create a new Authenticator
object.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L338 public void onStartCollectionBtnPressed() { try { gaitAuthService.startFeatureCollectionForTesting(model); } catch (GaitAuthException e) { showToast("Failed to start feature collection for testing."); } } // https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/GaitAuthService.java#L213 public void startFeatureCollectionForTesting(GaitModel model) throws GaitAuthException { GaitQuantileConfig config = new GaitQuantileConfig(QUANTILE_THRESHOLD); authenticator = GaitAuth.getInstance().createAuthenticator(config, model); }
A quick aside on the GaitQuantileConfig
policy and how it works. In short it will authenticate the user if at least X percent of scores in the session scored Y or more (learn more about scores here). X is known as the quantile and Y is the score threshold. GaitQuantileConfig
sets the quantile to 50% by default and in the sample app the score threshold is set to 0.8. The table below shows three example sessions with these numbers. The first session has 60% of scores at or above the 0.8 score threshold, and therefore has an authenticated
result. However, the second and third sessions only have 40% and 20% of scores that meet the 0.8 score threshold, and therefore they produce an unauthenticated
result.

You can customize more than just the quantile and score threshold of the GaitQuantileConfig
. The method setMinNumScores
lets you configure how many scores are required for an authentication decision to be made. Any attempt to authenticate with a number of scores less than this minimum will return inconclusive
. Similarly, setMaxNumScores
configures the maximum amount of scores that will be considered. If there are more scores than the maximum, the most recent scores will be chosen. Finally, setMaxScoreAge
determines how old of scores can be used. Scores older than the given age will not be used for an authentication decision.
Back to the action. The sample app requires the user to stop collecting features before they can score them. Note that this is not a requirement of the GaitAuth SDK and is only done to simplify state management. The Authenticator
has a stop
method which helps us do this. Clicking the “Stop Collection” button kicks off this sequence of events.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L349 public void onStopCollectionBtnPressed() { gaitAuthService.stopFeatureCollectionForTesting(); } // https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/GaitAuthService.java#L223 public void stopFeatureCollectionForTesting() { if (authenticator != null) { authenticator.stop(); } }
Now the user can press the “Score Features” button. This entails getting the authenticator from the foreground service and then getting an authentication status from the authenticator. Then it sends the user to the ScoreFragment
. The authenticator also returns the individual scores that lead to the authentication decision. ScoreFragment
puts these in a graph to help visualize the decision. How you use the authenticator will be highly dependent on the specifics of your use case.
// https://github.com/UnifyID/gaitauth-sample-app-android/blob/470932205438dd2ce43dc6bad586df90c72cc800/app/src/main/java/id/unify/gaitauth_sample_app/MainActivity.java#L469 public void onScoreFeaturesPressed() { Authenticator authenticator = gaitAuthService.getAuthenticator(); authenticator.getStatus(new AuthenticationListener() { @Override public void onComplete(AuthenticationResult authenticationResult) { showFragment(scoreFragment); } @Override public void onFailure(GaitAuthException e) { showToast("Failed to get scores."); } }); }

Conclusion
And just like that we’ve integrated GaitAuth into a simple Android application. For more implementation details you can explore the code in depth on GitHub. If you build something with GaitAuth, let us know on social media. We’d love to hear about it. Finally, reach out to us if you have any trouble integrating.