diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/DelegationService.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/DelegationService.java index 43ef502e..d5438f5b 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/DelegationService.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/DelegationService.java @@ -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; @@ -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 mExtraCommandHandlers = new ArrayList<>(); private SharedPreferencesTokenStore mTokenStore; @@ -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); + } } diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java index dc115d99..550720ea 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java @@ -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; } @@ -111,18 +111,26 @@ 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); } /** @@ -130,7 +138,10 @@ public static void createNotificationChannel(Context context, String channelName * 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"; } } diff --git a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java index 2da76f43..dc900073 100644 --- a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java @@ -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; @@ -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) { diff --git a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java index 52ea88ed..a619317d 100644 --- a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java @@ -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 diff --git a/demos/twa-notification-high-priority/README.md b/demos/twa-notification-high-priority/README.md new file mode 100644 index 00000000..9bf0348a --- /dev/null +++ b/demos/twa-notification-high-priority/README.md @@ -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`. diff --git a/demos/twa-notification-high-priority/build.gradle b/demos/twa-notification-high-priority/build.gradle new file mode 100644 index 00000000..afaca21d --- /dev/null +++ b/demos/twa-notification-high-priority/build.gradle @@ -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') +} diff --git a/demos/twa-notification-high-priority/src/main/AndroidManifest.xml b/demos/twa-notification-high-priority/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b98e72a8 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/AndroidManifest.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/twa-notification-high-priority/src/main/java/com/google/androidbrowserhelper/demos/twa_notification_high_priority/NotificationDelegationService.java b/demos/twa-notification-high-priority/src/main/java/com/google/androidbrowserhelper/demos/twa_notification_high_priority/NotificationDelegationService.java new file mode 100644 index 00000000..76e65760 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/java/com/google/androidbrowserhelper/demos/twa_notification_high_priority/NotificationDelegationService.java @@ -0,0 +1,64 @@ +// 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. + +package com.google.androidbrowserhelper.demos.twa_notification_high_priority; + +import android.app.Notification; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; + +import com.google.androidbrowserhelper.trusted.DelegationService; +import com.google.androidbrowserhelper.trusted.NotificationUtils; + +/** Simple class to log notifications being sent from a higher + * priority channel, with debug logs. + */ +public class NotificationDelegationService extends DelegationService { + private static final String TAG = "NotificationDelegation"; + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "NotificationDelegationService onCreate() triggered"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i(TAG, "NotificationDelegationService onDestroy() triggered"); + } + + @Override + @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) + public boolean onNotifyNotificationWithChannel( + @NonNull String platformTag, + int platformId, + @NonNull Notification notification, + @NonNull String channelName) { + Log.i(TAG, "Notification triggered for channel: " + channelName + + " with importance: " + + NotificationUtils.getNotificationImportance(this)); + return super.onNotifyNotificationWithChannel(platformTag, platformId, notification, channelName); + } + + @Override + @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) + public boolean onAreNotificationsEnabled(@NonNull String channelName) { + boolean enabled = super.onAreNotificationsEnabled(channelName); + Log.i(TAG, "onAreNotificationsEnabled() called for channel: " + channelName + " -> " + enabled); + return enabled; + } +} diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-anydpi/ic_notification_icon.xml b/demos/twa-notification-high-priority/src/main/res/drawable-anydpi/ic_notification_icon.xml new file mode 100644 index 00000000..94b0a2ab --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/drawable-anydpi/ic_notification_icon.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-hdpi/ic_notification_icon.png b/demos/twa-notification-high-priority/src/main/res/drawable-hdpi/ic_notification_icon.png new file mode 100644 index 00000000..c44655ce Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/drawable-hdpi/ic_notification_icon.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-mdpi/ic_notification_icon.png b/demos/twa-notification-high-priority/src/main/res/drawable-mdpi/ic_notification_icon.png new file mode 100644 index 00000000..241bdb8e Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/drawable-mdpi/ic_notification_icon.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/twa-notification-high-priority/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..c478e087 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/ic_notification_icon.png b/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/ic_notification_icon.png new file mode 100644 index 00000000..ba11f018 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/ic_notification_icon.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/splash.png b/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 00000000..8de34211 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/splash.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/drawable-xxhdpi/ic_notification_icon.png b/demos/twa-notification-high-priority/src/main/res/drawable-xxhdpi/ic_notification_icon.png new file mode 100644 index 00000000..e749e2f5 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/drawable-xxhdpi/ic_notification_icon.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/drawable/ic_launcher_background.xml b/demos/twa-notification-high-priority/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..ab8bc42e --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6b78462d --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..287f5053 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..a571e600 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..61da551c Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c41dd285 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..db5080a7 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..6dba46da Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..da31a871 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..15ac6817 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b216f2d3 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..f25a4197 Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..e96783cc Binary files /dev/null and b/demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/demos/twa-notification-high-priority/src/main/res/values/colors.xml b/demos/twa-notification-high-priority/src/main/res/values/colors.xml new file mode 100644 index 00000000..77536b23 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ + + + + #303F9F + #303F9F + #303F9F + #303F9F + #303F9F + #303F9F + diff --git a/demos/twa-notification-high-priority/src/main/res/values/strings.xml b/demos/twa-notification-high-priority/src/main/res/values/strings.xml new file mode 100644 index 00000000..5e38e384 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/values/strings.xml @@ -0,0 +1,24 @@ + + + twa-notification-high-priority + + [{ + \"relation\": [\"delegate_permission/common.handle_all_urls\"], + \"target\": { + \"namespace\": \"web\", + \"site\": \"https://dp-goog.github.io\"} + }] + + com.google.browser.examples.twa_notification_high_priority.fileprovider + diff --git a/demos/twa-notification-high-priority/src/main/res/xml/filepaths.xml b/demos/twa-notification-high-priority/src/main/res/xml/filepaths.xml new file mode 100644 index 00000000..828271b0 --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/res/xml/filepaths.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/playbilling/README.md b/playbilling/README.md index 24520421..ed1d5c30 100644 --- a/playbilling/README.md +++ b/playbilling/README.md @@ -1,12 +1,17 @@ # Play Billing +> [!IMPORTANT] +> **Action Required:** Android Browser Helper Billing 1.2.0 upgrades Google Play Billing Library to version 8.3.0. Apps using older versions will be unable to publish updates to Google Play after August 31, 2026. +> +> See the [Migrate to Google Play Billing Library 8](docs/play-billing-library-8-migration.md) guide for update instructions and details on behavioral changes. + The Play Billing module provides capabilities for your TWA app to connect with [Google Play Billing library](https://developer.android.com/google/play/billing), for example you can: -* Query purchase history +* ~~Query purchase history~~ *(Note: No longer supported as of Play Billing Library 8. See migration guide.)* * Initialize a payment * Query product details -The module uses [Version 7.1.1](https://developer.android.com/google/play/billing/release-notes#7-1-1) of the Google Play Billing library. +The module uses [Version 8.3.0](https://developer.android.com/google/play/billing/release-notes#8-0-0) of the Google Play Billing library. ## How to use it? (Website) diff --git a/playbilling/docs/play-billing-library-8-migration.md b/playbilling/docs/play-billing-library-8-migration.md new file mode 100644 index 00000000..f6a44a66 --- /dev/null +++ b/playbilling/docs/play-billing-library-8-migration.md @@ -0,0 +1,76 @@ +# Migrate Digital Goods API to Google Play Billing Library 8 + +Google Play has announced that starting **August 31, 2026**, all new apps and app updates must use Play Billing Library (PBL) version 8 or newer. To support this, Android Browser Helper's Play Billing module has been updated to `1.2.0`, which uses Play Billing Library 8.3.0. + +While existing apps already published with PBL 7 will continue to function after the deadline, you will not be able to publish any new app updates to the Play Store until you migrate. (An extension path until November 1, 2026 is available via the Play Console for affected apps). + +## Who is affected? + +Any Trusted Web Activity (TWA) app that uses Google Play Billing through the Android Browser Helper (ABH) and the Digital Goods API is affected. Developers using these tools must update their dependencies and rebuild their Android package to continue publishing updates. + +## How to update + +Depending on how your project was generated, follow the appropriate update path below: + +### Bubblewrap users + +If you originally generated your TWA using [Bubblewrap](https://github.com/GoogleChromeLabs/bubblewrap), the easiest way to update is to upgrade your Bubblewrap CLI and regenerate the project. + +Bubblewrap version `1.25.0` updates the default billing dependency to `1.2.0` (and also bumps the `targetSdkVersion` to 36). + +1. Update your Bubblewrap CLI: + ```bash + npm i -g @bubblewrap/cli@1.25.0 + ``` +2. Navigate to your TWA project directory and update the project: + ```bash + cd my-twa-project + bubblewrap update + ``` + *Note: `bubblewrap update` may overwrite manual modifications you have made to the generated Android project files.* +3. Rebuild your app to generate the new APK/AAB: + ```bash + bubblewrap build + ``` + +### Android Browser Helper users + +If you manually maintain your TWA Android project, you only need to update the billing artifact version in your `build.gradle` (or `build.gradle.kts`) file. + +Locate your Play Billing dependency and update it from `1.1.0` to `1.2.0`: + +```gradle +dependencies { + // Update this line + implementation("com.google.androidbrowserhelper:billing:1.2.0") +} +``` + +*Note: The `billing` artifact has its own versioning, separate from the core `androidbrowserhelper` artifact.* + +After updating the dependency, rebuild and publish your new Android App Bundle. + +## Changes to Digital Goods API behavior + +The underlying transition from Play Billing Library 7 to 8 forced two significant changes to how Android Browser Helper implements the Digital Goods API (DGAPI). Please note that the DGAPI web specification has not changed, but the data returned by the Android provider has. + +### `listPurchaseHistory()` + +PBL 8 removed the underlying `queryPurchaseHistoryAsync()` API. To prevent existing web code from crashing, Android Browser Helper 1.2.0 leaves the DGAPI command intact but makes it a no-op. + +When your JavaScript calls: +```javascript +const history = await service.listPurchaseHistory(); +console.log(history); // [] +``` +It will now always return an empty list (`[]`). + +**Action required:** If your app relies on purchase history, you must [track historical purchases on your own backend server](https://developer.android.com/google/play/billing/query-purchase-history#track_historical_purchases), as recommended in the official Google Play Billing documentation. + +### `getDetails()` for subscriptions + +Google Play's subscription model is hierarchical (Subscriptions > Base Plans > Offers), but the DGAPI specification expects a single flat structure (`ItemDetails`). Because of this mismatch, Android Browser Helper now performs a **lossy conversion** for subscriptions and returns the **first eligible element**. + +*Note: Previously, developers could mark a specific base plan or offer as **backward-compatible**, and the legacy `querySkuDetailsAsync()` API would respect that designation. This is no longer the case: the library now returns the first eligible base plan or offer it finds.* + +**Action required:** Because DGAPI can represent only one flat subscription configuration per product, ABH exposes only one of the eligible Play subscription plans/offers returned for that product. Other base plans and offers aren't represented through `getDetails()`. If you use multiple base plans or offers per subscription, be aware that you **cannot currently inspect all of them** via DGAPI. diff --git a/settings.gradle b/settings.gradle index 48798a7f..5299f0bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,6 +45,7 @@ include ':demos:twa-firebase-analytics' include ':demos:twa-location-delegation' include ':demos:twa-multi-domain' include ':demos:twa-notification-delegation' +include ':demos:twa-notification-high-priority' include ':demos:twa-offline-first' include ':demos:twa-orientation' include ':demos:twa-play-billing'