Wondering about your COVID-19 risk level and seeking precautionary steps?

Check Now
  • Rally
  • Back Stack Management with Chrome Custom Tabs

Back Stack Management with Chrome Custom Tabs

By Scott Pierce | January 20, 2020

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.

  • The user successfully logs in.
  • The website inside the CustomTabs fires a redirect that the Application intercepts via an IntentFilter on an Activity (Only Activitys can intercept browser Intents, not BroadcastReceivers). The redirect includes an auth code that needs to be redeemed for an auth session and a refresh token.
  • Redeem the auth code with the server for an auth session and 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 be singleTask 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 the CustomTabs Activity, which isn’t always desirable.
    • Redeem Code Activity would need to know about App 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:

    1. The app launches the entire flow by going to the Auth Manager Activity.
    2. Auth Manager Activity has no current state, so it launches into the CustomTabs browser flow where the user authenticates.
    3. After authentication completes, the Redirect Activity receives the callback containing the auth code.
    4. The Redirect Activity sends the intercepted URI to the Auth Manager Activity, and immediately calls finish(), removing the CustomTabs Activity from the back stack.
    5. The Auth Manager Activity redirects the user to the Redeem Code Activity, where the user is shown a UI while the network request to redeem the code is pending.
    6. After successfully redeeming the auth code, the refresh token and session information are persisted and the Redeem Code Activity is finish()ed, sending the user back to the Auth Manager Activity.
    7. The Auth Manager Activity detects a successful authentication, so it finish()es itself and the user is taken back to the Activity 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.

    Scott Pierce