Skip to content

Expose CustomTabsSession on TwaLauncher and LauncherActivity - #584

Open
dnikolaev wants to merge 1 commit into
GoogleChrome:mainfrom
dnikolaev:expose-customtabs-session
Open

Expose CustomTabsSession on TwaLauncher and LauncherActivity#584
dnikolaev wants to merge 1 commit into
GoogleChrome:mainfrom
dnikolaev:expose-customtabs-session

Conversation

@dnikolaev

@dnikolaev dnikolaev commented May 1, 2026

Copy link
Copy Markdown

Summary

Adds two @Nullable read-only accessors over fields that already exist:

  • TwaLauncher#getSession() (public)
  • LauncherActivity#getCustomTabsSession() (protected)

No behavior change for callers that don't use them.

Motivation

Apps that subclass LauncherActivity (e.g. anything generated by Bubblewrap) can't reach the CustomTabsSession that TwaLauncher constructs internally. Since Chrome 115 enabled postMessage in TWAs, that session is the only entry point to requestPostMessageChannel / postMessage, so the high-level launcher is currently incompatible with PostMessage.

The demos/twa-post-message sample added in #429 builds its own session and does not extend LauncherActivity, so it doesn't translate to the Bubblewrap launcher path. In the wild this is worked around with reflection on mSession or by spinning up a parallel CustomTabsServiceConnection and relaunching the TWA — both fragile.

Lifecycle

The session is established asynchronously in onCustomTabsServiceConnected and cleared in onServiceDisconnected / destroy(). Both getters are @Nullable and the Javadoc tells callers to guard.

Usage

With this in place, a subclass can wire PostMessage on the existing getCustomTabsCallback() hook:

@Override
protected CustomTabsCallback getCustomTabsCallback() {
    return new CustomTabsCallback() {
        @Override public void onNavigationEvent(int event, Bundle extras) {
            if (event != NAVIGATION_FINISHED) return;
            CustomTabsSession s = getCustomTabsSession();
            if (s != null) s.requestPostMessageChannel(source, target, new Bundle());
        }
        @Override public void onPostMessage(@NonNull String msg, @Nullable Bundle extras) {
            CustomTabsSession s = getCustomTabsSession();
            if (s != null) s.postMessage("pong", null);
        }
    };
}

Verified end-to-end in a Bubblewrap-generated production app.

Scope

  • No postMessage helpers or callback wrappers — those belong in androidx.browser.customtabs.
  • No new fields, lifecycle changes, threads, or dependencies.
  • No change to session creation timing or DEFAULT_SESSION_ID.

Closes #472. Closes #510. Refs GoogleChromeLabs/bubblewrap#954.

🤖 Generated with Claude Code

@google-cla

google-cla Bot commented May 1, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@dnikolaev

Copy link
Copy Markdown
Author

@googlebot I've signed CLA.

@gstepniewski-google

Copy link
Copy Markdown
Collaborator

Hi Dima, thanks for submitting this! I have two comments:

  1. I like the idea of exposing the CustomTabsSession, but I'm not fully sold on the implementation - you mention in the javadoc that the field is initially null and then becomes available when session is established. Would it be a good idea to return the session wrapped in an observable so that clients can subscribe to it and automatically perform actions when it becomes available? There's a launchWhenSessionEstablished in TwaLauncher that could be used for this.

  2. The CLA check is still failing despite me rerunning it a couple of times - do you mind double checking that everything is correct on your end?

Thanks!

@sergio-veed

Copy link
Copy Markdown

Would really love to see this merged if possible, it would simplify the initialisation code in our app quite a bit!

@raffij

raffij commented Jul 5, 2026

Copy link
Copy Markdown

@dnikolaev is this something you are wanting to finish off yourself?

Adds two @nullable read-only accessors over fields that already exist:

- TwaLauncher#getSession() (public)
- LauncherActivity#getCustomTabsSession() (protected)

This unblocks PostMessage in apps that subclass LauncherActivity
(e.g. Bubblewrap-generated TWAs). Since Chrome 115 enabled postMessage
in TWAs, the active CustomTabsSession is the only entry point to
requestPostMessageChannel / postMessage, but the high-level launcher
held it privately with no accessor.

The session is established asynchronously in onCustomTabsServiceConnected
and cleared in onServiceDisconnected / destroy(); both getters are
@nullable and document the lifecycle. No behavior change for callers
that don't use them. No new fields, threads, lifecycle changes, or
dependencies. No change to session creation timing.

Closes GoogleChrome#472
Closes GoogleChrome#510
Refs GoogleChromeLabs/bubblewrap#954
@dnikolaev
dnikolaev force-pushed the expose-customtabs-session branch from ed5829e to febadb4 Compare August 11, 2026 05:14
@dnikolaev

Copy link
Copy Markdown
Author

@gstepniewski-google Thanks for the review, and apologies for the slow follow-up — both points are now addressed or actionable:

1. CLA — fixed. The check was failing on a Co-Authored-By trailer pointing at an AI assistant's no-reply address, which can never sign the CLA (my own signature was recognized all along). I've rewritten the commit to drop the trailer and rebased onto current main; the PR is now a single commit with the same 2-file / +28 diff, and both cla/google and check-changes are green.

2. The observable question. You're right that the async lifecycle is the sharp edge — and it got sharper with #549 (LauncherActivity now finishes after launch), so I'd rather converge on a shape you'd accept than defend the minimal getter. Why I started with plain accessors:

That said, if you'd prefer a callback, I'm happy to add one in this PR — two shapes that stay small:

a) a protected hook LauncherActivity#onCustomTabsSessionAvailable(CustomTabsSession) invoked when the connection delivers the session; or
b) a registration API on TwaLauncher: addOnSessionAvailableListener(Consumer<CustomTabsSession>), delivered immediately if the session already exists.

Tell me which (either, both, or getter-only) and I'll push it right away.

One scoping note: the channel-reliability problems around postMessage (#510's timing flakiness, the Chrome 150 regression in #602) are Chromium-side and orthogonal — this change is only about reaching the session that already exists.

@raffij yes — finishing this myself; today's rebase + CLA fix are that, and I'll turn the API decision around as soon as there's a signal. @sergio-veed thanks for the support — hoping this gets us over the line.

@gstepniewski-google

Copy link
Copy Markdown
Collaborator

Hi Dima, thanks for working on this!

I agree that the full observable pattern is a bit of an overkill here and we should try to make keep this change smaller.
I like your idea of adding a protected hook to LauncherActivity, it's very simple, to the point, and supports clients who follow this approach (including bubblewrap).
For TwaLauncher, I think we might be able to leverage the already existing completionCallback in launch(): since session is guaranteed to be non-null by the time it fires, calling the getter inside it should be a safe operation - which means we don't need the addListener method (and all the callback management boilerplate that would have to follow).

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bubblewrap/LauncherActivity will not work with post messages Trusted Web Activity Bubblewrap with PostMessage

4 participants