Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c01972e
feat(twa-launcher): allow adding extra to twa intent
dargmuesli Nov 16, 2025
f0a159a
Replace AppCompat dialog with platform AlertDialog
dtonderski-google May 12, 2026
602ea27
Add string translations for provider_unavailable_default_browser
dtonderski-google May 14, 2026
1e3b72d
FocusActivity improvements for reordering tasks to the foreground
gstepniewski-google Jun 17, 2026
ce78b4d
androidbrowserhelper version bump to 2.7.2
gstepniewski-google Jun 17, 2026
399d79d
feat(pblv8): configure PendingPurchasesParams for one-time products (…
SharkMan201 Jun 2, 2026
ccbbb3a
feat(pblv8): refactor queryPurchases to use QueryPurchasesParams (FR-06)
SharkMan201 Jun 2, 2026
5f03be2
feat(pblv8): refactor BillingWrapper and implement product details ma…
SharkMan201 Jun 3, 2026
d2ea99f
feat(pblv8): make listPurchaseHistory return OK/empty list immediatel…
SharkMan201 Jun 3, 2026
162380a
feat(pblv8): upgrade play billing library to v8.3.0 and fix listener …
SharkMan201 Jun 3, 2026
a3638f2
feat(pblv8): bump play billing module version and versionCode (FR-07)
SharkMan201 Jul 28, 2026
c8395ad
Support USE_HIGH_PRI_NOTIFICATIONS manifest metadata for TWA notifica…
Dp-Goog Aug 6, 2026
eb18d51
Resolve comments
Dp-Goog Aug 7, 2026
acc2490
Address comments
Dp-Goog Aug 11, 2026
795fd34
Tests
Dp-Goog Aug 12, 2026
6a6ece4
androidbrowserhelper version bump to 2.7.3
dtonderski-google Aug 12, 2026
bcda8cb
Merge branch 'stable' into main
gdziadosz-google Aug 13, 2026
2bc6a53
Migrate notification channels
Dp-Goog Aug 13, 2026
7613b84
Migrate notification channels
Dp-Goog Aug 13, 2026
2d58139
docs: Add Play Billing Library 8 migration guide
sashalukin Aug 13, 2026
f886b2e
Merge pull request #612 from Dp-Goog/migrate-channels
glennhartmann Aug 14, 2026
e222970
Add TWA high priority notifications test site (#608)
Dp-Goog Aug 14, 2026
c413253
Merge branch 'main' into docs/play-billing-8-migration
gdziadosz-google Aug 18, 2026
57db71d
docs: Apply user edits to migration guide and README
sashalukin Aug 18, 2026
200d5f8
docs: clarify lossy conversion and backwards compatible plans
sashalukin Aug 18, 2026
67933b2
docs: format backwards compatibility note
sashalukin Aug 18, 2026
81f9fbd
Merge pull request #614 from GoogleChrome/docs/play-billing-8-migration
sashalukin Aug 18, 2026
571891c
Update DelegationService to override default notification channel cre…
Dp-Goog Aug 24, 2026
cbecece
Merge branch 'stable' into main
gdziadosz-google Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@
package com.google.androidbrowserhelper.trusted;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.browser.trusted.Token;
import androidx.browser.trusted.TokenStore;
import androidx.browser.trusted.TrustedWebActivityCallbackRemote;
Expand All @@ -31,9 +37,11 @@
/**
* An extension of {@link TrustedWebActivityService} that implements
* {@link TrustedWebActivityService#getTokenStore()} using a
* {@link SharedPreferencesTokenStore}.
* {@link SharedPreferencesTokenStore}, as well as creation of high
* priority notifications if the metadata is set correctly.
*/
public class DelegationService extends TrustedWebActivityService {
private static final String TAG = "DelegationService";
private final List<ExtraCommandHandler> mExtraCommandHandlers = new ArrayList<>();
private SharedPreferencesTokenStore mTokenStore;

Expand Down Expand Up @@ -78,4 +86,40 @@ public Bundle onExtraCommand(
public void registerExtraCommandHandler(ExtraCommandHandler handler) {
mExtraCommandHandlers.add(handler);
}

@Override
@RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS)
public boolean onNotifyNotificationWithChannel(
@NonNull String platformTag,
int platformId,
@NonNull Notification notification,
@NonNull String channelName) {
if (!NotificationUtils.shouldUseHighPriorityNotifications(this)) {
return super.onNotifyNotificationWithChannel(platformTag, platformId, notification, channelName);
}

NotificationUtils.createNotificationChannel(this, channelName);
String channelId = NotificationUtils.channelNameToId(this, channelName);

NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if(!NotificationUtils.areNotificationsEnabled(this, channelName)) {
return false;
}

Notification.Builder builder = Notification.Builder.recoverBuilder(this, notification);
builder.setChannelId(channelId);
notification = builder.build();
}
notificationManager.notify(platformTag, platformId, notification);
return true;
}

@Override
@RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS)
public boolean onAreNotificationsEnabled(@NonNull String channelName) {
return NotificationUtils.areNotificationsEnabled(this, channelName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static boolean areNotificationsEnabled(Context context, String channelNam
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true;

NotificationChannel channel =
NotificationManagerCompat.from(context).getNotificationChannel(channelNameToId(channelName));
NotificationManagerCompat.from(context).getNotificationChannel(channelNameToId(context, channelName));
return channel == null || channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
}

Expand Down Expand Up @@ -111,26 +111,37 @@ public static int getNotificationImportance(Context context) {
* from manifest metadata.
*/
public static void createNotificationChannel(Context context, String channelName) {
createNotificationChannel(context, channelName, getNotificationImportance(context));
createNotificationChannelAndMaybeDeleteOldOne(context, channelName, getNotificationImportance(context));
}

/**
* Creates a notification channel using the given channel name and explicit importance level.
* Creates a notification channel and cleans up the obsolete one.
*/
public static void createNotificationChannel(Context context, String channelName, int importance) {
static void createNotificationChannelAndMaybeDeleteOldOne(Context context, String channelName, int importance) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;

NotificationChannel channel = new NotificationChannel(channelNameToId(channelName),
NotificationChannel channel = new NotificationChannel(channelNameToId(context, channelName),
channelName, importance);
NotificationManagerCompat.from(context).createNotificationChannel(channel);

// Clean up the obsolete channel to prevent duplicates in system settings.
String baseId = channelName.toLowerCase(Locale.ROOT).replace(' ', '_');
String obsoleteChannelId = shouldUseHighPriorityNotifications(context)
? baseId + "_channel_id"
: baseId + "_channel_id_high_pri";

NotificationManagerCompat.from(context).deleteNotificationChannel(obsoleteChannelId);
}

/**
* Generates a notification channel id from a channel name.
* TODO: Remove this when we can use the method defined in AndroidX instead.
*/
@VisibleForTesting
static String channelNameToId(String name) {
return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id";
static String channelNameToId(Context context, String name) {
String baseId = name.toLowerCase(Locale.ROOT).replace(' ', '_');
return shouldUseHighPriorityNotifications(context)
? baseId + "_channel_id_high_pri"
: baseId + "_channel_id";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.robolectric.Shadows.shadowOf;

import android.app.NotificationChannel;
Expand Down Expand Up @@ -163,10 +164,21 @@ public void shouldUseHighPriorityNotifications_matchesExpectedBoolean() {
public void createNotificationChannel_resolvesImportanceCorrectly() {
NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME);

NotificationChannel channel = mNotificationManager.getNotificationChannel(EXPECTED_CHANNEL_ID);
String expectedChannelId = mExpectedShouldUseHighPriority
? "general_notifications_channel_id_high_pri"
: "general_notifications_channel_id";

NotificationChannel channel = mNotificationManager.getNotificationChannel(expectedChannelId);
assertNotNull(channel);
assertEquals(CHANNEL_NAME, channel.getName().toString());
assertEquals(mExpectedImportance, channel.getImportance());

String obsoleteChannelId = mExpectedShouldUseHighPriority
? "general_notifications_channel_id"
: "general_notifications_channel_id_high_pri";

NotificationChannel obsoleteChannel = mNotificationManager.getNotificationChannel(obsoleteChannelId);
assertNull(obsoleteChannel);
}

private void setServiceMetadata(String key, Object value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,11 @@ public void setUp() {
@Test
public void channelNameToId_replacesSpacesAndAppendsSuffix() {
assertEquals("general_notifications_channel_id",
NotificationUtils.channelNameToId("General Notifications"));
NotificationUtils.channelNameToId(mContext, "General Notifications"));
assertEquals("chat_channel_id",
NotificationUtils.channelNameToId("Chat"));
NotificationUtils.channelNameToId(mContext, "Chat"));
assertEquals("test_channel_name_channel_id",
NotificationUtils.channelNameToId("Test Channel Name"));
}

@Test
public void createNotificationChannel_explicitImportanceOverridesMetadata() {
NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);

NotificationChannel channel = mNotificationManager.getNotificationChannel(EXPECTED_CHANNEL_ID);
assertNotNull(channel);
assertEquals(CHANNEL_NAME, channel.getName().toString());
assertEquals(NotificationManager.IMPORTANCE_LOW, channel.getImportance());
NotificationUtils.channelNameToId(mContext, "Test Channel Name"));
}

@Test
Expand Down
8 changes: 8 additions & 0 deletions demos/twa-notification-high-priority/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Trusted Web Activity / High Priority Notification Delegation Demo

This demo application shows how a developer can override notification channels and high-priority notification behaviors using native code and `androidx.browser.trusted.USE_HIGH_PRI_NOTIFICATIONS` metadata.

The demo launches the Notification API sample page at:
`https://dp-goog.github.io/samples/pwa-testing/notificationsapi/`

The relevant debug code lives inside `NotificationDelegationService`.
32 changes: 32 additions & 0 deletions demos/twa-notification-high-priority/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
plugins {
id 'com.android.application'
}

android {
namespace 'com.google.androidbrowserhelper.demos.twa_notification_high_priority'

defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.twa_notification_high_priority"
minSdkVersion 23
compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

buildTypes {
release {
minifyEnabled false
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':androidbrowserhelper')
}
117 changes: 117 additions & 0 deletions demos/twa-notification-high-priority/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">

<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />

<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity">
<meta-data
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
android:value="https://dp-goog.github.io/notifications-api-testing/" />
</activity>

<activity android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
android:label="@string/app_name"
android:exported="true">
<meta-data android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="https://dp-goog.github.io/notifications-api-testing/" />

<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
android:resource="@color/colorPrimary" />

<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
android:resource="@color/colorPrimaryDark" />

<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
android:resource="@color/navigationColor" />

<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
android:resource="@color/navigationColorDark" />

<meta-data android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
android:resource="@drawable/splash"/>

<meta-data android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
android:resource="@color/backgroundColor"/>

<meta-data android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
android:value="300"/>

<meta-data android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
android:value="@string/provider_authority"/>

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="dp-goog.github.io" android:pathPrefix="/notifications-api-testing"/>
</intent-filter>

</activity>

<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />

<activity android:name="com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/provider_authority"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

<service
android:name=".NotificationDelegationService"
android:enabled="true"
android:exported="true">

<meta-data android:name="android.support.customtabs.trusted.SMALL_ICON"
android:resource="@drawable/ic_notification_icon" />

<meta-data
android:name="androidx.browser.trusted.USE_HIGH_PRI_NOTIFICATIONS"
android:value="true" />

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