Custom App Authentication on Android

Problem

The Training Plan Android app is set up such that the majority of the app is controlled from a single Activity (MainActivity) that displays different content fragments dependent on the screen that a user needs to see.

A few other activities are used for example the IndividualRunActivity which displays tabbed content associated with an individual run that constitutes part of the users training plan.

Following on from my recent piece on redesigning the Running.org authentication system, I was looking to add some onboarding prompts to areas of the app to inform users to connect their Strava accounts to their Running.org account to unlock the full capabilities of the app.

Previous Setup

Previously a fragment displayed within my MainActivity sent a call off to the Strava authentication url in whatever browser a user had set up on their device. Upon successful authentication the user would be redirected by Strava to a processing endpoint on TrainingPlan.com. This endpoint would then update the database appropriately. Unfortunately however, the user would have authenticated their Strava account but would still be on their web browser - it would not automatically close.

Connecting with Strava

Unfortunately Android doesn't allow an external app to control the launched handling browser. There is no way of knowing that the authentication process is complete (at which point one would ideally be able to programatically close the browser app and update the user interface to reflect the changed connection status).

The initial resolution for this was to use custom URL schemes. The processing endpoint would redirect to training-plan://auth a scheme that would be handled by the Training Plan app.

This is achieved with an intent-filter in the AndroidManifest.

<intent-filter>
	<action android:name="android.intent.action.VIEW" />
	<category android:name="android.intent.category.DEFAULT" />
	<category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="${schemeName}" />
</intent-filter>

along with a manifestPlaceholder definition in the apps build.gradle (this allows different scheme names on different builds e.g development)

manifestPlaceholders = [schemeName:"training-plan",usesCleartextTraffic:"false"]

Previously the intent-filter was applied to the MainActivity which handled the call and updated the user interface with the updated connection status.

This worked perfectly but a problem arose when I wanted to allow authentication from the IndividualRunActivity too. The callback would want to be handled by MainActivity meaning that the activity that the user had left the app from would no longer be displayed - a poor user experience.

The Resolution

The answer that pointed me in the right direction was this one.

The basic premise is that rather than having MainActivity handle the callbacks one should have a separate activity that handles the callbacks.

I opted to have this be an activity that displays a single WebView. My approach is similar in nature to the approach forced upon iOS developers by Apples strict developer guidelines. They require users to utilise an embedded SFSafariViewController to display authentication links.

My thought process was that regardless of which activity I am coming from, I would launch this universal Strava authentication activity using startActivityForResult. This would allow me to have different interfaces to the authentication process depending on the situation.

Upon successful authentication it would redirect to my custom URL scheme which would be handled by the same activity. I could then finish the activity and handle the update of the previous user interface in the onActivityResult method of the calling activity.

In practice however this did not work.

Another Problem

I stumbled upon another problem, namely that my embedded WebView would show an error (ERR_UNKNOWN_URL_SCHEME) when it tried to redirect to my custom URL scheme.

The answers to this question pointed me in the right direction for a resolution.

Essentially, an embedded WebView can not handle a custom URL scheme unless you tell it how to.

I implemented the shouldOverrideUrlLoading on a custom WebViewClient and allowed it to handle all network URLs itself:

if( URLUtil.isNetworkUrl(url) ) {

	return false;
}

This left me to handle any non network URLs (including my custom URL scheme) myself.

I handled this as outlined above by setting a result, and finishing the authentication activity.

setResult(resultCode, resultIntent)
finish()

return true

I then handle updating the user interface of the calling Activity in onActivityResult

Note

This took longer to implement (because I had to work out the approach) than the initial method, but it is significantly simpler and clearer with an obvious separation of concerns.

What is however interesting is that this approach does not actually use custom URL schemes in the traditional sense.

My authentication endpoint on TrainingPlan.com could have redirected the user to any non network URL and I could have handled it in shouldOverrideUrlLoading in the same manner.

The reason that I didn't is because the iOS app uses the same custom URL scheme. What is actually important here is that the user is authenticated by an embedded WebView owned by my app.