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

153 views Asked by At

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 NotificationService\u0027s receiver.
  • Sent Transition #106 createdAt\u003d01-16 11:24:08.780 via request\u003dTransitionRequestInfo
  • 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{userId\u003d0 taskId\u003d45 displayId\u003d0 isRunning\u003dtrue baseIntent\u003dIntent { act\u003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flg\u003d0x188c0000 cmp\u003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity } baseActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} topActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} origActivity\u003dnull realActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} numActivities\u003d1 lastActiveTime\u003d65382793 supportsMultiWindow\u003dtrue resizeMode\u003d1 isResizeable\u003dtrue minWidth\u003d-1 minHeight\u003d-1 defaultMinSize\u003d220 token\u003dWCT{android.window.IWindowContainerToken$Stub$Proxy@c957c11} topActivityType\u003d1 pictureInPictureParams\u003dnull shouldDockBigOverlays\u003dfalse launchIntoPipHostTaskId\u003d-1 lastParentTaskIdBeforePip\u003d-1 displayCutoutSafeInsets\u003dnull topActivityInfo\u003dActivityInfo{d993176 org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} launchCookies\u003d[] positionInParent\u003dPoint(0, 0) parentTaskId\u003d-1 isFocused\u003dfalse isVisible\u003dfalse isVisibleRequested\u003dfalse isSleeping\u003dfalse topActivityInSizeCompat\u003dfalse topActivityEligibleForLetterboxEducation\u003d false topActivityLetterboxed\u003d false isFromDoubleTap\u003d false topActivityLetterboxVerticalPosition\u003d -1 topActivityLetterboxHorizontalPosition\u003d -1 topActivityLetterboxWidth\u003d-1 topActivityLetterboxHeight\u003d-1 locusId\u003dnull displayAreaFeatureId\u003d1 cameraCompatControlState\u003dhidden}, 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 {act\u003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flg\u003d0x40000 cmp\u003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity (has extras)} with LAUNCH_MULTIPLE from uid 10136 (realCallingUid\u003d10166) (BAL_ALLOW_PENDING_INTENT) result code\u003d0"


 "message": "Received a notification intent in the NotificationService\u0027s receiver."

  "message": "Sent Transition #106 createdAt\u003d01-16 11:24:08.780 via request\u003dTransitionRequestInfo { type \u003d OPEN, triggerTask \u003d TaskInfo{userId\u003d0 taskId\u003d45 displayId\u003d0 isRunning\u003dtrue baseIntent\u003dIntent { act\u003dnotifications.NotificationIntentInterceptor.INTENT_ACTION flg\u003d0x188c0000 cmp\u003dcom.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity } baseActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} topActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} origActivity\u003dnull realActivity\u003dComponentInfo{com.android.chrome/org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} numActivities\u003d1 lastActiveTime\u003d65382793 supportsMultiWindow\u003dtrue resizeMode\u003d1 isResizeable\u003dtrue minWidth\u003d-1 minHeight\u003d-1 defaultMinSize\u003d220 token\u003dWCT{RemoteToken{8b071b3 Task{4dc2d79 #45 type\u003dstandard A\u003d10136:com.android.chrome}}} topActivityType\u003d1 pictureInPictureParams\u003dnull shouldDockBigOverlays\u003dfalse launchIntoPipHostTaskId\u003d-1 lastParentTaskIdBeforePip\u003d-1 displayCutoutSafeInsets\u003dnull topActivityInfo\u003dActivityInfo{9372370 org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$TrampolineActivity} launchCookies\u003d[] positionInParent\u003dPoint(0, 0) parentTaskId\u003d-1 isFocused\u003dfalse isVisible\u003dfalse isVisibleRequested\u003dfalse isSleeping\u003dfalse topActivityInSizeCompat\u003dfalse topActivityEligibleForLetterboxEducation\u003d false topActivityLetterboxed\u003d false isFromDoubleTap\u003d false topActivityLetterboxVerticalPosition\u003d -1 topActivityLetterboxHorizontalPosition\u003d -1 topActivityLetterboxWidth\u003d-1 topActivityLetterboxHeight\u003d-1 locusId\u003dnull displayAreaFeatureId\u003d1 cameraCompatControlState\u003dhidden}, 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"

0

There are 0 answers