PWABuilder TWA: Clicking Android Web-Push Notification Won’t Bring App to Foreground (Service Worker Click Handler Running)

  Kiến thức lập trình

Context:

I’ve wrapped our web app, built using Meteor and React, using the PWABuilder website and generated Android source code from it. To enable push notifications, we’ve successfully integrated the web-push npm library into our web app. We’re able to trigger push notifications to Android devices from our server without any issues. The notifications are being delivered correctly, and the notification click handler in our service worker is also being invoked and executed as expected.

The problem:

When the TWA app is already open and in focus, clicking a notification triggers the expected page routing (using React Router 6). However, if the app isn’t in focus – either minimized behind another app or on the Android home screen – clicking a notification doesn’t bring the app to the forefront even though the service worker’s notificationClick handler code runs successfully.

Code I got from PWABuilder.com:

Because this is a TWA app built by pwabuilder.com website, I got only three classes

  • LauncherActivity extends com.google.androidbrowserhelper.trusted.LauncherActivity
  • DelegationService extends com.google.androidbrowserhelper.trusted.DelegationService
  • Application extends android.app.Application

If you are familiar with pwabuilder’s code generation, you would know the above classes are mere skeletons.

In the android manifest (AndroidManifest.xml), there are 4 activities generated.

  • android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
  • android:name="LauncherActivity"
  • android:name="com.google.androidbrowserhelper.trusted.FocusActivity"
  • android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"

And 1 service using DelegationService having the below intent

<intent-filter>
                <action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>

Things I did:

Since the default behavior wasn’t bringing the TWA app to the front, I’ve experimented with code modifications.

I understood that <intent-filter> elements are responsible for handling intents from the Android OS. The existing LauncherActivity already has intent filters, and the default intents function as expected when launching the app by clicking its icon.

I introduced a custom scheme mytwaapp for opening the app forcefully, as shown in the following code snippet.

<intent-filter android:label="mytwaapp">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="mytwaapp"
                    android:host="@string/hostName" />
            </intent-filter>

And in my service worker’s notificationClick handler, I have updated the code like below to forcefully open the app, but no luck.

self.clients.openWindow(
          `intent://example.com/#Intent;scheme=mytwaapp;package=com.mytwaapp.app.twa;end`
        );

Intent Scheme Validation:

To validate the intent scheme registration with Android, I implemented a temporary button within the webapp with the following code:

window.open('intent://example.com/#Intent;scheme=mytwaapp;package=com.mytwaapp.app.twa;end');

Clicking this button within Android’s Chrome browser successfully opened the TWA app, confirming the scheme’s proper registration with the OS. This further highlights the challenge of replicating this behavior from within the service worker’s notification click handler.

I’ve examined the logs in Android Studio LogCat while running the app and clicking notifications. Below are the relevant logs I could gather to understand what is happening.

Summarized / cleaned mog messages appeared chronologically:

  • Transition requested: android.os.BinderProxy@29e3438 TransitionRequestInfo
  • START
  • Received a notification intent in the NotificationServiceu0027s receiver.
  • Sent Transition #106 createdAtu003d01-16 11:24:08.780 via requestu003dTransitionRequestInfo
  • Dispatching notification event to native: p#https://my-local-app.ngrok-free.app/#010025
    [INFO:notification_event_dispatcher_impl.cc(54)] The notification event has finished: – Operation has succeeded
  • Initiating notification refresh from MarkAsNotifiedHandler
    — log ends. verbose logs are given at the bottom —

Investigating Trampoline Activity:

Further analysis of the logs revealed potential limitations associated with Trampoline Activity on Android 12 and above. As the documentation explains:

“When users interact with notifications, some apps respond by
launching an intermediary component before displaying the main
activity. This component is called a Trampoline Activity. To improve
performance and user experience, apps targeting Android 12+ cannot
start activities from services or broadcast receivers used as
notification trampolines. This means your app can’t call
startActivity() within a service or broadcast receiver when a user
taps a notification or an action button within it.”

The TWA Dilemma:

Interestingly, despite being a TWA app, I didn’t encounter the specific error mentioned in the documentation within Logcat. This led me to further research and conversations with chat assistants, where I learned about the potential of using PendingIntent within the notification click handler to circumvent the issue. However, this approach proved challenging due to the lack of notification builder capabilities within TWA apps.

This impasse prompted me to consider alternative solutions, such as:

  • Integrating FCM (Firebase Cloud Messaging) directly within the webapp for Android devices.
  • Developing a native app with FCM implementation and loading the website within a WebView.

I’m currently evaluating these options to find the most efficient solution for bringing the TWA app to the forefront after a notification click, regardless of its initial state.

Summary:

Despite the service worker handling notification clicks effectively, bringing the TWA app to focus when not already open remains a challenge. Trampoline Activity restrictions on Android 12+ and limitations of notification builder capabilities in TWAs complicate matters.

Questions:

  1. Can TWA apps reliably bring themselves to focus upon notification click, even when in the background, while adhering to Android’s Trampoline Activity restrictions?

Are alternative approaches like:

  1. Integrating FCM directly in the webapp for Android devices within the TWA framework
  2. Developing a native app with FCM and loading the website in a WebView?

viable paths to overcome this notification click focus issue?

Verbose logs:

 "message": "Transition requested: android.os.BinderProxy@29e3438 TransitionRequestInfo { type u003d OPEN, triggerTask u003d TaskInfo{userIdu003d0 taskIdu003d45 displayIdu003d0 isRunningu003dtrue baseIntentu003dIntent { actu003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flgu003d0x188c0000 cmpu003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity } baseActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} topActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} origActivityu003dnull realActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} numActivitiesu003d1 lastActiveTimeu003d65382793 supportsMultiWindowu003dtrue resizeModeu003d1 isResizeableu003dtrue minWidthu003d-1 minHeightu003d-1 defaultMinSizeu003d220 tokenu003dWCT{android.window.IWindowContainerToken$Stub$Proxy@c957c11} topActivityTypeu003d1 pictureInPictureParamsu003dnull shouldDockBigOverlaysu003dfalse launchIntoPipHostTaskIdu003d-1 lastParentTaskIdBeforePipu003d-1 displayCutoutSafeInsetsu003dnull topActivityInfou003dActivityInfo{d993176 org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} launchCookiesu003d[] positionInParentu003dPoint(0, 0) parentTaskIdu003d-1 isFocusedu003dfalse isVisibleu003dfalse isVisibleRequestedu003dfalse isSleepingu003dfalse topActivityInSizeCompatu003dfalse topActivityEligibleForLetterboxEducationu003d false topActivityLetterboxedu003d false isFromDoubleTapu003d false topActivityLetterboxVerticalPositionu003d -1 topActivityLetterboxHorizontalPositionu003d -1 topActivityLetterboxWidthu003d-1 topActivityLetterboxHeightu003d-1 locusIdu003dnull displayAreaFeatureIdu003d1 cameraCompatControlStateu003dhidden}, remoteTransition u003d RemoteTransition { remoteTransition u003d com.android.systemui.shared.system.RemoteAnimationRunnerCompat$1@e903377, appThread u003d null, debugName u003d SysUILaunch }, displayChange u003d null }"


 "message": "START u0 {actu003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flgu003d0x40000 cmpu003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity (has extras)} with LAUNCH_MULTIPLE from uid 10136 (realCallingUidu003d10166) (BAL_ALLOW_PENDING_INTENT) result codeu003d0"


 "message": "Received a notification intent in the NotificationServiceu0027s receiver."

  "message": "Sent Transition #106 createdAtu003d01-16 11:24:08.780 via requestu003dTransitionRequestInfo { type u003d OPEN, triggerTask u003d TaskInfo{userIdu003d0 taskIdu003d45 displayIdu003d0 isRunningu003dtrue baseIntentu003dIntent { actu003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flgu003d0x188c0000 cmpu003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity } baseActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} topActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} origActivityu003dnull realActivityu003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} numActivitiesu003d1 lastActiveTimeu003d65382793 supportsMultiWindowu003dtrue resizeModeu003d1 isResizeableu003dtrue minWidthu003d-1 minHeightu003d-1 defaultMinSizeu003d220 tokenu003dWCT{RemoteToken{8b071b3 Task{4dc2d79 #45 typeu003dstandard Au003d10136:com.android.chrome}}} topActivityTypeu003d1 pictureInPictureParamsu003dnull shouldDockBigOverlaysu003dfalse launchIntoPipHostTaskIdu003d-1 lastParentTaskIdBeforePipu003d-1 displayCutoutSafeInsetsu003dnull topActivityInfou003dActivityInfo{9372370 org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} launchCookiesu003d[] positionInParentu003dPoint(0, 0) parentTaskIdu003d-1 isFocusedu003dfalse isVisibleu003dfalse isVisibleRequestedu003dfalse isSleepingu003dfalse topActivityInSizeCompatu003dfalse topActivityEligibleForLetterboxEducationu003d false topActivityLetterboxedu003d false isFromDoubleTapu003d false topActivityLetterboxVerticalPositionu003d -1 topActivityLetterboxHorizontalPositionu003d -1 topActivityLetterboxWidthu003d-1 topActivityLetterboxHeightu003d-1 locusIdu003dnull displayAreaFeatureIdu003d1 cameraCompatControlStateu003dhidden}, remoteTransition u003d RemoteTransition { remoteTransition u003d android.window.IRemoteTransition$Stub$Proxy@1e666e9, appThread u003d null, debugName u003d SysUILaunch }, displayChange u003d null }"

  "message": "Dispatching notification event to native: p#https://annually-flowing-octopus.ngrok-free.app/#010025"

  "message": "[INFO:notification_event_dispatcher_impl.cc(54)] The notification event has finished: Operation has succeeded"

  "processName": "com.android.chrome:sandboxed_process1:org.chromium.content.app.SandboxedProcessService1:4",
  "message": "Initiating notification refresh from MarkAsNotifiedHandler"

1

LEAVE A COMMENT