Chrome Custom Tabs go by many names. Originally documentation for them was under com.android.support:customtabs
, which recently for Android Jetpack was renamed to androidx.browser.customtabs
. They are a fantastic hybrid between redirecting users to an external browser, and sending them to an incomplete WebView
browser implementation in app. Even though CustomTabs
are commonly referred to as an in-app browser, and they run inside of your apps Android task, you have about as much access to controlling its back stack as if it were an external application. This can make creating custom back stack behavior frustrating while dealing with CustomTabs
, especially when the CustomTabs
. website redirects back to your application.
We recently dealt with this at Rally while we were in the process of moving to the OAuth 2.0 standard. We wanted to create a common library to handle the flow across all of our Android applications, and using the Chrome Custom Tabs in-app browser is a recommended best practice. To understand the problem we encountered and our solution, its easiest if you understand a little about the OAuth 2.0 flow, so the general flow goes something like this:
Application’s main Activity
is started.Detect that user isn’t logged in, so the authentication experience is launched inside the CustomTabs Activity
.
CustomTabs
fires a redirect that the Application
intercepts via an IntentFilter
on an Activity
(Only Activity
s can intercept browser Intent
s, not BroadcastReceiver
s). The redirect includes an auth code that needs to be redeemed for an auth session and a refresh token.Most developers when implementing the above flow would naturally create the following Activity
flow:
The problem with the above flow is that there is no reliable way to go back from the Redeem Code Activity
to the App Activity
. If finish() is called for the Redeem Code Activity
, the user is dropped off on the CustomTabs Activity
, with no way, except for user action, to get back to the App Activity
.
Make the App Activity launchMode=”singleTask"?
One potential solution is to set the launchMode
of App Activity
in AndroidManifest.xml
to singleTask
. This would make it so that if you launched your App Activity
from the Redeem Code Activity
, the user would be taken back to the existing instance of App Activity
, and anything after it on the back stack (in this case CustomTabs Activity
and Redeem Code Activity
) would be removed. There are a few down-sides with this approach:
- The
Activity
that you redirect back to has to besingleTask
for this to work, which may not always be the case. - If the user presses the back button while on the
Redeem Code Activity
, the user is sent back to theCustomTabs Activity
, which isn’t always desirable. Redeem Code Activity
would need to know aboutApp Activity
explicitly, making this a non-ideal solution to package into a library to re-use across multiple applications.
Fire an intent to redirect?
Another potential solution is to fire an internal intent from the Redeem Code Activity
, and another Activity
in your app can launch via an IntentFilter
. Typically you need to include some sort of Intent
flags, to clear the back stack as well. The main problem with this approach is that it’s an all-or-nothing solution. After authentication completes, you can choose to let the entire back stack be cleared, or none of it be cleared.
Our solution
Auth Manager Activity
- Create an Activity
that doesn’t have a UI, that instead acts as a state machine. This Activity
would maintain all of its state via onSaveInstanceState
, and in onResume
would check the current state, and then launch the next Activity
for the auth flow. This Activity would of course be `singleTask` as well. It’d look like this in your AndroidManifest.xml
:
<activity android:name=".ui.AuthManagementActivity" android:exported="false" android:theme="@android:style/Theme.Translucent.NoTitleBar" android:launchMode="singleTask" />
You can see that Google uses this same strategy in it’s own auth library. Take a look at their AuthorizationManagementActivity if you want more details.
Auth Redirect Activity
- Create another Activity
with no UI whose sole job is to intercept the redirect URL from the CustomTabs Activity
, and then have it immediately send the relevant data to the Auth Manager Activity
, and finish()
itself. Since the Auth Manager Activity
is singleTask
, this will pop CustomTabs Activity
off the backstack. The Auth Manager Activity
can then send the user to the Redeem Code Activity
and if the user presses back while the auth code is being redeemed, the user will be sent to the Auth Manager Activity
, which can detect the cancellation, and then decide what to do with the user.
The Redirect Activity
should look something like this:
internal class AuthRedirectReceiverActivity : Activity() { override fun onCreate(savedInstanceBundle: Bundle?) { super.onCreate(savedInstanceBundle) startActivity( Intent(this, AuthManagementActivity::class.java).apply { data = intent.data addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) } ) finish() } }
The end flow looks something like this:
- The app launches the entire flow by going to the
Auth Manager Activity
. Auth Manager Activity
has no current state, so it launches into theCustomTabs
browser flow where the user authenticates.- After authentication completes, the
Redirect Activity
receives the callback containing the auth code. - The
Redirect Activity
sends the intercepted URI to theAuth Manager Activity
, and immediately callsfinish()
, removing theCustomTabs Activity
from the back stack. - The
Auth Manager Activity
redirects the user to theRedeem Code Activity
, where the user is shown a UI while the network request to redeem the code is pending. - After successfully redeeming the auth code, the refresh token and session information are persisted and the
Redeem Code Activity
isfinish()
ed, sending the user back to theAuth Manager Activity
. - The
Auth Manager Activity
detects a successful authentication, so itfinish()
es itself and the user is taken back to theActivity
they started the authentication flow from.
After authentication successfully completes, the user is sent back to the Activity
that they started this flow on. Of course this approach could be combined with the Intent
fire approach mentioned above, by firing an Intent
while the Auth Manager Activity
calls finish()
on itself. In the end, this is the approach leaving devs with the most flexibility. Again, these techniques aren’t specific to OAuth 2.0, but could be applied for maintaining the backstack in many states involving Chrome Custom Tabs.
The entities and logos identified on this page may have trademarks and those trademarks are owned by the respective entity.