From c01972e6c2f8413d1f589161e3d26bcbaf819aa2 Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Sun, 16 Nov 2025 05:01:51 +0100 Subject: [PATCH 01/24] feat(twa-launcher): allow adding extra to twa intent Allows setting custom headers. Resolves https://github.com/GoogleChrome/android-browser-helper/issues/329 --- .../androidbrowserhelper/trusted/TwaLauncher.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java index 25d472d8..4d002459 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java @@ -355,7 +355,7 @@ private void launchWhenSplashScreenReady(TrustedWebActivityIntentBuilder builder // for further details. } Log.d(TAG, "Launching Trusted Web Activity."); - TrustedWebActivityIntent intent = builder.build(mSession); + TrustedWebActivityIntent intent = onPrepareIntent(builder.build(mSession)); if (mStartupUptimeMillis != 0) { intent.getIntent().putExtra(EXTRA_STARTUP_UPTIME_MILLIS, mStartupUptimeMillis); } @@ -369,6 +369,18 @@ private void launchWhenSplashScreenReady(TrustedWebActivityIntentBuilder builder } } + /** + * Hook method for subclasses to customize the Trusted Web Activity Intent before launch. + * This method is called with the built Intent and can be overridden to add custom headers, + * extras, or other modifications. + * + * @param intent the built TrustedWebActivityIntent that will be launched + * @return the (potentially modified) TrustedWebActivityIntent to launch + */ + protected TrustedWebActivityIntent onPrepareIntent(TrustedWebActivityIntent intent) { + return intent; + } + /** * Performs clean-up. */ From f0a159a861e89aedc4e7a8478b0be1374ba60348 Mon Sep 17 00:00:00 2001 From: David Tonderski Date: Tue, 12 May 2026 12:14:34 +0000 Subject: [PATCH 02/24] Replace AppCompat dialog with platform AlertDialog --- .../androidbrowserhelper/trusted/TwaLauncher.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java index 4d002459..f0e27fe8 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java @@ -15,9 +15,10 @@ package com.google.androidbrowserhelper.trusted; -import static androidx.appcompat.R.style.Theme_AppCompat_DayNight_Dialog_Alert; +import static android.R.style.Theme_DeviceDefault_Dialog_Alert; import android.app.Activity; +import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -28,10 +29,6 @@ import android.text.TextUtils; import android.util.Log; -import com.google.androidbrowserhelper.trusted.splashscreens.SplashScreenStrategy; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.ContextThemeWrapper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -440,8 +437,7 @@ private static void showBrowserUnavailableDialog(Activity activity, boolean quer } } - AlertDialog.Builder builder = new AlertDialog.Builder( - new ContextThemeWrapper(activity, Theme_AppCompat_DayNight_Dialog_Alert)); + AlertDialog.Builder builder = new AlertDialog.Builder(activity, Theme_DeviceDefault_Dialog_Alert); builder.setTitle(R.string.provider_unavailable_title); if (TextUtils.isEmpty(browserName)) { From 602ea279230c4745306b5d1de5c4843a2772fc89 Mon Sep 17 00:00:00 2001 From: David Tonderski Date: Thu, 14 May 2026 14:48:33 +0000 Subject: [PATCH 03/24] Add string translations for provider_unavailable_default_browser --- androidbrowserhelper/src/main/res/values-af/strings.xml | 1 + androidbrowserhelper/src/main/res/values-am/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ar/strings.xml | 1 + androidbrowserhelper/src/main/res/values-as/strings.xml | 1 + androidbrowserhelper/src/main/res/values-az/strings.xml | 1 + androidbrowserhelper/src/main/res/values-b+es+419/strings.xml | 1 + androidbrowserhelper/src/main/res/values-b+sr+Latn/strings.xml | 1 + androidbrowserhelper/src/main/res/values-be/strings.xml | 1 + androidbrowserhelper/src/main/res/values-bg/strings.xml | 1 + androidbrowserhelper/src/main/res/values-bn/strings.xml | 1 + androidbrowserhelper/src/main/res/values-bs/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ca/strings.xml | 1 + androidbrowserhelper/src/main/res/values-cs/strings.xml | 1 + androidbrowserhelper/src/main/res/values-da/strings.xml | 1 + androidbrowserhelper/src/main/res/values-de/strings.xml | 1 + androidbrowserhelper/src/main/res/values-el/strings.xml | 1 + androidbrowserhelper/src/main/res/values-en-rGB/strings.xml | 1 + androidbrowserhelper/src/main/res/values-es-rUS/strings.xml | 1 + androidbrowserhelper/src/main/res/values-es/strings.xml | 1 + androidbrowserhelper/src/main/res/values-et/strings.xml | 1 + androidbrowserhelper/src/main/res/values-eu/strings.xml | 1 + androidbrowserhelper/src/main/res/values-fa/strings.xml | 1 + androidbrowserhelper/src/main/res/values-fi/strings.xml | 1 + androidbrowserhelper/src/main/res/values-fil/strings.xml | 1 + androidbrowserhelper/src/main/res/values-fr-rCA/strings.xml | 1 + androidbrowserhelper/src/main/res/values-fr/strings.xml | 1 + androidbrowserhelper/src/main/res/values-gl/strings.xml | 1 + androidbrowserhelper/src/main/res/values-gu/strings.xml | 1 + androidbrowserhelper/src/main/res/values-he/strings.xml | 1 + androidbrowserhelper/src/main/res/values-hi/strings.xml | 1 + androidbrowserhelper/src/main/res/values-hr/strings.xml | 1 + androidbrowserhelper/src/main/res/values-hu/strings.xml | 1 + androidbrowserhelper/src/main/res/values-hy/strings.xml | 1 + androidbrowserhelper/src/main/res/values-id/strings.xml | 1 + androidbrowserhelper/src/main/res/values-in/strings.xml | 1 + androidbrowserhelper/src/main/res/values-is/strings.xml | 1 + androidbrowserhelper/src/main/res/values-it/strings.xml | 1 + androidbrowserhelper/src/main/res/values-iw/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ja/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ka/strings.xml | 1 + androidbrowserhelper/src/main/res/values-kk/strings.xml | 1 + androidbrowserhelper/src/main/res/values-km/strings.xml | 1 + androidbrowserhelper/src/main/res/values-kn/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ko/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ky/strings.xml | 1 + androidbrowserhelper/src/main/res/values-lo/strings.xml | 1 + androidbrowserhelper/src/main/res/values-lt/strings.xml | 1 + androidbrowserhelper/src/main/res/values-lv/strings.xml | 1 + androidbrowserhelper/src/main/res/values-mk/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ml/strings.xml | 1 + androidbrowserhelper/src/main/res/values-mn/strings.xml | 1 + androidbrowserhelper/src/main/res/values-mr/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ms/strings.xml | 1 + androidbrowserhelper/src/main/res/values-my/strings.xml | 1 + androidbrowserhelper/src/main/res/values-nb/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ne/strings.xml | 1 + androidbrowserhelper/src/main/res/values-nl/strings.xml | 1 + androidbrowserhelper/src/main/res/values-no/strings.xml | 1 + androidbrowserhelper/src/main/res/values-or/strings.xml | 1 + androidbrowserhelper/src/main/res/values-pa/strings.xml | 1 + androidbrowserhelper/src/main/res/values-pl/strings.xml | 1 + androidbrowserhelper/src/main/res/values-pt-rBR/strings.xml | 1 + androidbrowserhelper/src/main/res/values-pt-rPT/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ro/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ru/strings.xml | 1 + androidbrowserhelper/src/main/res/values-si/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sk/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sl/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sq/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sr/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sv/strings.xml | 1 + androidbrowserhelper/src/main/res/values-sw/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ta/strings.xml | 1 + androidbrowserhelper/src/main/res/values-te/strings.xml | 1 + androidbrowserhelper/src/main/res/values-th/strings.xml | 1 + androidbrowserhelper/src/main/res/values-tl/strings.xml | 1 + androidbrowserhelper/src/main/res/values-tr/strings.xml | 1 + androidbrowserhelper/src/main/res/values-uk/strings.xml | 1 + androidbrowserhelper/src/main/res/values-ur/strings.xml | 1 + androidbrowserhelper/src/main/res/values-uz/strings.xml | 1 + androidbrowserhelper/src/main/res/values-vi/strings.xml | 1 + androidbrowserhelper/src/main/res/values-zh-rCN/strings.xml | 1 + androidbrowserhelper/src/main/res/values-zh-rHK/strings.xml | 1 + androidbrowserhelper/src/main/res/values-zh-rTW/strings.xml | 1 + androidbrowserhelper/src/main/res/values-zu/strings.xml | 1 + androidbrowserhelper/src/main/res/values/strings.xml | 2 +- 86 files changed, 86 insertions(+), 1 deletion(-) diff --git a/androidbrowserhelper/src/main/res/values-af/strings.xml b/androidbrowserhelper/src/main/res/values-af/strings.xml index b56cf7e8..64651681 100755 --- a/androidbrowserhelper/src/main/res/values-af/strings.xml +++ b/androidbrowserhelper/src/main/res/values-af/strings.xml @@ -6,6 +6,7 @@ Hierdie app bevat geen blaaierdata nie. App is onbeskikbaar Hierdie app is onbeskikbaar omdat %1$s geblokkeer is + Hierdie app is onbeskikbaar. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-am/strings.xml b/androidbrowserhelper/src/main/res/values-am/strings.xml index be614720..faf7e16e 100755 --- a/androidbrowserhelper/src/main/res/values-am/strings.xml +++ b/androidbrowserhelper/src/main/res/values-am/strings.xml @@ -6,6 +6,7 @@ ይህ መተግበሪያ ምንም የአሰሳ ውሂብ አይዝም። መተግበሪያ አይገኝም %1$s ስለታገደ ይህ መተግበሪያ አይገኝም + ይህ መተግበሪያ አይገኝም። እሺ Chrome diff --git a/androidbrowserhelper/src/main/res/values-ar/strings.xml b/androidbrowserhelper/src/main/res/values-ar/strings.xml index fecb557d..48f24aff 100755 --- a/androidbrowserhelper/src/main/res/values-ar/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ar/strings.xml @@ -6,6 +6,7 @@ لا يحتفظ هذا التطبيق بأي بيانات تصفُّح. التطبيق غير متوفّر هذا التطبيق غير متوفّر بسبب حظر \"%1$s\" + هذا التطبيق غير متوفّر. حسنًا Chrome diff --git a/androidbrowserhelper/src/main/res/values-as/strings.xml b/androidbrowserhelper/src/main/res/values-as/strings.xml index 5912ebc8..f81c40da 100755 --- a/androidbrowserhelper/src/main/res/values-as/strings.xml +++ b/androidbrowserhelper/src/main/res/values-as/strings.xml @@ -6,6 +6,7 @@ এই এপ্‌টোৱে কোনো ব্ৰাউজিঙৰ ডেটা ধৰি নাৰাখে। এপ্‌ উপলব্ধ নহয় %1$s অৱৰোধ কৰাৰ বাবে এই এপ্‌টো উপলব্ধ নহয় + এই এপ্‌টো উপলব্ধ নহয়। ঠিক আছে Chrome diff --git a/androidbrowserhelper/src/main/res/values-az/strings.xml b/androidbrowserhelper/src/main/res/values-az/strings.xml index 85543dd0..38722c8d 100755 --- a/androidbrowserhelper/src/main/res/values-az/strings.xml +++ b/androidbrowserhelper/src/main/res/values-az/strings.xml @@ -6,6 +6,7 @@ Bu tətbiqdə axtarış datası yoxdur. Tətbiq əlçatan deyil %1$s blok olunduğu üçün bu tətbiq əlçatan deyil + Bu tətbiq əlçatan deyil. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-b+es+419/strings.xml b/androidbrowserhelper/src/main/res/values-b+es+419/strings.xml index f86d8325..5f233a87 100755 --- a/androidbrowserhelper/src/main/res/values-b+es+419/strings.xml +++ b/androidbrowserhelper/src/main/res/values-b+es+419/strings.xml @@ -6,6 +6,7 @@ Esta app no contiene datos de navegación. App no disponible Esta app no está disponible porque el proveedor %1$s está bloqueado + Esta app no está disponible. Aceptar Chrome diff --git a/androidbrowserhelper/src/main/res/values-b+sr+Latn/strings.xml b/androidbrowserhelper/src/main/res/values-b+sr+Latn/strings.xml index 46f29895..ee063d0b 100755 --- a/androidbrowserhelper/src/main/res/values-b+sr+Latn/strings.xml +++ b/androidbrowserhelper/src/main/res/values-b+sr+Latn/strings.xml @@ -6,6 +6,7 @@ Ova aplikacija ne čuva podatke pregledanja. Aplikacija nije dostupna Ova aplikacija nije dostupna jer je pregledač %1$s blokiran + Ova aplikacija je nedostupna. Potvrdi Chrome diff --git a/androidbrowserhelper/src/main/res/values-be/strings.xml b/androidbrowserhelper/src/main/res/values-be/strings.xml index 454ee155..c529ef7e 100755 --- a/androidbrowserhelper/src/main/res/values-be/strings.xml +++ b/androidbrowserhelper/src/main/res/values-be/strings.xml @@ -6,6 +6,7 @@ Гэта праграма не змяшчае даныя пра работу ў браўзеры. Праграма недаступная Гэта праграма недаступная, бо праца \"%1$s\" заблакіравана + Гэта праграма недаступная. ОК Chrome diff --git a/androidbrowserhelper/src/main/res/values-bg/strings.xml b/androidbrowserhelper/src/main/res/values-bg/strings.xml index da2f80c6..af5d7b7b 100755 --- a/androidbrowserhelper/src/main/res/values-bg/strings.xml +++ b/androidbrowserhelper/src/main/res/values-bg/strings.xml @@ -6,6 +6,7 @@ Това приложение не съдържа данни за сърфирането. Приложението не е налице Това приложение не е налице, защото %1$s е блокирано + Това приложение не е налице. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-bn/strings.xml b/androidbrowserhelper/src/main/res/values-bn/strings.xml index e16419e3..8e14783e 100755 --- a/androidbrowserhelper/src/main/res/values-bn/strings.xml +++ b/androidbrowserhelper/src/main/res/values-bn/strings.xml @@ -6,6 +6,7 @@ এই অ্যাপে কোনও ব্রাউজ করা ডেটা নেই। অ্যাপ উপলভ্য নেই %1$s ব্লক করা রয়েছে বলে এই অ্যাপ উপলভ্য নেই + এই অ্যাপ উপলভ্য নেই। ঠিক আছে Chrome diff --git a/androidbrowserhelper/src/main/res/values-bs/strings.xml b/androidbrowserhelper/src/main/res/values-bs/strings.xml index e19214f5..0061e871 100755 --- a/androidbrowserhelper/src/main/res/values-bs/strings.xml +++ b/androidbrowserhelper/src/main/res/values-bs/strings.xml @@ -6,6 +6,7 @@ Aplikacija ne sadržava podatke o pregledanju. Aplikacija nije dostupna Aplikacija nije dostupna jer je pružalac usluga %1$s blokiran + Ta aplikacija nije dostupna. Uredu Chrome diff --git a/androidbrowserhelper/src/main/res/values-ca/strings.xml b/androidbrowserhelper/src/main/res/values-ca/strings.xml index 5a18b0e4..f6190ee4 100755 --- a/androidbrowserhelper/src/main/res/values-ca/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ca/strings.xml @@ -6,6 +6,7 @@ Aquesta aplicació no conserva dades de navegació. Aplicació no disponible Aquesta aplicació no està disponible perquè %1$s està bloquejat + Aquesta aplicació no està disponible. D\'acord Chrome diff --git a/androidbrowserhelper/src/main/res/values-cs/strings.xml b/androidbrowserhelper/src/main/res/values-cs/strings.xml index 2be8ae61..81cd2142 100755 --- a/androidbrowserhelper/src/main/res/values-cs/strings.xml +++ b/androidbrowserhelper/src/main/res/values-cs/strings.xml @@ -6,6 +6,7 @@ Tato aplikace neuchovává žádné údaje o prohlížení. Aplikace není dostupná Tato aplikace není k dispozici, protože je blokován poskytovatel %1$s + Tato aplikace není k dispozici. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-da/strings.xml b/androidbrowserhelper/src/main/res/values-da/strings.xml index 225225ee..40ff78fe 100755 --- a/androidbrowserhelper/src/main/res/values-da/strings.xml +++ b/androidbrowserhelper/src/main/res/values-da/strings.xml @@ -6,6 +6,7 @@ Denne app indeholder ingen browserdata. Appen er ikke tilgængelig Denne app er ikke tilgængelig, fordi %1$s er blokeret + Denne app er ikke tilgængelig. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-de/strings.xml b/androidbrowserhelper/src/main/res/values-de/strings.xml index e5f0fdbd..95d205ee 100755 --- a/androidbrowserhelper/src/main/res/values-de/strings.xml +++ b/androidbrowserhelper/src/main/res/values-de/strings.xml @@ -6,6 +6,7 @@ Diese App enthält keine Browserdaten. App nicht verfügbar Diese App ist nicht verfügbar, weil %1$s blockiert ist + Diese App ist nicht verfügbar. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-el/strings.xml b/androidbrowserhelper/src/main/res/values-el/strings.xml index 1f232aff..649e668a 100755 --- a/androidbrowserhelper/src/main/res/values-el/strings.xml +++ b/androidbrowserhelper/src/main/res/values-el/strings.xml @@ -6,6 +6,7 @@ Αυτή η εφαρμογή δεν διατηρεί δεδομένα περιήγησης. Η εφαρμογή δεν είναι διαθέσιμη Αυτή η εφαρμογή δεν είναι διαθέσιμη, επειδή ο πάροχος %1$s έχει αποκλειστεί + Αυτή η εφαρμογή δεν είναι διαθέσιμη. ΟΚ Chrome diff --git a/androidbrowserhelper/src/main/res/values-en-rGB/strings.xml b/androidbrowserhelper/src/main/res/values-en-rGB/strings.xml index 4ab424ea..28fed328 100755 --- a/androidbrowserhelper/src/main/res/values-en-rGB/strings.xml +++ b/androidbrowserhelper/src/main/res/values-en-rGB/strings.xml @@ -6,6 +6,7 @@ This app holds no browsing data. App unavailable This app is unavailable because %1$s is blocked + This app is unavailable. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-es-rUS/strings.xml b/androidbrowserhelper/src/main/res/values-es-rUS/strings.xml index f86d8325..5f233a87 100755 --- a/androidbrowserhelper/src/main/res/values-es-rUS/strings.xml +++ b/androidbrowserhelper/src/main/res/values-es-rUS/strings.xml @@ -6,6 +6,7 @@ Esta app no contiene datos de navegación. App no disponible Esta app no está disponible porque el proveedor %1$s está bloqueado + Esta app no está disponible. Aceptar Chrome diff --git a/androidbrowserhelper/src/main/res/values-es/strings.xml b/androidbrowserhelper/src/main/res/values-es/strings.xml index 339d1500..c1bb93f8 100755 --- a/androidbrowserhelper/src/main/res/values-es/strings.xml +++ b/androidbrowserhelper/src/main/res/values-es/strings.xml @@ -6,6 +6,7 @@ Esta aplicación no conserva datos de navegación. Aplicación no disponible Esta aplicación no está disponible porque %1$s está bloqueado + Esta aplicación no está disponible. Aceptar Chrome diff --git a/androidbrowserhelper/src/main/res/values-et/strings.xml b/androidbrowserhelper/src/main/res/values-et/strings.xml index 853a6f6d..134ae082 100755 --- a/androidbrowserhelper/src/main/res/values-et/strings.xml +++ b/androidbrowserhelper/src/main/res/values-et/strings.xml @@ -6,6 +6,7 @@ See rakendus ei sisalda sirvimisandmeid. Rakendus pole saadaval See rakendus pole saadaval, kuna %1$s on blokeeritud. + Rakendus ei ole saadaval. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-eu/strings.xml b/androidbrowserhelper/src/main/res/values-eu/strings.xml index 4e35c2ad..a73cf55c 100755 --- a/androidbrowserhelper/src/main/res/values-eu/strings.xml +++ b/androidbrowserhelper/src/main/res/values-eu/strings.xml @@ -6,6 +6,7 @@ Aplikazio honek ez du arakatze-daturik. Aplikazioa ez dago erabilgarri Aplikazioa ez dago erabilgarri %1$s blokeatuta dagoelako + Aplikazioa ez dago erabilgarri. Ados Chrome diff --git a/androidbrowserhelper/src/main/res/values-fa/strings.xml b/androidbrowserhelper/src/main/res/values-fa/strings.xml index fd26ed4e..e9d47d40 100755 --- a/androidbrowserhelper/src/main/res/values-fa/strings.xml +++ b/androidbrowserhelper/src/main/res/values-fa/strings.xml @@ -6,6 +6,7 @@ این برنامه داده‌های مرور ندارد. برنامه دردسترس نیست این برنامه دردسترس نیست زیرا %1$s موقتاً مسدود شده است + این برنامه دردسترس نیست. بسیارخوب Chrome diff --git a/androidbrowserhelper/src/main/res/values-fi/strings.xml b/androidbrowserhelper/src/main/res/values-fi/strings.xml index 2fb5f2bf..1146f766 100755 --- a/androidbrowserhelper/src/main/res/values-fi/strings.xml +++ b/androidbrowserhelper/src/main/res/values-fi/strings.xml @@ -6,6 +6,7 @@ Tämä sovellus ei sisällä selausdataa. Sovellus ei käytettävissä Sovellus ei ole käytettävissä, koska %1$s on estetty + Sovellus ei ole käytettävissä. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-fil/strings.xml b/androidbrowserhelper/src/main/res/values-fil/strings.xml index 89970123..ed8a6e2d 100755 --- a/androidbrowserhelper/src/main/res/values-fil/strings.xml +++ b/androidbrowserhelper/src/main/res/values-fil/strings.xml @@ -6,6 +6,7 @@ Walang data mula sa pag-browse ang app na ito. Hindi Available ang App Hindi available ang app na ito dahil naka-block ang %1$s + Hindi available ang app na ito. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-fr-rCA/strings.xml b/androidbrowserhelper/src/main/res/values-fr-rCA/strings.xml index 05f8a496..b0537185 100755 --- a/androidbrowserhelper/src/main/res/values-fr-rCA/strings.xml +++ b/androidbrowserhelper/src/main/res/values-fr-rCA/strings.xml @@ -6,6 +6,7 @@ Cette appli ne contient aucune donnée de navigation. Appli non disponible Cette appli n\'est pas accessible, car %1$s est bloqué + Cette appli n\'est pas accessible. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-fr/strings.xml b/androidbrowserhelper/src/main/res/values-fr/strings.xml index 3c5c8cd8..fa6072eb 100755 --- a/androidbrowserhelper/src/main/res/values-fr/strings.xml +++ b/androidbrowserhelper/src/main/res/values-fr/strings.xml @@ -6,6 +6,7 @@ Cette appli ne contient aucune donnée de navigation. Appli non disponible Cette appli n\'est pas disponible, car %1$s est bloqué + Cette appli n\'est pas disponible. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-gl/strings.xml b/androidbrowserhelper/src/main/res/values-gl/strings.xml index 9c7f764f..f0b0478a 100755 --- a/androidbrowserhelper/src/main/res/values-gl/strings.xml +++ b/androidbrowserhelper/src/main/res/values-gl/strings.xml @@ -6,6 +6,7 @@ Esta aplicación non contén ningún dato de navegación. Aplicación non dispoñible Esta aplicación non está dispoñible porque se bloqueou %1$s + Esta aplicación non está dispoñible. Aceptar Chrome diff --git a/androidbrowserhelper/src/main/res/values-gu/strings.xml b/androidbrowserhelper/src/main/res/values-gu/strings.xml index ab9b82c0..f81cacf2 100755 --- a/androidbrowserhelper/src/main/res/values-gu/strings.xml +++ b/androidbrowserhelper/src/main/res/values-gu/strings.xml @@ -6,6 +6,7 @@ આ ઍપમાં કોઈ બ્રાઉઝિંગ ડેટા નથી. ઍપ ઉપલબ્ધ નથી %1$s બ્લૉક કરેલી હોવાથી આ ઍપ ઉપલબ્ધ નથી + આ ઍપ ઉપલબ્ધ નથી. ઓકે Chrome diff --git a/androidbrowserhelper/src/main/res/values-he/strings.xml b/androidbrowserhelper/src/main/res/values-he/strings.xml index fbaf50f5..60926d07 100755 --- a/androidbrowserhelper/src/main/res/values-he/strings.xml +++ b/androidbrowserhelper/src/main/res/values-he/strings.xml @@ -6,6 +6,7 @@ האפליקציה הזו לא מאחסנת נתוני גלישה. האפליקציה לא זמינה אי אפשר להשתמש באפליקציה הזו כי %1$s חסום + אי אפשר להשתמש באפליקציה הזו. אישור Chrome diff --git a/androidbrowserhelper/src/main/res/values-hi/strings.xml b/androidbrowserhelper/src/main/res/values-hi/strings.xml index 0e38d004..a2646c43 100755 --- a/androidbrowserhelper/src/main/res/values-hi/strings.xml +++ b/androidbrowserhelper/src/main/res/values-hi/strings.xml @@ -6,6 +6,7 @@ इस ऐप्लिकेशन में कोई ब्राउज़िंग डेटा नहीं है. ऐप्लिकेशन उपलब्ध नहीं है यह ऐप्लिकेशन उपलब्ध नहीं है, क्योंकि %1$s को ब्लॉक किया गया है + यह ऐप्लिकेशन उपलब्ध नहीं है. ठीक है Chrome diff --git a/androidbrowserhelper/src/main/res/values-hr/strings.xml b/androidbrowserhelper/src/main/res/values-hr/strings.xml index 387a6196..6caef367 100755 --- a/androidbrowserhelper/src/main/res/values-hr/strings.xml +++ b/androidbrowserhelper/src/main/res/values-hr/strings.xml @@ -6,6 +6,7 @@ Ova aplikacija ne sadrži podatke o pregledavanju. Aplikacija nije dostupna Ova aplikacija nije dostupna jer je davatelj usluga %1$s blokiran + Aplikacija nije dostupna. U redu Chrome diff --git a/androidbrowserhelper/src/main/res/values-hu/strings.xml b/androidbrowserhelper/src/main/res/values-hu/strings.xml index 496cd2fa..f8d24b8d 100755 --- a/androidbrowserhelper/src/main/res/values-hu/strings.xml +++ b/androidbrowserhelper/src/main/res/values-hu/strings.xml @@ -6,6 +6,7 @@ Ez az alkalmazás nem tárol böngészési adatokat. Az alkalmazás nem áll rendelkezésre. Az alkalmazás nem áll rendelkezésre, mert a(z) %1$s le van tiltva. + Az alkalmazás nem áll rendelkezésre. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-hy/strings.xml b/androidbrowserhelper/src/main/res/values-hy/strings.xml index 77571b5f..4eda404e 100755 --- a/androidbrowserhelper/src/main/res/values-hy/strings.xml +++ b/androidbrowserhelper/src/main/res/values-hy/strings.xml @@ -6,6 +6,7 @@ Այս հավելվածը այցելությունների պատմության տվյալներ չի պահում։ Հավելվածը հասանելի չէ Այս հավելվածն անհասանելի է, քանի որ մատակարարը (%1$s) արգելափակված է + Այս հավելվածն անհասանելի է: Եղավ Chrome diff --git a/androidbrowserhelper/src/main/res/values-id/strings.xml b/androidbrowserhelper/src/main/res/values-id/strings.xml index dae9acb9..ee47b916 100755 --- a/androidbrowserhelper/src/main/res/values-id/strings.xml +++ b/androidbrowserhelper/src/main/res/values-id/strings.xml @@ -6,6 +6,7 @@ Aplikasi ini tidak menyimpan data penjelajahan. Aplikasi Tidak Tersedia Aplikasi ini tidak tersedia karena %1$s diblokir + Aplikasi ini tidak tersedia. Oke Chrome diff --git a/androidbrowserhelper/src/main/res/values-in/strings.xml b/androidbrowserhelper/src/main/res/values-in/strings.xml index dae9acb9..ee47b916 100755 --- a/androidbrowserhelper/src/main/res/values-in/strings.xml +++ b/androidbrowserhelper/src/main/res/values-in/strings.xml @@ -6,6 +6,7 @@ Aplikasi ini tidak menyimpan data penjelajahan. Aplikasi Tidak Tersedia Aplikasi ini tidak tersedia karena %1$s diblokir + Aplikasi ini tidak tersedia. Oke Chrome diff --git a/androidbrowserhelper/src/main/res/values-is/strings.xml b/androidbrowserhelper/src/main/res/values-is/strings.xml index bde31ae1..01f84834 100755 --- a/androidbrowserhelper/src/main/res/values-is/strings.xml +++ b/androidbrowserhelper/src/main/res/values-is/strings.xml @@ -6,6 +6,7 @@ Þetta forrit geymir engin vefskoðunargögn. Forrit er ekki í boði Þetta forrit er ekki í boði vegna þess að gert var hlé á %1$s + Þetta forrit er ekki í boði. Í lagi Chrome diff --git a/androidbrowserhelper/src/main/res/values-it/strings.xml b/androidbrowserhelper/src/main/res/values-it/strings.xml index 971d94a8..f85b3027 100755 --- a/androidbrowserhelper/src/main/res/values-it/strings.xml +++ b/androidbrowserhelper/src/main/res/values-it/strings.xml @@ -6,6 +6,7 @@ Questa app non contiene dati di navigazione. App non disponibile Questa app non è disponibile perché %1$s è bloccato + Questa app non è disponibile. Ok Chrome diff --git a/androidbrowserhelper/src/main/res/values-iw/strings.xml b/androidbrowserhelper/src/main/res/values-iw/strings.xml index fbaf50f5..60926d07 100755 --- a/androidbrowserhelper/src/main/res/values-iw/strings.xml +++ b/androidbrowserhelper/src/main/res/values-iw/strings.xml @@ -6,6 +6,7 @@ האפליקציה הזו לא מאחסנת נתוני גלישה. האפליקציה לא זמינה אי אפשר להשתמש באפליקציה הזו כי %1$s חסום + אי אפשר להשתמש באפליקציה הזו. אישור Chrome diff --git a/androidbrowserhelper/src/main/res/values-ja/strings.xml b/androidbrowserhelper/src/main/res/values-ja/strings.xml index 2cce9214..5d3ee58d 100755 --- a/androidbrowserhelper/src/main/res/values-ja/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ja/strings.xml @@ -6,6 +6,7 @@ このアプリには閲覧データは保存されません。 アプリを利用できません %1$s がブロックされているため、このアプリは利用できません + このアプリは利用できません。 OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-ka/strings.xml b/androidbrowserhelper/src/main/res/values-ka/strings.xml index e4d9e90a..b5ae04da 100755 --- a/androidbrowserhelper/src/main/res/values-ka/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ka/strings.xml @@ -6,6 +6,7 @@ ეს აპი არ ინახავს დათვალიერების მონაცემებს. აპი მიუწვდომელია ეს აპი მიუწვდომელია, რადგან %1$s დაბლოკილია + ეს აპი მიუწვდომელია. კარგი Chrome diff --git a/androidbrowserhelper/src/main/res/values-kk/strings.xml b/androidbrowserhelper/src/main/res/values-kk/strings.xml index e7622f34..2bc61d28 100755 --- a/androidbrowserhelper/src/main/res/values-kk/strings.xml +++ b/androidbrowserhelper/src/main/res/values-kk/strings.xml @@ -6,6 +6,7 @@ Бұл қолданбада браузерді пайдалану деректері жоқ. Қолданба қолжетімді емес Бұл қолданба қолжетімді емес, себебі %1$s блокталған. + Бұл қолданба қолжетімді емес. Жарайды Chrome diff --git a/androidbrowserhelper/src/main/res/values-km/strings.xml b/androidbrowserhelper/src/main/res/values-km/strings.xml index 09903534..31ad7c8c 100755 --- a/androidbrowserhelper/src/main/res/values-km/strings.xml +++ b/androidbrowserhelper/src/main/res/values-km/strings.xml @@ -6,6 +6,7 @@ កម្មវិធីនេះ​មិនរក្សាទុក​ទិន្នន័យរុករក​ទេ។ មិនអាចប្រើ​កម្មវិធី​បានទេ មិនអាចប្រើ​កម្មវិធីនេះ​បានទេ ដោយសារ %1$s ត្រូវបាន​ទប់ស្កាត់ + មិនអាចប្រើ​កម្មវិធីនេះ​បានទេ។ យល់ព្រម Chrome diff --git a/androidbrowserhelper/src/main/res/values-kn/strings.xml b/androidbrowserhelper/src/main/res/values-kn/strings.xml index 7183da01..edc03812 100755 --- a/androidbrowserhelper/src/main/res/values-kn/strings.xml +++ b/androidbrowserhelper/src/main/res/values-kn/strings.xml @@ -6,6 +6,7 @@ ಈ ಆ್ಯಪ್ ಯಾವುದೇ ಬ್ರೌಸಿಂಗ್ ಡೇಟಾವನ್ನು ಹೊಂದಿಲ್ಲ. ಆ್ಯಪ್ ಲಭ್ಯವಿಲ್ಲ %1$s ಅನ್ನು ನಿರ್ಬಂಧಿಸಿರುವ ಕಾರಣ ಈ ಆ್ಯಪ್ ಲಭ್ಯವಿಲ್ಲ + ಈ ಆ್ಯಪ್ ಲಭ್ಯವಿಲ್ಲ. ಸರಿ Chrome diff --git a/androidbrowserhelper/src/main/res/values-ko/strings.xml b/androidbrowserhelper/src/main/res/values-ko/strings.xml index 85c0309f..55996419 100755 --- a/androidbrowserhelper/src/main/res/values-ko/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ko/strings.xml @@ -6,6 +6,7 @@ 이 앱에 저장된 인터넷 사용 기록이 없습니다. 사용할 수 없는 앱 %1$s이(가) 차단되어 있어 이 앱을 사용할 수 없습니다. + 사용할 수 없는 앱입니다. 확인 Chrome diff --git a/androidbrowserhelper/src/main/res/values-ky/strings.xml b/androidbrowserhelper/src/main/res/values-ky/strings.xml index 7968b82f..b241a58e 100755 --- a/androidbrowserhelper/src/main/res/values-ky/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ky/strings.xml @@ -6,6 +6,7 @@ Бул колдонмодо серептөө дайындары сакталбайт. Колдонмо жеткиликсиз %1$s бөгөттөлгөндүктөн, бул колдонмо жеткиликсиз + Бул колдонмо жеткиликтүү эмес. Жарайт Chrome diff --git a/androidbrowserhelper/src/main/res/values-lo/strings.xml b/androidbrowserhelper/src/main/res/values-lo/strings.xml index fd287ab4..b1880c8f 100755 --- a/androidbrowserhelper/src/main/res/values-lo/strings.xml +++ b/androidbrowserhelper/src/main/res/values-lo/strings.xml @@ -6,6 +6,7 @@ ແອັບນີ້ບໍ່ມີຂໍ້ມູນການທ່ອງເວັບ. ແອັບບໍ່ພ້ອມນຳໃຊ້ ແອັບນີ້ບໍ່ພ້ອມນຳໃຊ້ເນື່ອງຈາກ %1$s ຖືກບລັອກ + ແອັບນີ້ບໍ່ພ້ອມນຳໃຊ້. ຕົກລົງ Chrome diff --git a/androidbrowserhelper/src/main/res/values-lt/strings.xml b/androidbrowserhelper/src/main/res/values-lt/strings.xml index 298c515f..7fed1677 100755 --- a/androidbrowserhelper/src/main/res/values-lt/strings.xml +++ b/androidbrowserhelper/src/main/res/values-lt/strings.xml @@ -6,6 +6,7 @@ Šioje programoje nėra laikomi jokie naršymo duomenys. Programa nepasiekiama Ši programa nepasiekiama, nes „%1$s“ užblokuota + Ši programa nepasiekiama. Gerai Chrome diff --git a/androidbrowserhelper/src/main/res/values-lv/strings.xml b/androidbrowserhelper/src/main/res/values-lv/strings.xml index 33e6feff..d7ba611c 100755 --- a/androidbrowserhelper/src/main/res/values-lv/strings.xml +++ b/androidbrowserhelper/src/main/res/values-lv/strings.xml @@ -6,6 +6,7 @@ Šajā lietotnē netiek glabāti pārlūkošanas dati. Lietotne nav pieejama Šī lietotne nav pieejama, jo %1$s ir bloķēts. + Šī lietotne nav pieejama. Labi Chrome diff --git a/androidbrowserhelper/src/main/res/values-mk/strings.xml b/androidbrowserhelper/src/main/res/values-mk/strings.xml index d851a60f..19938644 100755 --- a/androidbrowserhelper/src/main/res/values-mk/strings.xml +++ b/androidbrowserhelper/src/main/res/values-mk/strings.xml @@ -6,6 +6,7 @@ Апликацијава не содржи податоци од прелистување. Апликацијата е недостапна Апликацијава е недостапна бидејќи %1$s е блокиран + Апликацијата е недостапна. Во ред Chrome diff --git a/androidbrowserhelper/src/main/res/values-ml/strings.xml b/androidbrowserhelper/src/main/res/values-ml/strings.xml index 4861ed9a..0dca5937 100755 --- a/androidbrowserhelper/src/main/res/values-ml/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ml/strings.xml @@ -6,6 +6,7 @@ ഈ ആപ്പിൽ ബ്രൗസിംഗ് ഡാറ്റയൊന്നും നിലനിർത്തിയിട്ടില്ല. ആപ്പ് ലഭ്യമല്ല %1$s ബ്ലോക്ക് ചെയ്‌തതിനാൽ ഈ ആപ്പ് ലഭ്യമല്ല + ഈ ആപ്പ് ലഭ്യമല്ല. ശരി Chrome diff --git a/androidbrowserhelper/src/main/res/values-mn/strings.xml b/androidbrowserhelper/src/main/res/values-mn/strings.xml index 7becc2e4..96e0e03b 100755 --- a/androidbrowserhelper/src/main/res/values-mn/strings.xml +++ b/androidbrowserhelper/src/main/res/values-mn/strings.xml @@ -6,6 +6,7 @@ Энэ апп интернэтээр үзсэн ямар ч өгөгдлийг хадгалдаггүй. Аппыг ашиглах боломжгүй %1$s-г блоклосон тул энэ аппыг ашиглах боломжгүй байна + Энэ аппыг ашиглах боломжгүй байна. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-mr/strings.xml b/androidbrowserhelper/src/main/res/values-mr/strings.xml index a7f48921..fb55b117 100755 --- a/androidbrowserhelper/src/main/res/values-mr/strings.xml +++ b/androidbrowserhelper/src/main/res/values-mr/strings.xml @@ -6,6 +6,7 @@ या ॲपमध्ये कोणताही ब्राउझिंग डेटा नाही. अ‍ॅप उपलब्ध नाही %1$s ब्लॉक केले असल्यामुळे हे अ‍ॅप उपलब्ध नाही + हे अ‍ॅप उपलब्ध नाही ओके Chrome diff --git a/androidbrowserhelper/src/main/res/values-ms/strings.xml b/androidbrowserhelper/src/main/res/values-ms/strings.xml index 8dd374d2..6f9ebabb 100755 --- a/androidbrowserhelper/src/main/res/values-ms/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ms/strings.xml @@ -6,6 +6,7 @@ Apl ini tidak menyimpan data semakan imbas. Apl Tidak Tersedia Apl ini tidak tersedia kerana %1$s disekat + Apl ini tidak tersedia. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-my/strings.xml b/androidbrowserhelper/src/main/res/values-my/strings.xml index 0f9b6a35..cd8c1e16 100755 --- a/androidbrowserhelper/src/main/res/values-my/strings.xml +++ b/androidbrowserhelper/src/main/res/values-my/strings.xml @@ -6,6 +6,7 @@ ဤအက်ပ်တွင် ဖွင့်ကြည့်ထားသည့်မှတ်တမ်းဒေတာ မရှိပါ။ အက်ပ် မရနိုင်ပါ %1$s ကို ခဏရပ်ထားသဖြင့် ဤအက်ပ်ကို မရနိုင်ပါ + ဤအက်ပ်ကို မရနိုင်ပါ။ OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-nb/strings.xml b/androidbrowserhelper/src/main/res/values-nb/strings.xml index 0741054c..6e2b997d 100755 --- a/androidbrowserhelper/src/main/res/values-nb/strings.xml +++ b/androidbrowserhelper/src/main/res/values-nb/strings.xml @@ -6,6 +6,7 @@ Denne appen har ingen nettlesingsdata. Appen er utilgjengelig Denne appen er utilgjengelig fordi %1$s er blokkert + Denne appen er utilgjengelig. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-ne/strings.xml b/androidbrowserhelper/src/main/res/values-ne/strings.xml index db97751b..43c24fc9 100755 --- a/androidbrowserhelper/src/main/res/values-ne/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ne/strings.xml @@ -6,6 +6,7 @@ यो एपले कुनै पनि ब्राउजिङ डेटा भण्डारण गर्दैन। एप उपलब्ध छैन %1$s ब्लक गरिएकाले यो एप उपलब्ध छैन + यो एप उपलब्ध छैन। ठिक छ Chrome diff --git a/androidbrowserhelper/src/main/res/values-nl/strings.xml b/androidbrowserhelper/src/main/res/values-nl/strings.xml index 7c198e34..ac4b3a33 100755 --- a/androidbrowserhelper/src/main/res/values-nl/strings.xml +++ b/androidbrowserhelper/src/main/res/values-nl/strings.xml @@ -6,6 +6,7 @@ Deze app bevat geen browsegegevens. App niet beschikbaar Deze app is niet beschikbaar omdat %1$s is geblokkeerd + Deze app is niet beschikbaar. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-no/strings.xml b/androidbrowserhelper/src/main/res/values-no/strings.xml index 0741054c..6e2b997d 100755 --- a/androidbrowserhelper/src/main/res/values-no/strings.xml +++ b/androidbrowserhelper/src/main/res/values-no/strings.xml @@ -6,6 +6,7 @@ Denne appen har ingen nettlesingsdata. Appen er utilgjengelig Denne appen er utilgjengelig fordi %1$s er blokkert + Denne appen er utilgjengelig. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-or/strings.xml b/androidbrowserhelper/src/main/res/values-or/strings.xml index 7e7626e4..a318fe3d 100755 --- a/androidbrowserhelper/src/main/res/values-or/strings.xml +++ b/androidbrowserhelper/src/main/res/values-or/strings.xml @@ -6,6 +6,7 @@ ଏହି ଆପରେ କୌଣସି ବ୍ରାଉଜିଂ ଡାଟା ନାହିଁ। ଆପ ଉପଲବ୍ଧ ନାହିଁ %1$sକୁ ବ୍ଲକ କରାଯାଇଥିବା ଯୋଗୁଁ ଏହି ଆପ ଉପଲବ୍ଧ ନାହିଁ + ଏହି ଆପ ଉପଲବ୍ଧ ନାହିଁ। ଠିକ ଅଛି Chrome diff --git a/androidbrowserhelper/src/main/res/values-pa/strings.xml b/androidbrowserhelper/src/main/res/values-pa/strings.xml index fc32a03c..997d5ee7 100755 --- a/androidbrowserhelper/src/main/res/values-pa/strings.xml +++ b/androidbrowserhelper/src/main/res/values-pa/strings.xml @@ -6,6 +6,7 @@ ਇਸ ਐਪ ਵਿੱਚ ਕੋਈ ਬ੍ਰਾਊਜ਼ਿੰਗ ਡਾਟਾ ਨਹੀਂ ਹੈ। ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ ਇਹ ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ, ਕਿਉਂਕਿ %1$s ਨੂੰ ਬਲਾਕ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ + ਇਹ ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ। ਠੀਕ ਹੈ Chrome diff --git a/androidbrowserhelper/src/main/res/values-pl/strings.xml b/androidbrowserhelper/src/main/res/values-pl/strings.xml index 1fd8c940..360a93a8 100755 --- a/androidbrowserhelper/src/main/res/values-pl/strings.xml +++ b/androidbrowserhelper/src/main/res/values-pl/strings.xml @@ -6,6 +6,7 @@ Ta aplikacja nie przechowuje żadnych danych przeglądania. Aplikacja jest niedostępna Ta aplikacja jest niedostępna, ponieważ przeglądarka %1$s jest zablokowana + Ta aplikacja jest niedostępna. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-pt-rBR/strings.xml b/androidbrowserhelper/src/main/res/values-pt-rBR/strings.xml index 4c596371..ea719012 100755 --- a/androidbrowserhelper/src/main/res/values-pt-rBR/strings.xml +++ b/androidbrowserhelper/src/main/res/values-pt-rBR/strings.xml @@ -6,6 +6,7 @@ Este app não tem dados de navegação. App indisponível Este app está indisponível porque %1$s está bloqueado + Esse app está indisponível. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-pt-rPT/strings.xml b/androidbrowserhelper/src/main/res/values-pt-rPT/strings.xml index ab29be2f..9ba26667 100755 --- a/androidbrowserhelper/src/main/res/values-pt-rPT/strings.xml +++ b/androidbrowserhelper/src/main/res/values-pt-rPT/strings.xml @@ -6,6 +6,7 @@ Esta app não contém dados de navegação. App indisponível Esta app não está disponível porque o %1$s está bloqueado + Esta app não está disponível. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-ro/strings.xml b/androidbrowserhelper/src/main/res/values-ro/strings.xml index 412cd39c..25a4df0d 100755 --- a/androidbrowserhelper/src/main/res/values-ro/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ro/strings.xml @@ -6,6 +6,7 @@ Această aplicație nu deține date de navigare. Aplicația nu este disponibilă Aplicația nu este disponibilă deoarece %1$s este blocat + Această aplicație nu este disponibilă. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-ru/strings.xml b/androidbrowserhelper/src/main/res/values-ru/strings.xml index dab7ae8b..5c29eba7 100755 --- a/androidbrowserhelper/src/main/res/values-ru/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ru/strings.xml @@ -6,6 +6,7 @@ В этом приложении нет данных о работе в браузере. Приложение недоступно Это приложение недоступно, поскольку сервис \"%1$s\" заблокирован. + Приложение недоступно. ОК Chrome diff --git a/androidbrowserhelper/src/main/res/values-si/strings.xml b/androidbrowserhelper/src/main/res/values-si/strings.xml index c14a5f99..bdf9b650 100755 --- a/androidbrowserhelper/src/main/res/values-si/strings.xml +++ b/androidbrowserhelper/src/main/res/values-si/strings.xml @@ -6,6 +6,7 @@ මෙම යෙදුම බ්‍රවුස් කිරීමේ දත්ත කිසිවක් රඳවා නොගනී. යෙදුම නොමැත %1$s විරාම කර ඇති නිසා මෙම යෙදුම නොමැත + මෙම යෙදුම නොතිබේ. හරි Chrome diff --git a/androidbrowserhelper/src/main/res/values-sk/strings.xml b/androidbrowserhelper/src/main/res/values-sk/strings.xml index 520760a2..1dd6c329 100755 --- a/androidbrowserhelper/src/main/res/values-sk/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sk/strings.xml @@ -6,6 +6,7 @@ Táto aplikácia neobsahuje žiadne dáta prehliadania. Aplikácia nie je k dispozícii Táto aplikácia nie je k dispozícii, pretože poskytovateľ %1$s je blokovaný + Táto aplikácia nie je k dispozícii. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-sl/strings.xml b/androidbrowserhelper/src/main/res/values-sl/strings.xml index d960c4cc..b2265382 100755 --- a/androidbrowserhelper/src/main/res/values-sl/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sl/strings.xml @@ -6,6 +6,7 @@ Ta aplikacija ne vsebuje podatkov brskanja. Aplikacija ni na voljo Ta aplikacija ni na voljo, ker je ponudnik %1$s blokiran + Ta aplikacija ni na voljo. V redu Chrome diff --git a/androidbrowserhelper/src/main/res/values-sq/strings.xml b/androidbrowserhelper/src/main/res/values-sq/strings.xml index 016a64fb..e6d55f94 100755 --- a/androidbrowserhelper/src/main/res/values-sq/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sq/strings.xml @@ -6,6 +6,7 @@ Ky aplikacion nuk mban të dhëna shfletimi. Aplikacioni nuk ofrohet Ky aplikacion nuk ofrohet sepse %1$s është bllokuar + Ky aplikacion nuk është i disponueshëm. Në rregull Chrome diff --git a/androidbrowserhelper/src/main/res/values-sr/strings.xml b/androidbrowserhelper/src/main/res/values-sr/strings.xml index db5611d6..9742e59a 100755 --- a/androidbrowserhelper/src/main/res/values-sr/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sr/strings.xml @@ -6,6 +6,7 @@ Ова апликација не чува податке прегледања. Апликација није доступна Ова апликација није доступна јер је прегледач %1$s блокиран + Ова апликација је недоступна. Потврди Chrome diff --git a/androidbrowserhelper/src/main/res/values-sv/strings.xml b/androidbrowserhelper/src/main/res/values-sv/strings.xml index 24d91859..b5b8e7c0 100755 --- a/androidbrowserhelper/src/main/res/values-sv/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sv/strings.xml @@ -6,6 +6,7 @@ Den här appen innehåller ingen webbinformation. Appen är inte tillgänglig Den här appen är inte tillgänglig eftersom %1$s har blockerats + Den här appen är inte tillgänglig. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-sw/strings.xml b/androidbrowserhelper/src/main/res/values-sw/strings.xml index 3937e88f..327a6a2f 100755 --- a/androidbrowserhelper/src/main/res/values-sw/strings.xml +++ b/androidbrowserhelper/src/main/res/values-sw/strings.xml @@ -6,6 +6,7 @@ Programu hii haina data ya kuvinjari. Programu Haipatikani Programu hii haipatikani kwa sababu %1$s imezuiwa + Programu hii haipatikani. Sawa Chrome diff --git a/androidbrowserhelper/src/main/res/values-ta/strings.xml b/androidbrowserhelper/src/main/res/values-ta/strings.xml index e553dab5..7431bed6 100755 --- a/androidbrowserhelper/src/main/res/values-ta/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ta/strings.xml @@ -6,6 +6,7 @@ இந்த ஆப்ஸில் பிரவுசிங் தரவு எதுவும் இல்லை. ஆப்ஸ் கிடைக்கவில்லை %1$s தடுக்கப்பட்டுள்ளதால் இந்த ஆப்ஸ் கிடைக்கவில்லை + இந்த ஆப்ஸ் கிடைக்கவில்லை. சரி Chrome diff --git a/androidbrowserhelper/src/main/res/values-te/strings.xml b/androidbrowserhelper/src/main/res/values-te/strings.xml index 61053b3f..f1d42a7d 100755 --- a/androidbrowserhelper/src/main/res/values-te/strings.xml +++ b/androidbrowserhelper/src/main/res/values-te/strings.xml @@ -6,6 +6,7 @@ ఈ యాప్‌లో బ్రౌజింగ్ డేటా ఏదీ లేదు. యాప్ అందుబాటులో లేదు %1$s బ్లాక్ అయి ఉన్నందున, ఈ యాప్ అందుబాటులో లేదు + ఈ యాప్ అందుబాటులో లేదు. సరే Chrome diff --git a/androidbrowserhelper/src/main/res/values-th/strings.xml b/androidbrowserhelper/src/main/res/values-th/strings.xml index d723ccc1..5267f0e0 100755 --- a/androidbrowserhelper/src/main/res/values-th/strings.xml +++ b/androidbrowserhelper/src/main/res/values-th/strings.xml @@ -6,6 +6,7 @@ แอปนี้ไม่มีข้อมูลการท่องเว็บ แอปไม่พร้อมใช้งาน แอปนี้ไม่พร้อมใช้งานเนื่องจาก %1$s ถูกบล็อก + แอปนี้ไม่พร้อมใช้งาน ตกลง Chrome diff --git a/androidbrowserhelper/src/main/res/values-tl/strings.xml b/androidbrowserhelper/src/main/res/values-tl/strings.xml index 89970123..ed8a6e2d 100755 --- a/androidbrowserhelper/src/main/res/values-tl/strings.xml +++ b/androidbrowserhelper/src/main/res/values-tl/strings.xml @@ -6,6 +6,7 @@ Walang data mula sa pag-browse ang app na ito. Hindi Available ang App Hindi available ang app na ito dahil naka-block ang %1$s + Hindi available ang app na ito. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-tr/strings.xml b/androidbrowserhelper/src/main/res/values-tr/strings.xml index 7aa95117..9cc0b632 100755 --- a/androidbrowserhelper/src/main/res/values-tr/strings.xml +++ b/androidbrowserhelper/src/main/res/values-tr/strings.xml @@ -6,6 +6,7 @@ Bu uygulamada tarama verisi yok. Uygulama kullanılamıyor %1$s engellendiğinden bu uygulama kullanılamıyor + Bu uygulama kullanılamıyor. Tamam Chrome diff --git a/androidbrowserhelper/src/main/res/values-uk/strings.xml b/androidbrowserhelper/src/main/res/values-uk/strings.xml index cdcb149f..11d7b7fd 100755 --- a/androidbrowserhelper/src/main/res/values-uk/strings.xml +++ b/androidbrowserhelper/src/main/res/values-uk/strings.xml @@ -6,6 +6,7 @@ У цьому додатку немає даних вебперегляду. Додаток недоступний Цей додаток недоступний, оскільки %1$s заблоковано + Цей додаток недоступний. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-ur/strings.xml b/androidbrowserhelper/src/main/res/values-ur/strings.xml index e559b68f..12643f12 100755 --- a/androidbrowserhelper/src/main/res/values-ur/strings.xml +++ b/androidbrowserhelper/src/main/res/values-ur/strings.xml @@ -6,6 +6,7 @@ یہ ایپ براؤزنگ ڈیٹا نہیں رکھتی ہے۔ ایپ دستیاب نہیں ہے یہ ایپ دستیاب نہیں ہے کیونکہ %1$s لاک ہے + یہ ایپ دستیاب نہیں ہے۔ ٹھیک ہے Chrome diff --git a/androidbrowserhelper/src/main/res/values-uz/strings.xml b/androidbrowserhelper/src/main/res/values-uz/strings.xml index 6638c8c4..9ff03897 100755 --- a/androidbrowserhelper/src/main/res/values-uz/strings.xml +++ b/androidbrowserhelper/src/main/res/values-uz/strings.xml @@ -6,6 +6,7 @@ Bu ilovada brauzer maʼlumotlari saqlanmaydi. Ilova mavjud emas Bu ilova ishlamaydi, chunki %1$s bloklangan + Bu ilova mavjud emas. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-vi/strings.xml b/androidbrowserhelper/src/main/res/values-vi/strings.xml index d0dc5460..e3c340f0 100755 --- a/androidbrowserhelper/src/main/res/values-vi/strings.xml +++ b/androidbrowserhelper/src/main/res/values-vi/strings.xml @@ -6,6 +6,7 @@ Ứng dụng này không lưu trữ dữ liệu duyệt web. Ứng dụng không dùng được Ứng dụng này không dùng được vì %1$s đã bị chặn + Ứng dụng này không sẵn dùng. OK Chrome diff --git a/androidbrowserhelper/src/main/res/values-zh-rCN/strings.xml b/androidbrowserhelper/src/main/res/values-zh-rCN/strings.xml index dd9093ca..73c30e28 100755 --- a/androidbrowserhelper/src/main/res/values-zh-rCN/strings.xml +++ b/androidbrowserhelper/src/main/res/values-zh-rCN/strings.xml @@ -6,6 +6,7 @@ 此应用没保留任何浏览数据。 无法使用此应用 无法使用此应用,因为“%1$s”被屏蔽了 + 无法使用这款应用。 知道了 Chrome diff --git a/androidbrowserhelper/src/main/res/values-zh-rHK/strings.xml b/androidbrowserhelper/src/main/res/values-zh-rHK/strings.xml index 2b8c46a8..0ea3ee06 100755 --- a/androidbrowserhelper/src/main/res/values-zh-rHK/strings.xml +++ b/androidbrowserhelper/src/main/res/values-zh-rHK/strings.xml @@ -6,6 +6,7 @@ 此應用程式沒有保留任何瀏覽資料。 無法啟動應用程式 「%1$s」已遭封鎖,因此無法啟動此應用程式 + 此應用程式不存在。 確定 Chrome diff --git a/androidbrowserhelper/src/main/res/values-zh-rTW/strings.xml b/androidbrowserhelper/src/main/res/values-zh-rTW/strings.xml index 576ac133..0e621ec6 100755 --- a/androidbrowserhelper/src/main/res/values-zh-rTW/strings.xml +++ b/androidbrowserhelper/src/main/res/values-zh-rTW/strings.xml @@ -6,6 +6,7 @@ 此應用程式不會保留任何瀏覽資料。 無法啟動應用程式 「%1$s」已封鎖,因此無法啟動應用程式 + 無法提供這個應用程式。 確定 Chrome diff --git a/androidbrowserhelper/src/main/res/values-zu/strings.xml b/androidbrowserhelper/src/main/res/values-zu/strings.xml index 3c6c1ef6..3e11c30f 100755 --- a/androidbrowserhelper/src/main/res/values-zu/strings.xml +++ b/androidbrowserhelper/src/main/res/values-zu/strings.xml @@ -6,6 +6,7 @@ Le app ayinayo idatha yokubhrawuza. I-app Ayitholakali Le app ayitholakali ngoba i-%1$s ivinjiwe + Le app ayitholakali. KULUNGILE Chrome diff --git a/androidbrowserhelper/src/main/res/values/strings.xml b/androidbrowserhelper/src/main/res/values/strings.xml index 68c0d5d5..7cfd9896 100644 --- a/androidbrowserhelper/src/main/res/values/strings.xml +++ b/androidbrowserhelper/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ This app holds no browsing data. App Unavailable This app is unavailable because %1$s is blocked - This app is unavailable. + This app is unavailable. OK Chrome From 1e3b72d672045f5010259c6f3413ee71c8e800bf Mon Sep 17 00:00:00 2001 From: Greg Stepniewski Date: Wed, 17 Jun 2026 07:52:43 +0000 Subject: [PATCH 04/24] FocusActivity improvements for reordering tasks to the foreground --- .../trusted/FocusActivity.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/FocusActivity.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/FocusActivity.java index 997768d2..0c9a3c8a 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/FocusActivity.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/FocusActivity.java @@ -1,11 +1,14 @@ package com.google.androidbrowserhelper.trusted; +import android.annotation.SuppressLint; import android.app.Activity; +import android.app.ActivityManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.util.Log; import androidx.annotation.Nullable; @@ -17,8 +20,14 @@ * the browser can launch that PendingIntent bringing this Activity to the foreground. This Activity * then finishes itself, revealing the other Activities in the app's Task. Hopefully the topmost * Activity will be the web browsing Activity. + * + *

Note: For this Activity to successfully function, the client application must declare the + * {@code android.permission.REORDER_TASKS} permission in its {@code AndroidManifest.xml}. The + * client app must also declare this Activity in its manifest, specifying {@code android:exported="true"} + * to allow the browser to launch it. */ public class FocusActivity extends Activity { + private static final String TAG = "FocusActivity"; // This value should be moved into androidx.browser. private static final String EXTRA_FOCUS_INTENT = "androidx.browser.customtabs.extra.FOCUS_INTENT"; @@ -49,8 +58,17 @@ public static void addToIntent(Intent containerIntent, Context context) { } @Override + @SuppressLint("MissingPermission") protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + if (manager != null) { + try { + manager.moveTaskToFront(getTaskId(), 0); + } catch (Exception e) { + Log.e(TAG, "Failed to move task to front", e); + } + } finish(); } } \ No newline at end of file From ce78b4d14f1c91626120af3e1753091a43820186 Mon Sep 17 00:00:00 2001 From: Greg Stepniewski Date: Wed, 17 Jun 2026 09:20:52 +0000 Subject: [PATCH 05/24] androidbrowserhelper version bump to 2.7.2 --- README.md | 2 +- androidbrowserhelper/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 843f6d57..e9caaf18 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Android Browser helper is available on the Google Maven. To use it, modify your ```gradle dependencies { //... - implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.7.0' + implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.7.2' } ``` diff --git a/androidbrowserhelper/build.gradle b/androidbrowserhelper/build.gradle index 5a3c2042..c56b9429 100644 --- a/androidbrowserhelper/build.gradle +++ b/androidbrowserhelper/build.gradle @@ -22,8 +22,8 @@ plugins { // Before 2.6.1, the version code was unused and was kept at 1. // This is now being used to report metrics, and should be bumped with // every version bump. -def VERSION = "2.7.1"; -def VERSION_CODE = 7; +def VERSION = "2.7.2"; +def VERSION_CODE = 8; android { namespace "com.google.androidbrowserhelper" From 399d79df914fa51d1200feef040272f4b82a8b7b Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:11:37 +0000 Subject: [PATCH 06/24] feat(pblv8): configure PendingPurchasesParams for one-time products (FR-04) --- .../playbilling/provider/PlayBillingWrapper.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java index 7d7bbfb5..f200a553 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java @@ -27,6 +27,7 @@ import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.PendingPurchasesParams; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; @@ -63,7 +64,9 @@ public PlayBillingWrapper(Context context, Listener listener) { mClient = BillingClient .newBuilder(context) .setListener(mPurchaseUpdateListener) - .enablePendingPurchases() + .enablePendingPurchases(PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .build()) .build(); } From ccbbb3a31a23496e65424665940e9b5e103d6a7c Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:21:16 +0000 Subject: [PATCH 07/24] feat(pblv8): refactor queryPurchases to use QueryPurchasesParams (FR-06) --- .../playbilling/provider/PlayBillingWrapper.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java index f200a553..05ca26d6 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java @@ -28,6 +28,7 @@ import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PendingPurchasesParams; +import com.android.billingclient.api.QueryPurchasesParams; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; @@ -88,9 +89,12 @@ public void querySkuDetails(@BillingClient.SkuType String skuType, List } @Override - public void queryPurchases(@BillingClient.SkuType String skuType, + public void queryPurchases(@BillingClient.SkuType String skuType, PurchasesResponseListener callback) { - mClient.queryPurchasesAsync(skuType, callback); + QueryPurchasesParams params = QueryPurchasesParams.newBuilder() + .setProductType(skuType) + .build(); + mClient.queryPurchasesAsync(params, callback); } @Override From 5f03be270e57ae0277674ec1c077bf9a948df6b8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:49:55 +0000 Subject: [PATCH 08/24] feat(pblv8): refactor BillingWrapper and implement product details mapping (FR-02, FR-05) --- playbilling/README.md | 6 +- playbilling/build.gradle | 2 + .../playbilling/QuerySkuDetailsCallTest.kt | 96 ---------- .../provider/PaymentActivityTest.java | 42 +++-- .../digitalgoods/BillingResultMerger.java | 10 +- .../digitalgoods/ConnectedBillingWrapper.java | 22 +-- .../digitalgoods/GetDetailsCall.java | 12 +- .../playbilling/digitalgoods/ItemDetails.java | 137 ++++++++++++-- .../digitalgoods/LegacyPurchaseDetails.java | 2 +- .../digitalgoods/ListPurchaseHistoryCall.java | 4 +- .../digitalgoods/ListPurchasesCall.java | 4 +- .../digitalgoods/PurchaseDetails.java | 6 +- .../playbilling/provider/BillingWrapper.java | 16 +- .../provider/MockBillingWrapper.java | 84 ++++----- ...=> MultiProductTypeInvocationTracker.java} | 22 +-- .../playbilling/provider/PaymentActivity.java | 24 +-- .../provider/PlayBillingWrapper.java | 58 ++++-- .../ConnectedBillingWrapperTest.java | 10 +- .../digitalgoods/GetDetailsCallTest.java | 43 +++-- .../digitalgoods/ItemDetailsTest.java | 175 ++++++++++++------ .../ListPurchaseHistoryCallTest.java | 4 +- 21 files changed, 448 insertions(+), 331 deletions(-) delete mode 100644 playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/QuerySkuDetailsCallTest.kt rename playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/{MultiSkuTypeInvocationTracker.java => MultiProductTypeInvocationTracker.java} (67%) diff --git a/playbilling/README.md b/playbilling/README.md index 84b8df94..24520421 100644 --- a/playbilling/README.md +++ b/playbilling/README.md @@ -4,7 +4,7 @@ The Play Billing module provides capabilities for your TWA app to connect with [ * Query purchase history * Initialize a payment -* Query SKU details +* 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. @@ -13,13 +13,13 @@ The module uses [Version 7.1.1](https://developer.android.com/google/play/billin To use this API from your website you can do it as follows: ```js const PAYMENT_METHOD = 'https://play.google.com/billing'; -const SKUS = [ +const ITEM_IDS = [ 'android.test.purchased', 'android.test.canceled', ] const service = await window.getDigitalGoodsService(PAYMENT_METHOD); -const details = await service.getDetails(SKUS); +const details = await service.getDetails(ITEM_IDS); console.log(details); ``` diff --git a/playbilling/build.gradle b/playbilling/build.gradle index bbbdf754..56de6f17 100644 --- a/playbilling/build.gradle +++ b/playbilling/build.gradle @@ -66,11 +66,13 @@ dependencies { testImplementation libs.junit testImplementation libs.robolectric + testImplementation libs.mockito.core androidTestImplementation libs.androidx.runner androidTestImplementation libs.androidx.rules androidTestImplementation libs.androidx.ext.junit androidTestImplementation libs.junit + androidTestImplementation libs.mockito.core debugImplementation project(path: ':androidbrowserhelper') releaseImplementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.2.2' } diff --git a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/QuerySkuDetailsCallTest.kt b/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/QuerySkuDetailsCallTest.kt deleted file mode 100644 index b1e8ba70..00000000 --- a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/QuerySkuDetailsCallTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 Google Inc. All Rights Reserved. -// -// 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 -// -// http://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.playbilling - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.billingclient.api.BillingResult -import com.android.billingclient.api.SkuDetails -import com.android.billingclient.api.SkuDetailsParams -import com.android.billingclient.api.SkuDetailsResponseListener -import com.google.androidbrowserhelper.playbilling.bundles.QuerySkuDetailsCall -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference - -/** - * Test for communicating the parameters and listener for a BillingClient#querySkuDetailsAsync call - * using a QuerySkuDetailsCall - */ -@RunWith(AndroidJUnit4::class) -internal class QuerySkuDetailsCallTest { - @Test - fun querySkuDetailsCall() { - // This has to be an Android Test (not a Unit Test) because Robolectric doesn't support - // Bundle#putBinder. - - // It is helpful to think of this code being executed in two processes, the client which - // initiates the request and the provider which queries to Play Billing and then responds. - // TODO(peconn): Create a test that actually runs this on different Services/Activities. - - // The parameters the client is going to provide. - val expParams = SkuDetailsParams.newBuilder() - .setType("type") - .setSkusList(listOf("sku1", "sku2")) - .build() - - // Holders for the results that will be returned to the client. - val actBillingResult = AtomicReference() - val actSkuDetails = AtomicReference>() - - // A listener (on the client) that will be triggered by the provider. - val latch = CountDownLatch(1) - val listener = SkuDetailsResponseListener { billingResult, skuDetails -> - actBillingResult.set(billingResult) - actSkuDetails.set(skuDetails) - latch.countDown() - } - - // The client wraps the listener and params up into a Bundle. - val bundle = QuerySkuDetailsCall(expParams, listener).toBundle() - - // Now we move over to the provider. - - // The provider gets the listener and params out of the Bundle. - val querySkuDetailsCall = QuerySkuDetailsCall.fromBundle(bundle) - - // Check that the params the provider got are what we expected. - val actParams = querySkuDetailsCall.params - Assert.assertEquals(expParams.skuType, actParams.skuType) - Assert.assertArrayEquals(expParams.skusList.toTypedArray(), - actParams.skusList.toTypedArray()) - - // Now the provider goes off and calls the Play Billing APIs, getting some results. - - // The results from the provider. - val expBillingResult = BillingResult.newBuilder().setResponseCode(23).build() - val expSkuDetails = listOf(SkuDetails("{}"), SkuDetails("{}")) - - // The provider returns the results to the client through the listener (which forwards - // the results over a Binder). - querySkuDetailsCall.listener.onSkuDetailsResponse(expBillingResult, expSkuDetails) - - // And we're back on the client. - - // Wait for the listener to be called. - Assert.assertTrue(latch.await(3, TimeUnit.SECONDS)) - - // Check that the results the client got are actually what the provider sent. - Assert.assertEquals(expBillingResult.responseCode, actBillingResult.get().responseCode) - Assert.assertArrayEquals(expSkuDetails.toTypedArray(), actSkuDetails.get().toTypedArray()) - } -} diff --git a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java b/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java index 5a38aafe..9c40dcc2 100644 --- a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java +++ b/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java @@ -19,9 +19,13 @@ import android.content.Intent; import android.os.Bundle; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.ProductDetails; import com.google.androidbrowserhelper.trusted.SharedPreferencesTokenStore; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.json.JSONException; import org.junit.After; import org.junit.Before; @@ -81,8 +85,8 @@ public void successfulFlow() throws InterruptedException, JSONException { assertTrue(mWrapper.waitForConnect()); mWrapper.triggerConnected(); - assertTrue(mWrapper.waitForQuerySkuDetails()); - mWrapper.triggerOnGotSkuDetails(getSkuDetailsList()); + assertTrue(mWrapper.waitForQueryProductDetails()); + mWrapper.triggerOnGotProductDetails(getProductDetailsList()); assertTrue(mWrapper.waitForLaunchPaymentFlow()); mWrapper.triggerOnPurchasesUpdated(); @@ -99,8 +103,8 @@ public void setsProxy() throws InterruptedException, JSONException { assertTrue(mWrapper.waitForConnect()); mWrapper.triggerConnected(); - assertTrue(mWrapper.waitForQuerySkuDetails()); - mWrapper.triggerOnGotSkuDetails(getSkuDetailsList()); + assertTrue(mWrapper.waitForQueryProductDetails()); + mWrapper.triggerOnGotProductDetails(getProductDetailsList()); assertTrue(mWrapper.waitForLaunchPaymentFlow()); mWrapper.triggerOnPurchasesUpdated(); @@ -120,8 +124,8 @@ public void cancelledFlow() throws InterruptedException, JSONException { assertTrue(mWrapper.waitForConnect()); mWrapper.triggerConnected(); - assertTrue(mWrapper.waitForQuerySkuDetails()); - mWrapper.triggerOnGotSkuDetails(getSkuDetailsList()); + assertTrue(mWrapper.waitForQueryProductDetails()); + mWrapper.triggerOnGotProductDetails(getProductDetailsList()); assertTrue(mWrapper.waitForLaunchPaymentFlow()); @@ -164,8 +168,8 @@ public void noSkuFound() throws InterruptedException { assertTrue(mWrapper.waitForConnect()); mWrapper.triggerConnected(); - assertTrue(mWrapper.waitForQuerySkuDetails()); - mWrapper.triggerOnGotSkuDetails(Collections.emptyList()); + assertTrue(mWrapper.waitForQueryProductDetails()); + mWrapper.triggerOnGotProductDetails(Collections.emptyList()); assertActivityResult(Activity.RESULT_CANCELED); } @@ -177,16 +181,22 @@ public void skuPassedToPlayBilling() throws InterruptedException { assertTrue(mWrapper.waitForConnect()); mWrapper.triggerConnected(); - assertTrue(mWrapper.waitForQuerySkuDetails()); - List queriedSkuDetails = mWrapper.getQueriedSkuDetails(); + assertTrue(mWrapper.waitForQueryProductDetails()); + List queriedProductDetails = mWrapper.getQueriedProductDetails(); + + assertEquals(2, queriedProductDetails.size()); + assertTrue(queriedProductDetails.contains(SKU)); + } - assertEquals(2, queriedSkuDetails.size()); - assertTrue(queriedSkuDetails.contains(SKU)); + private List getProductDetailsList() { + return Arrays.asList(mockProductDetails("mySku")); } - private List getSkuDetailsList() throws JSONException { - String json = "{ 'productId' = 'mySku', 'type' = 'inapp' }".replace('\'', '\"'); - return Arrays.asList(new SkuDetails(json)); + private ProductDetails mockProductDetails(String id) { + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn(id); + when(productDetails.getProductType()).thenReturn(BillingClient.ProductType.INAPP); + return productDetails; } private Intent getIntent(@Nullable String sku) { diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/BillingResultMerger.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/BillingResultMerger.java index 464d55e6..39095d2b 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/BillingResultMerger.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/BillingResultMerger.java @@ -15,7 +15,7 @@ package com.google.androidbrowserhelper.playbilling.digitalgoods; import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.BillingClient.SkuType; +import com.android.billingclient.api.BillingClient.ProductType; import com.android.billingclient.api.BillingResult; import java.util.ArrayList; @@ -24,10 +24,10 @@ import androidx.annotation.Nullable; /** - * Play Billing SKUs are split into purchases and subscriptions. This isn't a distinction that - * exists in the Digital Goods API, but since SKU ids are unique across both product types, we can - * just call methods that take a skuType twice, once with {@link SkuType#INAPP} and once with - * {@link SkuType#SUBS}. This class exists to combine the results of those calls. + * Play Billing products are split into purchases (INAPP) and subscriptions (SUBS). This isn't a distinction that + * exists in the Digital Goods API, but since product ids are unique across both product types, we can + * just call methods that take a productType twice, once with {@link ProductType#INAPP} and once with + * {@link ProductType#SUBS}. This class exists to combine the results of those calls. * * Although the Play Billing methods are asynchronous, their callbacks (which will call * {@link #setInAppResult} and {@link #setSubsResult}) should both take place on the UI thread, so diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java index c567a550..0818991c 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java @@ -23,8 +23,8 @@ import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapper; import com.google.androidbrowserhelper.playbilling.provider.MethodData; @@ -33,7 +33,7 @@ /** * A wrapper around {@link BillingWrapper} that ensures it is connected before calling - * {@link #querySkuDetails}, {@link #acknowledge} or {@link #consume}. + * {@link #queryProductDetails}, {@link #acknowledge} or {@link #consume}. */ public class ConnectedBillingWrapper implements BillingWrapper { private final BillingWrapper mInner; @@ -89,19 +89,19 @@ public void connect(BillingClientStateListener callback) { } @Override - public void querySkuDetails(@BillingClient.SkuType String skuType, List skus, - SkuDetailsResponseListener callback) { - execute(() -> mInner.querySkuDetails(skuType, skus, callback)); + public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, + ProductDetailsResponseListener callback) { + execute(() -> mInner.queryProductDetails(productType, productIds, callback)); } @Override - public void queryPurchases(String skuType, PurchasesResponseListener callback) { - execute(() -> mInner.queryPurchases(skuType, callback)); + public void queryPurchases(String productType, PurchasesResponseListener callback) { + execute(() -> mInner.queryPurchases(productType, callback)); } @Override - public void queryPurchaseHistory(String skuType, PurchaseHistoryResponseListener callback) { - execute(() -> mInner.queryPurchaseHistory(skuType, callback)); + public void queryPurchaseHistory(String productType, PurchaseHistoryResponseListener callback) { + execute(() -> mInner.queryPurchaseHistory(productType, callback)); } @Override @@ -115,7 +115,7 @@ public void consume(String token, ConsumeResponseListener callback) { } @Override - public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData data) { + public boolean launchPaymentFlow(Activity activity, ProductDetails productDetails, MethodData data) { throw new IllegalStateException( "EnsuredConnectionBillingWrapper doesn't handle launch Payment flow"); } diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCall.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCall.java index 95ef94a2..e5d74740 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCall.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCall.java @@ -19,7 +19,7 @@ import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.ProductDetails; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapper; import java.util.Arrays; @@ -62,7 +62,7 @@ public static GetDetailsCall create(@Nullable Bundle args, } /** Calls the callback provided in the constructor with serialized forms of the parameters. */ - private void respond(BillingResult result, @Nullable List detailsList) { + private void respond(BillingResult result, @Nullable List detailsList) { Logging.logGetDetailsResponse(result); Parcelable[] parcelables = new Parcelable[0]; @@ -70,7 +70,7 @@ private void respond(BillingResult result, @Nullable List detailsLis parcelables = new Parcelable[detailsList.size()]; int index = 0; - for (SkuDetails details : detailsList) { + for (ProductDetails details : detailsList) { parcelables[index++] = ItemDetails.create(details).toBundle(); } } @@ -86,10 +86,10 @@ private void respond(BillingResult result, @Nullable List detailsLis public void call(BillingWrapper billing) { Logging.logGetDetailsCall(mItemIds); - BillingResultMerger merger = new BillingResultMerger<>(this::respond); + BillingResultMerger merger = new BillingResultMerger<>(this::respond); - billing.querySkuDetails(BillingClient.SkuType.INAPP, mItemIds, merger::setInAppResult); - billing.querySkuDetails(BillingClient.SkuType.SUBS, mItemIds, merger::setSubsResult); + billing.queryProductDetails(BillingClient.ProductType.INAPP, mItemIds, merger::setInAppResult); + billing.queryProductDetails(BillingClient.ProductType.SUBS, mItemIds, merger::setSubsResult); } /** Creates a Bundle that can be used with {@link #create}. For testing. */ diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetails.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetails.java index 512ee303..fb6a2f28 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetails.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetails.java @@ -16,12 +16,16 @@ import android.os.Bundle; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.ProductDetails; + +import java.util.List; +import java.util.Objects; /** - * A data class representing an item (or SKU) from the Play Store. + * A data class representing an item (or product) from the Play Store. * - * Its main purpose is to serialize {@link SkuDetails} into {@link Bundle}s in such a way that + * Its main purpose is to serialize {@link ProductDetails} into {@link Bundle}s in such a way that * Chromium can read it for the Digital Goods API. See: * https://source.chromium.org/chromium/chromium/src/+/master:chrome/android/java/src/org/chromium/chrome/browser/browserservices/digitalgoods/GetDetailsConverter.java;drc=a04f522e96fc0eaa0bbcb6eafa96d02aabe5452a */ @@ -77,21 +81,120 @@ private ItemDetails(String id, String title, String description, String currency } /** - * Creates this class from a Play Billing {@link SkuDetails}. + * Creates this class from a Play Billing {@link ProductDetails}. */ - public static ItemDetails create(SkuDetails skuDetails) { + public static ItemDetails create(ProductDetails productDetails) { + if (productDetails.getProductType().equals(BillingClient.ProductType.INAPP)) { + ProductDetails.OneTimePurchaseOfferDetails oneTimeDetails = + productDetails.getOneTimePurchaseOfferDetails(); + if (oneTimeDetails == null) { + return createEmpty(productDetails); + } + return new ItemDetails( + productDetails.getProductId(), + productDetails.getTitle(), + productDetails.getDescription(), + oneTimeDetails.getPriceCurrencyCode(), + toPrice(oneTimeDetails.getPriceAmountMicros()), + productDetails.getProductType(), + "", // iconUrl + "", // subscriptionPeriod + "", // freeTrialPeriod + "", // introductoryPricePeriod + oneTimeDetails.getPriceCurrencyCode(), + toPrice(0), + 0); + } else { + List offerDetailsList = + productDetails.getSubscriptionOfferDetails(); + if (offerDetailsList == null || offerDetailsList.isEmpty()) { + return createEmpty(productDetails); + } + + ProductDetails.SubscriptionOfferDetails selectedOffer = null; + String targetBasePlanId = offerDetailsList.get(0).getBasePlanId(); + for (ProductDetails.SubscriptionOfferDetails offer : offerDetailsList) { + if (Objects.equals(targetBasePlanId, offer.getBasePlanId()) + && offer.getOfferId() != null && !offer.getOfferId().isEmpty()) { + selectedOffer = offer; + break; + } + } + if (selectedOffer == null) { + selectedOffer = offerDetailsList.get(0); + } + + List phases = + selectedOffer.getPricingPhases().getPricingPhaseList(); + if (phases == null || phases.isEmpty()) { + return createEmpty(productDetails); + } + + ProductDetails.PricingPhase recurringPhase = phases.get(phases.size() - 1); + String subscriptionPeriod = recurringPhase.getBillingPeriod(); + String currency = recurringPhase.getPriceCurrencyCode(); + String value = toPrice(recurringPhase.getPriceAmountMicros()); + + String freeTrialPeriod = ""; + String introductoryPricePeriod = ""; + String introductoryPriceCurrency = recurringPhase.getPriceCurrencyCode(); + String introductoryPriceValue = toPrice(0); + int introductoryPriceCycles = 0; + + if (phases.size() == 2) { + ProductDetails.PricingPhase firstPhase = phases.get(0); + if (firstPhase.getPriceAmountMicros() == 0) { + freeTrialPeriod = firstPhase.getBillingPeriod(); + } else { + introductoryPricePeriod = firstPhase.getBillingPeriod(); + introductoryPriceCurrency = firstPhase.getPriceCurrencyCode(); + introductoryPriceValue = toPrice(firstPhase.getPriceAmountMicros()); + introductoryPriceCycles = firstPhase.getBillingCycleCount(); + } + } else if (phases.size() >= 3) { + ProductDetails.PricingPhase firstPhase = phases.get(0); + freeTrialPeriod = firstPhase.getBillingPeriod(); + + ProductDetails.PricingPhase secondPhase = phases.get(1); + introductoryPricePeriod = secondPhase.getBillingPeriod(); + introductoryPriceCurrency = secondPhase.getPriceCurrencyCode(); + introductoryPriceValue = toPrice(secondPhase.getPriceAmountMicros()); + introductoryPriceCycles = secondPhase.getBillingCycleCount(); + } + + return new ItemDetails( + productDetails.getProductId(), + productDetails.getTitle(), + productDetails.getDescription(), + currency, + value, + productDetails.getProductType(), + "", // iconUrl + subscriptionPeriod, + freeTrialPeriod, + introductoryPricePeriod, + introductoryPriceCurrency, + introductoryPriceValue, + introductoryPriceCycles); + } + } + + private static ItemDetails createEmpty(ProductDetails productDetails) { return new ItemDetails( - skuDetails.getSku(), - skuDetails.getTitle(), - skuDetails.getDescription(), - skuDetails.getPriceCurrencyCode(), - toPrice(skuDetails.getPriceAmountMicros()), - skuDetails.getType(), skuDetails.getIconUrl(), skuDetails.getSubscriptionPeriod(), - skuDetails.getFreeTrialPeriod(), - skuDetails.getIntroductoryPricePeriod(), - skuDetails.getPriceCurrencyCode(), - toPrice(skuDetails.getIntroductoryPriceAmountMicros()), - skuDetails.getIntroductoryPriceCycles()); + productDetails.getProductId(), + productDetails.getTitle(), + productDetails.getDescription(), + "", // currency + toPrice(0), // value + productDetails.getProductType(), + "", // iconUrl + "", // subscriptionPeriod + "", // freeTrialPeriod + "", // introductoryPricePeriod + "", // introductoryPriceCurrency + toPrice(0), // introductoryPriceValue + 0 // introductoryPriceCycles + ); } /** @@ -147,7 +250,7 @@ public Bundle toBundle() { * Takes a price amount in micro units (1,000,000 micro units = 1 unit) and converts it to a * String representing the price amount in units. * - * The reason we need this method is because {@link SkuDetails} provides either a price amount + * The reason we need this method is because {@link ProductDetails} provides either a price amount * in units *with* the currency symbol, or a price amount in micro units (without the currency * symbol) while we need a price without the currency symbol. * diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/LegacyPurchaseDetails.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/LegacyPurchaseDetails.java index 1041c9e7..ff60b36c 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/LegacyPurchaseDetails.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/LegacyPurchaseDetails.java @@ -53,7 +53,7 @@ private LegacyPurchaseDetails(String id, String purchaseToken, boolean acknowled */ public static LegacyPurchaseDetails create(Purchase purchase) { return new LegacyPurchaseDetails( - purchase.getSkus().get(0), + purchase.getProducts().get(0), purchase.getPurchaseToken(), purchase.isAcknowledged(), toChromiumPurchaseState(purchase.getPurchaseState()), diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java index 1da878f2..dd4eb342 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java @@ -58,8 +58,8 @@ public void call(BillingWrapper billing) { BillingResultMerger merger = new BillingResultMerger<>(this::respond); - billing.queryPurchaseHistory(BillingClient.SkuType.INAPP, merger::setInAppResult); - billing.queryPurchaseHistory(BillingClient.SkuType.SUBS, merger::setSubsResult); + billing.queryPurchaseHistory(BillingClient.ProductType.INAPP, merger::setInAppResult); + billing.queryPurchaseHistory(BillingClient.ProductType.SUBS, merger::setSubsResult); } private void respond(BillingResult result, diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchasesCall.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchasesCall.java index ab1c537a..a1ac0ff2 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchasesCall.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchasesCall.java @@ -60,8 +60,8 @@ public void call(BillingWrapper billing) { BillingResultMerger merger = new BillingResultMerger<>(this::respond); - billing.queryPurchases(BillingClient.SkuType.INAPP, merger::setInAppResult); - billing.queryPurchases(BillingClient.SkuType.SUBS, merger::setSubsResult); + billing.queryPurchases(BillingClient.ProductType.INAPP, merger::setInAppResult); + billing.queryPurchases(BillingClient.ProductType.SUBS, merger::setSubsResult); } private void respond(BillingResult result, @Nullable List purchaseList) { diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/PurchaseDetails.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/PurchaseDetails.java index 7a8d2bb1..4b778dc7 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/PurchaseDetails.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/PurchaseDetails.java @@ -31,7 +31,7 @@ public class PurchaseDetails { static final String KEY_PURCHASE_TOKEN = "purchaseDetails.purchaseToken"; /** - * This is the id according to Chromium, which corresponds to {@link Purchase#getSku}, not to + * This is the id according to Chromium, which corresponds to {@link Purchase#getProducts}, not to * {@link Purchase#getOrderId}. */ public final String id; @@ -46,14 +46,14 @@ protected PurchaseDetails(String id, String purchaseToken) { * Creates this class from a Play Billing {@link Purchase}. */ public static PurchaseDetails create(Purchase purchase) { - return new PurchaseDetails(purchase.getSkus().get(0), purchase.getPurchaseToken()); + return new PurchaseDetails(purchase.getProducts().get(0), purchase.getPurchaseToken()); } /** * Creates this class from a Play Billing {@link PurchaseHistoryRecord}. */ public static PurchaseDetails create(PurchaseHistoryRecord record) { - return new PurchaseDetails(record.getSkus().get(0), record.getPurchaseToken()); + return new PurchaseDetails(record.getProducts().get(0), record.getPurchaseToken()); } /** diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java index ecda3d84..4de2b8e2 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java @@ -23,8 +23,8 @@ import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; import com.google.androidbrowserhelper.playbilling.digitalgoods.ConnectedBillingWrapper; import java.util.List; @@ -46,20 +46,20 @@ interface Listener { void connect(BillingClientStateListener callback); /** - * Get {@link SkuDetails} objects for the provided SKUs. + * Get {@link ProductDetails} objects for the provided product IDs. */ - void querySkuDetails(@BillingClient.SkuType String skuType, List skus, - SkuDetailsResponseListener callback); + void queryProductDetails(@BillingClient.ProductType String productType, List productIds, + ProductDetailsResponseListener callback); /** * Returns details for currently owned items. */ - void queryPurchases(@BillingClient.SkuType String skuType, PurchasesResponseListener callback); + void queryPurchases(@BillingClient.ProductType String productType, PurchasesResponseListener callback); /** * Returns details for all previously purchased items. */ - void queryPurchaseHistory(@BillingClient.SkuType String skuType, + void queryPurchaseHistory(@BillingClient.ProductType String productType, PurchaseHistoryResponseListener callback); /** @@ -76,6 +76,6 @@ void queryPurchaseHistory(@BillingClient.SkuType String skuType, * Launches the Payment Flow. If it returns {@code true}, * {@link Listener#onPurchaseFlowComplete} should be called. */ - boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData methodData); + boolean launchPaymentFlow(Activity activity, ProductDetails productDetails, MethodData methodData); } diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java index 7069120f..238dc116 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java @@ -26,8 +26,8 @@ import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; import java.util.ArrayList; import java.util.Collections; @@ -50,12 +50,12 @@ public class MockBillingWrapper implements BillingWrapper { private InvocationTracker mConsumeInvocation = new InvocationTracker<>(); - private MultiSkuTypeInvocationTracker, SkuDetailsResponseListener> - mQuerySkuDetailsInvocation = new MultiSkuTypeInvocationTracker<>(); - private MultiSkuTypeInvocationTracker - mQueryPurchasesInvocation = new MultiSkuTypeInvocationTracker<>(); - private MultiSkuTypeInvocationTracker - mQueryPurchaseHistoryInvocation = new MultiSkuTypeInvocationTracker<>(); + private MultiProductTypeInvocationTracker, ProductDetailsResponseListener> + mQueryProductDetailsInvocation = new MultiProductTypeInvocationTracker<>(); + private MultiProductTypeInvocationTracker + mQueryPurchasesInvocation = new MultiProductTypeInvocationTracker<>(); + private MultiProductTypeInvocationTracker + mQueryPurchaseHistoryInvocation = new MultiProductTypeInvocationTracker<>(); private Intent mPlayBillingFlowLaunchIntent; @@ -69,19 +69,19 @@ public void connect(BillingClientStateListener callback) { } @Override - public void querySkuDetails(@BillingClient.SkuType String skuType, List skus, - SkuDetailsResponseListener callback) { - mQuerySkuDetailsInvocation.call(skuType, skus, callback); + public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, + ProductDetailsResponseListener callback) { + mQueryProductDetailsInvocation.call(productType, productIds, callback); } @Override - public void queryPurchases(String skuType, PurchasesResponseListener callback) { - mQueryPurchasesInvocation.call(skuType, null, callback); + public void queryPurchases(@BillingClient.ProductType String productType, PurchasesResponseListener callback) { + mQueryPurchasesInvocation.call(productType, null, callback); } @Override - public void queryPurchaseHistory(String skuType, PurchaseHistoryResponseListener callback) { - mQueryPurchaseHistoryInvocation.call(skuType, null, callback); + public void queryPurchaseHistory(@BillingClient.ProductType String productType, PurchaseHistoryResponseListener callback) { + mQueryPurchaseHistoryInvocation.call(productType, null, callback); } @Override @@ -95,7 +95,7 @@ public void consume(String token, ConsumeResponseListener callback) { } @Override - public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData data) { + public boolean launchPaymentFlow(Activity activity, ProductDetails productDetails, MethodData data) { mPlayBillingFlowLaunchIntent = activity.getIntent(); mLaunchPaymentFlowLatch.countDown(); return mPaymentFlowSuccessful; @@ -110,42 +110,42 @@ public void triggerDisconnected() { mConnectionStateListener.onBillingServiceDisconnected(); } - public void triggerOnGotSkuDetails(List skuDetails) { - triggerOnGotInAppSkuDetails(skuDetails); - triggerOnGotSubsSkuDetails(Collections.emptyList()); + public void triggerOnGotProductDetails(List productDetails) { + triggerOnGotInAppProductDetails(productDetails); + triggerOnGotSubsProductDetails(Collections.emptyList()); } - public void triggerOnGotInAppSkuDetails(List skuDetails) { - triggerOnGotInAppSkuDetails(BillingClient.BillingResponseCode.OK, skuDetails); + public void triggerOnGotInAppProductDetails(List productDetails) { + triggerOnGotInAppProductDetails(BillingClient.BillingResponseCode.OK, productDetails); } - public void triggerOnGotInAppSkuDetails(int responseCode, List skuDetails) { - mQuerySkuDetailsInvocation.getCallback(BillingClient.SkuType.INAPP) - .onSkuDetailsResponse(toResult(responseCode), skuDetails); + public void triggerOnGotInAppProductDetails(int responseCode, List productDetails) { + mQueryProductDetailsInvocation.getCallback(BillingClient.ProductType.INAPP) + .onProductDetailsResponse(toResult(responseCode), productDetails); } - public void triggerOnGotSubsSkuDetails(List skuDetails) { - triggerOnGotSubsSkuDetails(BillingClient.BillingResponseCode.OK, skuDetails); + public void triggerOnGotSubsProductDetails(List productDetails) { + triggerOnGotSubsProductDetails(BillingClient.BillingResponseCode.OK, productDetails); } - public void triggerOnGotSubsSkuDetails(int responseCode, List skuDetails) { - mQuerySkuDetailsInvocation.getCallback(BillingClient.SkuType.SUBS) - .onSkuDetailsResponse(toResult(responseCode), skuDetails); + public void triggerOnGotSubsProductDetails(int responseCode, List productDetails) { + mQueryProductDetailsInvocation.getCallback(BillingClient.ProductType.SUBS) + .onProductDetailsResponse(toResult(responseCode), productDetails); } public void triggerOnGotInAppPurchaseDetails(List details) { - mQueryPurchasesInvocation.getCallback(BillingClient.SkuType.INAPP) + mQueryPurchasesInvocation.getCallback(BillingClient.ProductType.INAPP) .onQueryPurchasesResponse(toResult(BillingClient.BillingResponseCode.OK), details); } public void triggerOnGotSubsPurchaseDetails(List details) { - mQueryPurchasesInvocation.getCallback(BillingClient.SkuType.SUBS) + mQueryPurchasesInvocation.getCallback(BillingClient.ProductType.SUBS) .onQueryPurchasesResponse(toResult(BillingClient.BillingResponseCode.OK), details); } - public void triggerOnPurchaseHistoryResponse(String skuType, + public void triggerOnPurchaseHistoryResponse(String productType, List records) { - mQueryPurchaseHistoryInvocation.getCallback(skuType) + mQueryPurchaseHistoryInvocation.getCallback(productType) .onPurchaseHistoryResponse(toResult(BillingClient.BillingResponseCode.OK), records); } @@ -165,8 +165,8 @@ public boolean waitForConnect() throws InterruptedException { return wait(mConnectLatch); } - public boolean waitForQuerySkuDetails() throws InterruptedException { - return mQuerySkuDetailsInvocation.waitUntilCalled(); + public boolean waitForQueryProductDetails() throws InterruptedException { + return mQueryProductDetailsInvocation.waitUntilCalled(); } public boolean waitForLaunchPaymentFlow() throws InterruptedException { @@ -189,18 +189,18 @@ public void setPaymentFlowWillBeSuccessful(boolean successful) { mPaymentFlowSuccessful = successful; } - public List getQueriedSkuDetails() { - List skuDetails = new ArrayList<>(); + public List getQueriedProductDetails() { + List productDetails = new ArrayList<>(); - List inapp = mQuerySkuDetailsInvocation.getArgument(BillingClient.SkuType.INAPP); - List subs = mQuerySkuDetailsInvocation.getArgument(BillingClient.SkuType.SUBS); + List inapp = mQueryProductDetailsInvocation.getArgument(BillingClient.ProductType.INAPP); + List subs = mQueryProductDetailsInvocation.getArgument(BillingClient.ProductType.SUBS); if (inapp == null && subs == null) return null; - if (inapp != null) skuDetails.addAll(inapp); - if (subs != null) skuDetails.addAll(subs); + if (inapp != null) productDetails.addAll(inapp); + if (subs != null) productDetails.addAll(subs); - return skuDetails; + return productDetails; } public String getConsumeToken() { diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiSkuTypeInvocationTracker.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiProductTypeInvocationTracker.java similarity index 67% rename from playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiSkuTypeInvocationTracker.java rename to playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiProductTypeInvocationTracker.java index 69a7831f..0aedb92a 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiSkuTypeInvocationTracker.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MultiProductTypeInvocationTracker.java @@ -19,31 +19,31 @@ /** * A helper class for {@link MockBillingWrapper} that helps keep track of which methods were called * and the arguments they were called with. This class keeps track of two calls - one for the inapp - * SkuType and one for the subs SkuType. + * ProductType and one for the subs ProductType. */ -class MultiSkuTypeInvocationTracker { +class MultiProductTypeInvocationTracker { private final InvocationTracker mInAppInvocation = new InvocationTracker<>(); private final InvocationTracker mSubsInvocation = new InvocationTracker<>(); private InvocationTracker - getTracker(@BillingClient.SkuType String skuType) { - return BillingClient.SkuType.INAPP.equals(skuType) ? mInAppInvocation : mSubsInvocation; + getTracker(@BillingClient.ProductType String productType) { + return BillingClient.ProductType.INAPP.equals(productType) ? mInAppInvocation : mSubsInvocation; } - /** Pretend that the method was called with the given SKU type, argument and callback. */ - public void call(@BillingClient.SkuType String skuType, Argument arg, Callback callback) { - getTracker(skuType).call(arg, callback); + /** Pretend that the method was called with the given product type, argument and callback. */ + public void call(@BillingClient.ProductType String productType, Argument arg, Callback callback) { + getTracker(productType).call(arg, callback); } /** Returns the argument that the method was previously called with, if any. */ - public Argument getArgument(@BillingClient.SkuType String skuType) { - return getTracker(skuType).getArgument(); + public Argument getArgument(@BillingClient.ProductType String productType) { + return getTracker(productType).getArgument(); } /** Returns the callback that the method was previously called with, if any. */ - public Callback getCallback(@BillingClient.SkuType String skuType) { - return getTracker(skuType).getCallback(); + public Callback getCallback(@BillingClient.ProductType String productType) { + return getTracker(productType).getCallback(); } /** Wait until the method was called, returns {@code false} if the wait timed out. */ diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java index 8471561f..879b1df5 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java @@ -25,7 +25,7 @@ import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode; import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.ProductDetails; import com.google.androidbrowserhelper.playbilling.digitalgoods.BillingResultMerger; import java.util.Collections; @@ -60,7 +60,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { mMethodData = MethodData.fromIntent(getIntent()); if (mMethodData == null) { - fail("Could not parse SKU."); + fail("Could not parse product ID."); return; } if (mMethodData.isPriceChangeConfirmation) { @@ -102,26 +102,26 @@ public void onDisconnected() { } public void onConnected() { - BillingResultMerger merger = new BillingResultMerger<>(this::onSkusQueried); + BillingResultMerger merger = new BillingResultMerger<>(this::onProductDetailsQueried); List ids = Collections.singletonList(mMethodData.sku); - mWrapper.querySkuDetails(BillingClient.SkuType.INAPP, ids, merger::setInAppResult); - mWrapper.querySkuDetails(BillingClient.SkuType.SUBS, ids, merger::setSubsResult); + mWrapper.queryProductDetails(BillingClient.ProductType.INAPP, ids, merger::setInAppResult); + mWrapper.queryProductDetails(BillingClient.ProductType.SUBS, ids, merger::setSubsResult); } - private void onSkusQueried(BillingResult result, List skus) { - if (skus == null || skus.isEmpty()) { - fail("Play Billing returned did not find SKU."); + private void onProductDetailsQueried(BillingResult result, List productDetailsList) { + if (productDetailsList == null || productDetailsList.isEmpty()) { + fail("Play Billing did not find product."); return; } - SkuDetails sku = skus.get(0); + ProductDetails productDetails = productDetailsList.get(0); - launchPaymentFlow(sku); + launchPaymentFlow(productDetails); } - private void launchPaymentFlow(SkuDetails sku) { - if (mWrapper.launchPaymentFlow(this, sku, mMethodData)) + private void launchPaymentFlow(ProductDetails productDetails) { + if (mWrapper.launchPaymentFlow(this, productDetails, mMethodData)) return; fail("Payment attempt failed (have you already bought the item?)."); diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java index 05ca26d6..2198ce8c 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java @@ -29,14 +29,15 @@ import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PendingPurchasesParams; import com.android.billingclient.api.QueryPurchasesParams; +import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsParams; -import com.android.billingclient.api.SkuDetailsResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; +import java.util.ArrayList; import java.util.List; /** @@ -77,29 +78,36 @@ public void connect(BillingClientStateListener callback) { } @Override - public void querySkuDetails(@BillingClient.SkuType String skuType, List skus, - SkuDetailsResponseListener callback) { - SkuDetailsParams params = SkuDetailsParams - .newBuilder() - .setSkusList(skus) - .setType(skuType) + public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, + ProductDetailsResponseListener callback) { + List productList = new ArrayList<>(); + for (String productId : productIds) { + productList.add( + QueryProductDetailsParams.Product.newBuilder() + .setProductId(productId) + .setProductType(productType) + .build() + ); + } + QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() + .setProductList(productList) .build(); - mClient.querySkuDetailsAsync(params, callback); + mClient.queryProductDetailsAsync(params, callback); } @Override - public void queryPurchases(@BillingClient.SkuType String skuType, + public void queryPurchases(@BillingClient.ProductType String productType, PurchasesResponseListener callback) { QueryPurchasesParams params = QueryPurchasesParams.newBuilder() - .setProductType(skuType) + .setProductType(productType) .build(); mClient.queryPurchasesAsync(params, callback); } @Override - public void queryPurchaseHistory(String skuType, PurchaseHistoryResponseListener callback) { - mClient.queryPurchaseHistoryAsync(skuType, callback); + public void queryPurchaseHistory(@BillingClient.ProductType String productType, PurchaseHistoryResponseListener callback) { + mClient.queryPurchaseHistoryAsync(productType, callback); } @Override @@ -123,11 +131,27 @@ public void consume(String token, ConsumeResponseListener callback) { } @Override - public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData methodData) { + public boolean launchPaymentFlow(Activity activity, ProductDetails productDetails, MethodData methodData) { BillingFlowParams.SubscriptionUpdateParams.Builder subUpdateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder(); - BillingFlowParams.Builder builder = BillingFlowParams.newBuilder(); - builder.setSkuDetails(sku); + + BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails); + + if (productDetails.getProductType().equals(BillingClient.ProductType.SUBS)) { + List offerDetails = + productDetails.getSubscriptionOfferDetails(); + if (offerDetails != null && !offerDetails.isEmpty()) { + productDetailsParamsBuilder.setOfferToken(offerDetails.get(0).getOfferToken()); + } + } + + List productDetailsParamsList = new ArrayList<>(); + productDetailsParamsList.add(productDetailsParamsBuilder.build()); + + BillingFlowParams.Builder builder = BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList); if (methodData.replacementMode != null) { subUpdateParamsBuilder.setSubscriptionReplacementMode(methodData.replacementMode); diff --git a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapperTest.java b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapperTest.java index fec9533d..2051f1f1 100644 --- a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapperTest.java +++ b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapperTest.java @@ -40,11 +40,11 @@ public class ConnectedBillingWrapperTest { @Test public void delays_getDetailsCall() { - mConnectedWrapper.querySkuDetails("type", Collections.singletonList("id1"), null); - assertNull(mInnerBillingWrapper.getQueriedSkuDetails()); + mConnectedWrapper.queryProductDetails("type", Collections.singletonList("id1"), null); + assertNull(mInnerBillingWrapper.getQueriedProductDetails()); mInnerBillingWrapper.triggerConnected(); - assertEquals(mInnerBillingWrapper.getQueriedSkuDetails().get(0), "id1"); + assertEquals(mInnerBillingWrapper.getQueriedProductDetails().get(0), "id1"); } @Test @@ -70,8 +70,8 @@ public void forwards_getDetailsCall() { mConnectedWrapper.connect(null); mInnerBillingWrapper.triggerConnected(); - mConnectedWrapper.querySkuDetails("type", Collections.singletonList("id1"), null); - assertEquals(mInnerBillingWrapper.getQueriedSkuDetails().get(0), "id1"); + mConnectedWrapper.queryProductDetails("type", Collections.singletonList("id1"), null); + assertEquals(mInnerBillingWrapper.getQueriedProductDetails().get(0), "id1"); } @Test diff --git a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCallTest.java b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCallTest.java index 2eb7982f..973959dd 100644 --- a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCallTest.java +++ b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/GetDetailsCallTest.java @@ -19,7 +19,7 @@ import android.os.Parcelable; import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.ProductDetails; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapperFactory; import com.google.androidbrowserhelper.playbilling.provider.MockBillingWrapper; @@ -31,6 +31,7 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.internal.DoNotInstrument; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -49,7 +50,6 @@ @Config(sdk = {Build.VERSION_CODES.O_MR1}) public class GetDetailsCallTest { private final static DigitalGoodsCallback EMPTY_CALLBACK = (name, args) -> {}; - private final static String SKU_DETAILS = ItemDetailsTest.createTestJsonSkuDetails(); private final MockBillingWrapper mBillingWrapper = new MockBillingWrapper(); private DigitalGoodsRequestHandler mHandler; @@ -87,29 +87,40 @@ public void callsCallback() throws InterruptedException { assertTrue(mHandler.handle( GetDetailsCall.COMMAND_NAME, args, callback)); mBillingWrapper.triggerConnected(); - assertTrue(mBillingWrapper.waitForQuerySkuDetails()); - mBillingWrapper.triggerOnGotInAppSkuDetails(Collections.emptyList()); - mBillingWrapper.triggerOnGotSubsSkuDetails(Collections.emptyList()); + assertTrue(mBillingWrapper.waitForQueryProductDetails()); + mBillingWrapper.triggerOnGotInAppProductDetails(Collections.emptyList()); + mBillingWrapper.triggerOnGotSubsProductDetails(Collections.emptyList()); assertTrue(callbackTriggered.await(5, TimeUnit.SECONDS)); } @Test public void parsesResult_inApp() throws InterruptedException, JSONException { + ProductDetails productDetails = ItemDetailsTest.mockInAppProductDetails( + "id", "title", "desc", "GBP", 123_000_000); checkParsesResult( - Collections.singletonList(new SkuDetails(SKU_DETAILS)), - Collections.emptyList()); + Collections.singletonList(productDetails), + Collections.emptyList(), + true); } @Test public void parsesResult_subs() throws InterruptedException, JSONException { + List phases = new ArrayList<>(); + phases.add(ItemDetailsTest.mockPricingPhase("week", "GBP", 0, 1)); + phases.add(ItemDetailsTest.mockPricingPhase("day", "GBP", 45_000_000, 3)); + phases.add(ItemDetailsTest.mockPricingPhase("month", "GBP", 123_000_000, 1)); + + ProductDetails productDetails = ItemDetailsTest.mockSubscriptionProductDetails( + "id", "title", "desc", "base_plan", "offer_id", phases); checkParsesResult( Collections.emptyList(), - Collections.singletonList(new SkuDetails(SKU_DETAILS))); + Collections.singletonList(productDetails), + false); } - private void checkParsesResult(List inAppSkuDetails, - List subsSkuDetails) throws InterruptedException { + private void checkParsesResult(List inAppProductDetails, + List subsProductDetails, boolean isInApp) throws InterruptedException { Bundle args = GetDetailsCall.createBundleForTesting("id1"); CountDownLatch callbackTriggered = new CountDownLatch(1); @@ -121,7 +132,11 @@ private void checkParsesResult(List inAppSkuDetails, Parcelable[] array = bundle.getParcelableArray(RESPONSE_GET_DETAILS_DETAILS_LIST); ItemDetails details = ItemDetails.create((Bundle) array[0]); - ItemDetailsTest.assertTestItemDetails(details); + if (isInApp) { + ItemDetailsTest.assertTestItemDetails_inApp(details); + } else { + ItemDetailsTest.assertTestItemDetails(details); + } callbackTriggered.countDown(); }; @@ -129,9 +144,9 @@ private void checkParsesResult(List inAppSkuDetails, assertTrue(mHandler.handle(GetDetailsCall.COMMAND_NAME, args, callback)); mBillingWrapper.triggerConnected(); - assertTrue(mBillingWrapper.waitForQuerySkuDetails()); - mBillingWrapper.triggerOnGotInAppSkuDetails(inAppSkuDetails); - mBillingWrapper.triggerOnGotSubsSkuDetails(subsSkuDetails); + assertTrue(mBillingWrapper.waitForQueryProductDetails()); + mBillingWrapper.triggerOnGotInAppProductDetails(inAppProductDetails); + mBillingWrapper.triggerOnGotSubsProductDetails(subsProductDetails); assertTrue(callbackTriggered.await(5, TimeUnit.SECONDS)); } diff --git a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetailsTest.java b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetailsTest.java index 56ac7a5b..78b37da0 100644 --- a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetailsTest.java +++ b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ItemDetailsTest.java @@ -17,7 +17,13 @@ import android.os.Build; import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.ProductDetails; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.json.JSONException; import org.junit.Test; @@ -60,58 +66,90 @@ public class ItemDetailsTest { private static final String INTRO_VALUE_STR = "45.000000"; @Test - public void create() throws JSONException { - String json = createTestJsonSkuDetails(); - ItemDetails item = ItemDetails.create(new SkuDetails(json)); + public void create_inApp() { + ProductDetails productDetails = mockInAppProductDetails(ID, TITLE, DESC, CURRENCY, VALUE); + ItemDetails item = ItemDetails.create(productDetails); + assertTestItemDetails_inApp(item); + } + + @Test + public void create_subscription() { + List phases = new ArrayList<>(); + phases.add(mockPricingPhase(FREE_PERIOD, CURRENCY, 0, 1)); + phases.add(mockPricingPhase(INTRO_PERIOD, CURRENCY, INTRO_VALUE, INTRO_CYCLES)); + phases.add(mockPricingPhase(SUBS_PERIOD, CURRENCY, VALUE, 1)); + ProductDetails productDetails = mockSubscriptionProductDetails(ID, TITLE, DESC, + "base_plan", "offer_id", phases); + + ItemDetails item = ItemDetails.create(productDetails); assertTestItemDetails(item); } @Test - public void create_mandatoryOnly() throws JSONException { - String json = createTestJsonSkuDetails_mandatoryOnly(); + public void create_mandatoryOnly() { + ProductDetails productDetails = mockInAppProductDetails(ID, TITLE, DESC, CURRENCY, VALUE); + ItemDetails item = ItemDetails.create(productDetails); + assertTestItemDetails_inApp(item); + } - ItemDetails item = ItemDetails.create(new SkuDetails(json)); - assertTestItemDetails_mandatoryOnly(item); + @Test + public void bundleConversion_inApp() { + ProductDetails productDetails = mockInAppProductDetails(ID, TITLE, DESC, CURRENCY, VALUE); + ItemDetails item = ItemDetails.create(ItemDetails.create(productDetails).toBundle()); + assertTestItemDetails_inApp(item); } @Test - public void bundleConversion() throws JSONException { - String json = createTestJsonSkuDetails(); + public void bundleConversion_subscription() { + List phases = new ArrayList<>(); + phases.add(mockPricingPhase(FREE_PERIOD, CURRENCY, 0, 1)); + phases.add(mockPricingPhase(INTRO_PERIOD, CURRENCY, INTRO_VALUE, INTRO_CYCLES)); + phases.add(mockPricingPhase(SUBS_PERIOD, CURRENCY, VALUE, 1)); - ItemDetails item = - ItemDetails.create(ItemDetails.create(new SkuDetails((json))).toBundle()); + ProductDetails productDetails = mockSubscriptionProductDetails(ID, TITLE, DESC, + "base_plan", "offer_id", phases); + + ItemDetails item = ItemDetails.create(ItemDetails.create(productDetails).toBundle()); assertTestItemDetails(item); } @Test - public void bundleConversion_mandatoryOnly() throws JSONException { - String json = createTestJsonSkuDetails_mandatoryOnly(); + public void create_subscription_no_offers() { + List phases = new ArrayList<>(); + phases.add(mockPricingPhase(SUBS_PERIOD, CURRENCY, VALUE, 1)); + + ProductDetails productDetails = mockSubscriptionProductDetails(ID, TITLE, DESC, + "base_plan", null, phases); - ItemDetails item = - ItemDetails.create(ItemDetails.create(new SkuDetails((json))).toBundle()); - assertTestItemDetails_mandatoryOnly(item); + ItemDetails item = ItemDetails.create(productDetails); + assertItemDetails(item, ID, TITLE, DESC, CURRENCY, VALUE_STR, "subs", "", + SUBS_PERIOD, "", "", CURRENCY, "0.000000", 0); } - public static String createTestJsonSkuDetails() { - return createSkuDetailsJson(ID, TITLE, DESC, CURRENCY, VALUE, TYPE, - ICON_URL, SUBS_PERIOD, FREE_PERIOD, INTRO_PERIOD, INTRO_VALUE, INTRO_CYCLES); + @Test + public void create_subscription_no_base_plans() { + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn(ID); + when(productDetails.getTitle()).thenReturn(TITLE); + when(productDetails.getDescription()).thenReturn(DESC); + when(productDetails.getProductType()).thenReturn(BillingClient.ProductType.SUBS); + when(productDetails.getSubscriptionOfferDetails()).thenReturn(null); + + ItemDetails item = ItemDetails.create(productDetails); + assertItemDetails(item, ID, TITLE, DESC, "", "0.000000", "subs", "", + "", "", "", "", "0.000000", 0); } public static void assertTestItemDetails(ItemDetails item) { - assertItemDetails(item, ID, TITLE, DESC, CURRENCY, VALUE_STR, TYPE, ICON_URL, + assertItemDetails(item, ID, TITLE, DESC, CURRENCY, VALUE_STR, "subs", "", SUBS_PERIOD, FREE_PERIOD, INTRO_PERIOD, INTRO_CURRENCY, INTRO_VALUE_STR, INTRO_CYCLES); } - private static String createTestJsonSkuDetails_mandatoryOnly() { - return createSkuDetailsJson(ID, TITLE, DESC, CURRENCY, VALUE, TYPE, - null, null, null, null, null, 0); - } - - private static void assertTestItemDetails_mandatoryOnly(ItemDetails item) { - assertItemDetails(item, ID, TITLE, DESC, CURRENCY, VALUE_STR, TYPE, "", - "", "", "", INTRO_CURRENCY, "0.000000", 0); + public static void assertTestItemDetails_inApp(ItemDetails item) { + assertItemDetails(item, ID, TITLE, DESC, CURRENCY, VALUE_STR, "inapp", "", + "", "", "", CURRENCY, "0.000000", 0); } static void assertItemDetails(ItemDetails item, String id, String title, @@ -135,34 +173,55 @@ static void assertItemDetails(ItemDetails item, String id, String title, assertEquals(item.introductoryPriceCycles, introductoryPriceCycles); } - static String createSkuDetailsJson(String id, String title, String description, - String currency, long value, String type, @Nullable String iconUrl, - @Nullable String subscriptionPeriod, @Nullable String freeTrialPeriod, - @Nullable String introductoryPricePeriod, @Nullable Long introductoryPriceValue, - int introductoryPriceCycles) { - StringBuilder b = new StringBuilder(); - - b.append("{"); - - addFieldWithoutLeadingComma(b, "productId", id); - addField(b, "title", title); - addField(b, "description", description); - addField(b, "price_amount_micros", value); - addField(b, "price_currency_code", currency); - addField(b, "type", type); - addOptionalField(b, "iconUrl", iconUrl); - - addOptionalField(b, "subscriptionPeriod", subscriptionPeriod); - addOptionalField(b, "freeTrialPeriod", freeTrialPeriod); - addOptionalField(b, "introductoryPricePeriod", introductoryPricePeriod); - addOptionalField(b, "introductoryPriceAmountMicros", introductoryPriceValue); - addField(b, "introductoryPriceCycles", introductoryPriceCycles); - - // The Play Billing library requires that all SkuDetails have a type, but we don't use it - // in our testing, so just set it to an arbitrary type. - addField(b, "type", BillingClient.SkuType.INAPP); - - b.append("}"); - return b.toString(); + public static ProductDetails mockInAppProductDetails(String id, String title, String description, + String currency, long priceAmountMicros) { + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn(id); + when(productDetails.getTitle()).thenReturn(title); + when(productDetails.getDescription()).thenReturn(description); + when(productDetails.getProductType()).thenReturn(BillingClient.ProductType.INAPP); + + ProductDetails.OneTimePurchaseOfferDetails oneTimeDetails = mock(ProductDetails.OneTimePurchaseOfferDetails.class); + when(oneTimeDetails.getPriceCurrencyCode()).thenReturn(currency); + when(oneTimeDetails.getPriceAmountMicros()).thenReturn(priceAmountMicros); + when(productDetails.getOneTimePurchaseOfferDetails()).thenReturn(oneTimeDetails); + + return productDetails; + } + + public static ProductDetails mockSubscriptionProductDetails(String id, String title, String description, + String basePlanId, @Nullable String offerId, List phases) { + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn(id); + when(productDetails.getTitle()).thenReturn(title); + when(productDetails.getDescription()).thenReturn(description); + when(productDetails.getProductType()).thenReturn(BillingClient.ProductType.SUBS); + + ProductDetails.SubscriptionOfferDetails offerDetails = mock(ProductDetails.SubscriptionOfferDetails.class); + when(offerDetails.getBasePlanId()).thenReturn(basePlanId); + when(offerDetails.getOfferId()).thenReturn(offerId); + if (offerId != null && !offerId.isEmpty()) { + when(offerDetails.getOfferToken()).thenReturn("offer_token_for_" + offerId); + } + + ProductDetails.PricingPhases pricingPhases = mock(ProductDetails.PricingPhases.class); + when(pricingPhases.getPricingPhaseList()).thenReturn(phases); + when(offerDetails.getPricingPhases()).thenReturn(pricingPhases); + + List offerDetailsList = new ArrayList<>(); + offerDetailsList.add(offerDetails); + when(productDetails.getSubscriptionOfferDetails()).thenReturn(offerDetailsList); + + return productDetails; + } + + public static ProductDetails.PricingPhase mockPricingPhase(String billingPeriod, String priceCurrencyCode, + long priceAmountMicros, int billingCycleCount) { + ProductDetails.PricingPhase phase = mock(ProductDetails.PricingPhase.class); + when(phase.getBillingPeriod()).thenReturn(billingPeriod); + when(phase.getPriceCurrencyCode()).thenReturn(priceCurrencyCode); + when(phase.getPriceAmountMicros()).thenReturn(priceAmountMicros); + when(phase.getBillingCycleCount()).thenReturn(billingCycleCount); + return phase; } } diff --git a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java index e4ed202e..4c1b1714 100644 --- a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java +++ b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java @@ -102,9 +102,9 @@ private void call(List inAppPurchaseHistory, mBillingWrapper.triggerConnected(); assertTrue(mBillingWrapper.waitForQueryPurchaseHistory()); - mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.SkuType.INAPP, + mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.ProductType.INAPP, inAppPurchaseHistory); - mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.SkuType.SUBS, + mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.ProductType.SUBS, subsPurchaseHistory); assertTrue(callbackTriggered.await(5, TimeUnit.SECONDS)); From d2ea99faea43ac9d5728fd7cabeee5fcc82ecae9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:11:45 +0000 Subject: [PATCH 09/24] feat(pblv8): make listPurchaseHistory return OK/empty list immediately and remove queryPurchaseHistory (FR-03) --- .../digitalgoods/ConnectedBillingWrapper.java | 6 --- .../digitalgoods/ListPurchaseHistoryCall.java | 30 +---------- .../playbilling/provider/BillingWrapper.java | 7 --- .../provider/MockBillingWrapper.java | 19 ------- .../provider/PlayBillingWrapper.java | 6 --- .../ListPurchaseHistoryCallTest.java | 52 +++---------------- 6 files changed, 10 insertions(+), 110 deletions(-) diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java index 0818991c..2a139f86 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java @@ -21,7 +21,6 @@ import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeResponseListener; -import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.ProductDetailsResponseListener; @@ -99,11 +98,6 @@ public void queryPurchases(String productType, PurchasesResponseListener callbac execute(() -> mInner.queryPurchases(productType, callback)); } - @Override - public void queryPurchaseHistory(String productType, PurchaseHistoryResponseListener callback) { - execute(() -> mInner.queryPurchaseHistory(productType, callback)); - } - @Override public void acknowledge(String token, AcknowledgePurchaseResponseListener callback) { execute(() -> mInner.acknowledge(token, callback)); diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java index dd4eb342..ba79073f 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCall.java @@ -19,14 +19,8 @@ import androidx.annotation.Nullable; -import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.PurchaseHistoryRecord; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapper; -import java.util.List; - /** * A class for parsing Digital Goods API calls from the browser and converting them into a format * suitable for calling the Play Billing library. @@ -56,29 +50,9 @@ public static ListPurchaseHistoryCall create(@Nullable DigitalGoodsCallback call public void call(BillingWrapper billing) { Logging.logListPurchaseHistoryCall(); - BillingResultMerger merger = new BillingResultMerger<>(this::respond); - - billing.queryPurchaseHistory(BillingClient.ProductType.INAPP, merger::setInAppResult); - billing.queryPurchaseHistory(BillingClient.ProductType.SUBS, merger::setSubsResult); - } - - private void respond(BillingResult result, - @Nullable List purchaseHistoryList) { - Logging.logListPurchaseHistoryResult(result); - - Parcelable[] parcelables = new Parcelable[0]; - if (purchaseHistoryList != null) { - parcelables = new Parcelable[purchaseHistoryList.size()]; - - int index = 0; - for (PurchaseHistoryRecord record : purchaseHistoryList) { - parcelables[index++] = PurchaseDetails.create(record).toBundle(); - } - } - Bundle args = new Bundle(); - args.putInt(KEY_RESPONSE_CODE, DigitalGoodsConverter.toChromiumResponseCode(result)); - args.putParcelableArray(KEY_PURCHASE_HISTORY_LIST, parcelables); + args.putInt(KEY_RESPONSE_CODE, DigitalGoodsConverter.CHROMIUM_RESULT_OK); + args.putParcelableArray(KEY_PURCHASE_HISTORY_LIST, new Parcelable[0]); mCallback.run(RESPONSE_COMMAND, args); } } \ No newline at end of file diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java index 4de2b8e2..8a485f76 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java @@ -21,7 +21,6 @@ import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeResponseListener; -import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.ProductDetailsResponseListener; @@ -56,12 +55,6 @@ void queryProductDetails(@BillingClient.ProductType String productType, List(); private MultiProductTypeInvocationTracker mQueryPurchasesInvocation = new MultiProductTypeInvocationTracker<>(); - private MultiProductTypeInvocationTracker - mQueryPurchaseHistoryInvocation = new MultiProductTypeInvocationTracker<>(); private Intent mPlayBillingFlowLaunchIntent; @@ -79,11 +75,6 @@ public void queryPurchases(@BillingClient.ProductType String productType, Purcha mQueryPurchasesInvocation.call(productType, null, callback); } - @Override - public void queryPurchaseHistory(@BillingClient.ProductType String productType, PurchaseHistoryResponseListener callback) { - mQueryPurchaseHistoryInvocation.call(productType, null, callback); - } - @Override public void acknowledge(String token, AcknowledgePurchaseResponseListener callback) { mAcknowledgeInvocation.call(token, callback); @@ -143,12 +134,6 @@ public void triggerOnGotSubsPurchaseDetails(List details) { .onQueryPurchasesResponse(toResult(BillingClient.BillingResponseCode.OK), details); } - public void triggerOnPurchaseHistoryResponse(String productType, - List records) { - mQueryPurchaseHistoryInvocation.getCallback(productType) - .onPurchaseHistoryResponse(toResult(BillingClient.BillingResponseCode.OK), records); - } - public void triggerAcknowledge(int responseCode) { mAcknowledgeInvocation.getCallback().onAcknowledgePurchaseResponse(toResult(responseCode)); } @@ -177,10 +162,6 @@ public boolean waitForQueryPurchases() throws InterruptedException { return mQueryPurchasesInvocation.waitUntilCalled(); } - public boolean waitForQueryPurchaseHistory() throws InterruptedException { - return mQueryPurchaseHistoryInvocation.waitUntilCalled(); - } - public void setListener(Listener listener) { mListener = listener; } diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java index 2198ce8c..a309fc5c 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java @@ -31,7 +31,6 @@ import com.android.billingclient.api.QueryPurchasesParams; import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.ProductDetails; @@ -105,11 +104,6 @@ public void queryPurchases(@BillingClient.ProductType String productType, mClient.queryPurchasesAsync(params, callback); } - @Override - public void queryPurchaseHistory(@BillingClient.ProductType String productType, PurchaseHistoryResponseListener callback) { - mClient.queryPurchaseHistoryAsync(productType, callback); - } - @Override public void acknowledge(String token, AcknowledgePurchaseResponseListener callback) { AcknowledgePurchaseParams params = AcknowledgePurchaseParams diff --git a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java index 4c1b1714..51c6ddd3 100644 --- a/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java +++ b/playbilling/src/test/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ListPurchaseHistoryCallTest.java @@ -18,12 +18,9 @@ import android.os.Bundle; import android.os.Parcelable; -import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.PurchaseHistoryRecord; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapperFactory; import com.google.androidbrowserhelper.playbilling.provider.MockBillingWrapper; -import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,12 +28,6 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.internal.DoNotInstrument; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static com.google.androidbrowserhelper.playbilling.digitalgoods.DigitalGoodsConverter.toChromiumResponseCode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -45,9 +36,6 @@ @DoNotInstrument @Config(sdk = {Build.VERSION_CODES.O_MR1}) public class ListPurchaseHistoryCallTest { - private final static String PURCHASE_RECORD = - "{ 'productId' = 'id', 'purchaseToken' = 'token' }" - .replace('\'', '"'); private final static DigitalGoodsCallback EMPTY_CALLBACK = (name, args) -> {}; private final MockBillingWrapper mBillingWrapper = new MockBillingWrapper(); @@ -66,47 +54,23 @@ public void goodArgs() { } @Test - public void parsesResult_inApp() throws JSONException, InterruptedException { - PurchaseHistoryRecord record = new PurchaseHistoryRecord(PURCHASE_RECORD, ""); - - call(Collections.singletonList(record), Collections.emptyList()); - } - - @Test - public void parsesResult_subs() throws JSONException, InterruptedException { - PurchaseHistoryRecord record = new PurchaseHistoryRecord(PURCHASE_RECORD, ""); - - call(Collections.emptyList(), Collections.singletonList(record)); - } - - private void call(List inAppPurchaseHistory, - List subsPurchaseHistory) throws InterruptedException { - CountDownLatch callbackTriggered = new CountDownLatch(1); + public void returnsOkAndEmptyListImmediately() { + boolean[] callbackTriggered = new boolean[1]; DigitalGoodsCallback callback = (name, bundle) -> { assertEquals(ListPurchaseHistoryCall.RESPONSE_COMMAND, name); - assertEquals(bundle.getInt(ListPurchaseHistoryCall.KEY_RESPONSE_CODE), - toChromiumResponseCode(BillingClient.BillingResponseCode.OK)); + assertEquals(DigitalGoodsConverter.CHROMIUM_RESULT_OK, + bundle.getInt(ListPurchaseHistoryCall.KEY_RESPONSE_CODE)); Parcelable[] array = bundle .getParcelableArray(ListPurchaseHistoryCall.KEY_PURCHASE_HISTORY_LIST); - PurchaseDetails details = PurchaseDetails.create((Bundle) array[0]); - - assertEquals("id", details.id); - assertEquals("token", details.purchaseToken); + assertEquals(0, array.length); - callbackTriggered.countDown(); + callbackTriggered[0] = true; }; assertTrue(mHandler.handle(ListPurchaseHistoryCall.COMMAND_NAME, new Bundle(), callback)); - mBillingWrapper.triggerConnected(); - - assertTrue(mBillingWrapper.waitForQueryPurchaseHistory()); - mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.ProductType.INAPP, - inAppPurchaseHistory); - mBillingWrapper.triggerOnPurchaseHistoryResponse(BillingClient.ProductType.SUBS, - subsPurchaseHistory); - - assertTrue(callbackTriggered.await(5, TimeUnit.SECONDS)); + assertTrue(callbackTriggered[0]); } } + From 162380a80a41045a6677e3afabd636073dd27591 Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:37:47 +0000 Subject: [PATCH 10/24] feat(pblv8): upgrade play billing library to v8.3.0 and fix listener compatibility (FR-01) --- gradle/libs.versions.toml | 2 +- .../digitalgoods/ConnectedBillingWrapper.java | 3 +-- .../playbilling/provider/BillingWrapper.java | 8 +++++++- .../playbilling/provider/MockBillingWrapper.java | 5 ++--- .../playbilling/provider/PlayBillingWrapper.java | 12 +++++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20b8075a..eb815c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidx-test-core = "1.4.0" androidx-ext-junit = "1.1.1" androidx-test-rules = "1.2.0" androidx-test-runner = "1.2.0" -billing = "7.1.1" +billing = "8.3.0" material = "1.12.0" firebase-bom = "25.12.0" google-services = "4.3.4" diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java index 2a139f86..97115bee 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java @@ -23,7 +23,6 @@ import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.ProductDetails; -import com.android.billingclient.api.ProductDetailsResponseListener; import com.google.androidbrowserhelper.playbilling.provider.BillingWrapper; import com.google.androidbrowserhelper.playbilling.provider.MethodData; @@ -89,7 +88,7 @@ public void connect(BillingClientStateListener callback) { @Override public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, - ProductDetailsResponseListener callback) { + BillingWrapper.ProductDetailsResponseListener callback) { execute(() -> mInner.queryProductDetails(productType, productIds, callback)); } diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java index 8a485f76..c51421c3 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java @@ -23,7 +23,6 @@ import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.ProductDetails; -import com.android.billingclient.api.ProductDetailsResponseListener; import com.google.androidbrowserhelper.playbilling.digitalgoods.ConnectedBillingWrapper; import java.util.List; @@ -33,6 +32,13 @@ * tests. */ public interface BillingWrapper { + /** + * Callback for product details query. + */ + interface ProductDetailsResponseListener { + void onProductDetailsResponse(BillingResult billingResult, List productDetailsList); + } + /** * Callbacks for connection state and for purchase flow completion. */ diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java index 2058a937..c8d435b6 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java @@ -25,7 +25,6 @@ import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.ProductDetails; -import com.android.billingclient.api.ProductDetailsResponseListener; import java.util.ArrayList; import java.util.Collections; @@ -48,7 +47,7 @@ public class MockBillingWrapper implements BillingWrapper { private InvocationTracker mConsumeInvocation = new InvocationTracker<>(); - private MultiProductTypeInvocationTracker, ProductDetailsResponseListener> + private MultiProductTypeInvocationTracker, BillingWrapper.ProductDetailsResponseListener> mQueryProductDetailsInvocation = new MultiProductTypeInvocationTracker<>(); private MultiProductTypeInvocationTracker mQueryPurchasesInvocation = new MultiProductTypeInvocationTracker<>(); @@ -66,7 +65,7 @@ public void connect(BillingClientStateListener callback) { @Override public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, - ProductDetailsResponseListener callback) { + BillingWrapper.ProductDetailsResponseListener callback) { mQueryProductDetailsInvocation.call(productType, productIds, callback); } diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java index a309fc5c..0e54623f 100644 --- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java +++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java @@ -34,7 +34,7 @@ import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.ProductDetails; -import com.android.billingclient.api.ProductDetailsResponseListener; +import com.android.billingclient.api.QueryProductDetailsResult; import java.util.ArrayList; import java.util.List; @@ -78,7 +78,7 @@ public void connect(BillingClientStateListener callback) { @Override public void queryProductDetails(@BillingClient.ProductType String productType, List productIds, - ProductDetailsResponseListener callback) { + final BillingWrapper.ProductDetailsResponseListener callback) { List productList = new ArrayList<>(); for (String productId : productIds) { productList.add( @@ -92,7 +92,13 @@ public void queryProductDetails(@BillingClient.ProductType String productType, L .setProductList(productList) .build(); - mClient.queryProductDetailsAsync(params, callback); + mClient.queryProductDetailsAsync(params, new com.android.billingclient.api.ProductDetailsResponseListener() { + @Override + public void onProductDetailsResponse(BillingResult billingResult, QueryProductDetailsResult queryProductDetailsResult) { + List productDetailsList = queryProductDetailsResult.getProductDetailsList(); + callback.onProductDetailsResponse(billingResult, productDetailsList != null ? productDetailsList : new ArrayList<>()); + } + }); } @Override From a3638f23537189f82165ff96fb2a60431b03f34c Mon Sep 17 00:00:00 2001 From: Abdelrahman Eed <18529757+SharkMan201@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:23:59 +0000 Subject: [PATCH 11/24] feat(pblv8): bump play billing module version and versionCode (FR-07) --- playbilling/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playbilling/build.gradle b/playbilling/build.gradle index 56de6f17..bea568fe 100644 --- a/playbilling/build.gradle +++ b/playbilling/build.gradle @@ -19,7 +19,7 @@ plugins { id 'maven-publish' } -def VERSION = "1.1.0"; +def VERSION = "1.2.0"; android { namespace "com.google.androidbrowserhelper.playbilling" @@ -28,7 +28,7 @@ android { minSdkVersion 23 compileSdk 36 targetSdkVersion 31 - versionCode 2 + versionCode 3 versionName VERSION testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From c8395ad6ad66dfa5c5ae2b01c67934d4aa009a35 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Thu, 6 Aug 2026 11:05:17 -0700 Subject: [PATCH 12/24] Support USE_HIGH_PRI_NOTIFICATIONS manifest metadata for TWA notification channels --- .../trusted/NotificationUtils.java | 99 ++++++- .../trusted/NotificationUtilsTest.java | 263 ++++++++++++++++++ 2 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java 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 abf3e525..7e5c4282 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java @@ -14,10 +14,17 @@ package com.google.androidbrowserhelper.trusted; +import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; import android.os.Build; +import android.os.Bundle; import androidx.core.app.NotificationManagerCompat; import java.util.Locale; @@ -25,6 +32,18 @@ * Helper for interacting with the notification manager and channels. */ public class NotificationUtils { + /** + * Metadata key to specify whether notification channels should be created with high importance. + */ + public static final String METADATA_USE_HIGH_PRI_NOTIFICATIONS = + "android.support.customtabs.trusted.USE_HIGH_PRI_NOTIFICATIONS"; + + /** + * AndroidX alias for {@link #METADATA_USE_HIGH_PRI_NOTIFICATIONS}. + */ + public static final String METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX = + "androidx.browser.trusted.USE_HIGH_PRI_NOTIFICATIONS"; + private NotificationUtils() {} /** @@ -41,13 +60,87 @@ public static boolean areNotificationsEnabled(Context context, String channelNam } /** - * Creates a notification channel using the given channel name. + * Checks if high-priority notifications are configured in the manifest metadata. + */ + public static boolean shouldUseHighPriorityNotifications(Context context) { + if (context == null) return false; + try { + Bundle metaData = context.getPackageManager().getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA).metaData; + if (metaData != null && isHighPriorityInBundle(metaData)) { + return true; + } + + if (context instanceof Activity) { + Activity activity = (Activity) context; + ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo( + activity.getComponentName(), PackageManager.GET_META_DATA); + if (activityInfo != null && activityInfo.metaData != null + && isHighPriorityInBundle(activityInfo.metaData)) { + return true; + } + } else if (context instanceof Service) { + Service service = (Service) context; + ServiceInfo serviceInfo = service.getPackageManager().getServiceInfo( + new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA); + if (serviceInfo != null && serviceInfo.metaData != null + && isHighPriorityInBundle(serviceInfo.metaData)) { + return true; + } + } + } catch (PackageManager.NameNotFoundException e) { + // Package or component not found; fallback to default. + } + return false; + } + + private static boolean isHighPriorityInBundle(Bundle metaData) { + if (metaData.containsKey(METADATA_USE_HIGH_PRI_NOTIFICATIONS)) { + Object val = metaData.get(METADATA_USE_HIGH_PRI_NOTIFICATIONS); + if (parseBooleanValue(val)) return true; + } + if (metaData.containsKey(METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX)) { + Object val = metaData.get(METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX); + if (parseBooleanValue(val)) return true; + } + return false; + } + + private static boolean parseBooleanValue(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + return false; + } + + /** + * Returns the notification importance level resolved from the application manifest metadata. + */ + public static int getNotificationImportance(Context context) { + return shouldUseHighPriorityNotifications(context) + ? NotificationManager.IMPORTANCE_HIGH + : NotificationManager.IMPORTANCE_DEFAULT; + } + + /** + * Creates a notification channel using the given channel name and the importance level resolved + * from manifest metadata. */ public static void createNotificationChannel(Context context, String channelName) { + createNotificationChannel(context, channelName, getNotificationImportance(context)); + } + + /** + * Creates a notification channel using the given channel name and explicit importance level. + */ + public static void createNotificationChannel(Context context, String channelName, int importance) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; NotificationChannel channel = new NotificationChannel(channelNameToId(channelName), - channelName, NotificationManager.IMPORTANCE_DEFAULT); + channelName, importance); NotificationManagerCompat.from(context).createNotificationChannel(channel); } @@ -55,7 +148,7 @@ public static void createNotificationChannel(Context context, String channelName * Generates a notification channel id from a channel name. * TODO: Remove this when we can use the method defined in AndroidX instead. */ - private static String channelNameToId(String name) { + static String channelNameToId(String name) { return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id"; } } diff --git a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java new file mode 100644 index 00000000..8c8b63c9 --- /dev/null +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java @@ -0,0 +1,263 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// 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 +// +// http://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.trusted; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationManagerCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.internal.DoNotInstrument; +import org.robolectric.shadows.ShadowNotificationManager; +import org.robolectric.shadows.ShadowPackageManager; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Parameterized tests for {@link NotificationUtils}. + */ +@RunWith(ParameterizedRobolectricTestRunner.class) +@DoNotInstrument +@Config(sdk = {Build.VERSION_CODES.O_MR1}) +public class NotificationUtilsTest { + @Parameter(0) + public String mTestName; + + @Parameter(1) + public String mMetadataKey; + + @Parameter(2) + public Object mMetadataValue; + + @Parameter(3) + public int mExpectedImportance; + + @Parameter(4) + public boolean mExpectedShouldUseHighPriority; + + private Context mContext; + private PackageManager mPackageManager; + private ShadowPackageManager mShadowPackageManager; + private NotificationManager mNotificationManager; + private ShadowNotificationManager mShadowNotificationManager; + + private static final String CHANNEL_NAME = "General Notifications"; + private static final String EXPECTED_CHANNEL_ID = "general_notifications_channel_id"; + + @Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + { + "default_unspecified", + null, + null, + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "customtabs_boolean_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + true, + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "customtabs_string_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "true", + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "customtabs_string_true_uppercase", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "TRUE", + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "androidx_boolean_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + true, + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "androidx_string_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + "true", + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "customtabs_boolean_false", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + false, + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "customtabs_string_false", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "false", + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "customtabs_string_invalid", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "invalid_string", + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + }); + } + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mPackageManager = mContext.getPackageManager(); + mShadowPackageManager = shadowOf(mPackageManager); + mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mShadowNotificationManager = shadowOf(mNotificationManager); + + if (mMetadataKey != null && mMetadataValue != null) { + setAppMetadata(mMetadataKey, mMetadataValue); + } + } + + @Test + public void channelNameToId_replacesSpacesAndAppendsSuffix() { + assertEquals("general_notifications_channel_id", + NotificationUtils.channelNameToId("General Notifications")); + assertEquals("chat_channel_id", + NotificationUtils.channelNameToId("Chat")); + assertEquals("test_channel_name_channel_id", + NotificationUtils.channelNameToId("Test Channel Name")); + } + + @Test + public void getNotificationImportance_matchesExpectedImportance() { + assertEquals(mExpectedImportance, NotificationUtils.getNotificationImportance(mContext)); + } + + @Test + public void shouldUseHighPriorityNotifications_matchesExpectedBoolean() { + assertEquals(mExpectedShouldUseHighPriority, + NotificationUtils.shouldUseHighPriorityNotifications(mContext)); + } + + @Test + public void createNotificationChannel_resolvesImportanceCorrectly() { + NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME); + + NotificationChannel channel = mNotificationManager.getNotificationChannel(EXPECTED_CHANNEL_ID); + assertNotNull(channel); + assertEquals(CHANNEL_NAME, channel.getName().toString()); + assertEquals(mExpectedImportance, channel.getImportance()); + } + + @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()); + } + + @Test + public void areNotificationsEnabled_returnsTrueForNewAndEnabledChannels() { + assertTrue(NotificationUtils.areNotificationsEnabled(mContext, CHANNEL_NAME)); + + NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME); + assertTrue(NotificationUtils.areNotificationsEnabled(mContext, CHANNEL_NAME)); + } + + @Test + public void areNotificationsEnabled_returnsFalseForBlockedChannel() { + NotificationChannel channel = new NotificationChannel( + EXPECTED_CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE); + mNotificationManager.createNotificationChannel(channel); + + assertFalse(NotificationUtils.areNotificationsEnabled(mContext, CHANNEL_NAME)); + } + + @Test + public void areNotificationsEnabled_returnsFalseWhenNotificationsDisabledGlobally() { + mShadowNotificationManager.setNotificationsEnabled(false); + assertFalse(NotificationUtils.areNotificationsEnabled(mContext, CHANNEL_NAME)); + } + + @Test + public void getNotificationImportance_withNullContext() { + assertEquals(NotificationManager.IMPORTANCE_DEFAULT, + NotificationUtils.getNotificationImportance(null)); + assertFalse(NotificationUtils.shouldUseHighPriorityNotifications(null)); + } + + private void setAppMetadata(String key, Object value) { + PackageInfo packageInfo = mShadowPackageManager.getInternalMutablePackageInfo(mContext.getPackageName()); + if (packageInfo == null) { + packageInfo = new PackageInfo(); + packageInfo.packageName = mContext.getPackageName(); + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.packageName = mContext.getPackageName(); + mShadowPackageManager.addPackage(packageInfo); + } + if (packageInfo.applicationInfo.metaData == null) { + packageInfo.applicationInfo.metaData = new Bundle(); + } + if (value instanceof Boolean) { + packageInfo.applicationInfo.metaData.putBoolean(key, (Boolean) value); + } else if (value instanceof String) { + packageInfo.applicationInfo.metaData.putString(key, (String) value); + } else if (value instanceof Integer) { + packageInfo.applicationInfo.metaData.putInt(key, (Integer) value); + } + } +} From eb18d51bb61c3282a857ce41d9a0332389e678a7 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Fri, 7 Aug 2026 09:09:04 -0700 Subject: [PATCH 13/24] Resolve comments --- .../trusted/NotificationUtils.java | 51 ++++-------- .../trusted/NotificationUtilsTest.java | 81 +++++++++---------- 2 files changed, 54 insertions(+), 78 deletions(-) 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 7e5c4282..cce47484 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java @@ -14,13 +14,11 @@ package com.google.androidbrowserhelper.trusted; -import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.ComponentName; import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.os.Build; @@ -35,12 +33,6 @@ public class NotificationUtils { /** * Metadata key to specify whether notification channels should be created with high importance. */ - public static final String METADATA_USE_HIGH_PRI_NOTIFICATIONS = - "android.support.customtabs.trusted.USE_HIGH_PRI_NOTIFICATIONS"; - - /** - * AndroidX alias for {@link #METADATA_USE_HIGH_PRI_NOTIFICATIONS}. - */ public static final String METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX = "androidx.browser.trusted.USE_HIGH_PRI_NOTIFICATIONS"; @@ -63,42 +55,26 @@ public static boolean areNotificationsEnabled(Context context, String channelNam * Checks if high-priority notifications are configured in the manifest metadata. */ public static boolean shouldUseHighPriorityNotifications(Context context) { - if (context == null) return false; + if (!(context instanceof Service)) return false; try { - Bundle metaData = context.getPackageManager().getApplicationInfo( - context.getPackageName(), PackageManager.GET_META_DATA).metaData; - if (metaData != null && isHighPriorityInBundle(metaData)) { + Service service = (Service) context; + ServiceInfo serviceInfo = service.getPackageManager().getServiceInfo( + new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA); + if (serviceInfo != null && serviceInfo.metaData != null + && isHighPriorityInBundle(serviceInfo.metaData)) { return true; } - - if (context instanceof Activity) { - Activity activity = (Activity) context; - ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo( - activity.getComponentName(), PackageManager.GET_META_DATA); - if (activityInfo != null && activityInfo.metaData != null - && isHighPriorityInBundle(activityInfo.metaData)) { - return true; - } - } else if (context instanceof Service) { - Service service = (Service) context; - ServiceInfo serviceInfo = service.getPackageManager().getServiceInfo( - new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA); - if (serviceInfo != null && serviceInfo.metaData != null - && isHighPriorityInBundle(serviceInfo.metaData)) { - return true; - } - } } catch (PackageManager.NameNotFoundException e) { - // Package or component not found; fallback to default. + // Service not found; fallback to default. } return false; } + /** + * Checks if the given metadata bundle contains the high-priority notification configuration + * and evaluates to true. + */ private static boolean isHighPriorityInBundle(Bundle metaData) { - if (metaData.containsKey(METADATA_USE_HIGH_PRI_NOTIFICATIONS)) { - Object val = metaData.get(METADATA_USE_HIGH_PRI_NOTIFICATIONS); - if (parseBooleanValue(val)) return true; - } if (metaData.containsKey(METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX)) { Object val = metaData.get(METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX); if (parseBooleanValue(val)) return true; @@ -106,6 +82,9 @@ private static boolean isHighPriorityInBundle(Bundle metaData) { return false; } + /** + * Parses an object value from metadata into a boolean, handling both Boolean and String representations. + */ private static boolean parseBooleanValue(Object value) { if (value instanceof Boolean) { return (Boolean) value; @@ -148,7 +127,7 @@ public static void createNotificationChannel(Context context, String channelName * Generates a notification channel id from a channel name. * TODO: Remove this when we can use the method defined in AndroidX instead. */ - static String channelNameToId(String name) { + public static String channelNameToId(String name) { return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id"; } } 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 8c8b63c9..d99ddc32 100644 --- a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java @@ -98,57 +98,43 @@ public static Collection data() { false }, { - "customtabs_boolean_true", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "androidx_boolean_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, true, NotificationManager.IMPORTANCE_HIGH, true }, { - "customtabs_string_true", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "androidx_string_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, "true", NotificationManager.IMPORTANCE_HIGH, true }, { - "customtabs_string_true_uppercase", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, - "TRUE", - NotificationManager.IMPORTANCE_HIGH, - true - }, - { - "androidx_boolean_true", + "androidx_string_true_uppercase", NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - true, + "TRUE", NotificationManager.IMPORTANCE_HIGH, true }, { - "androidx_string_true", + "androidx_boolean_false", NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - "true", - NotificationManager.IMPORTANCE_HIGH, - true - }, - { - "customtabs_boolean_false", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, false, NotificationManager.IMPORTANCE_DEFAULT, false }, { - "customtabs_string_false", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "androidx_string_false", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, "false", NotificationManager.IMPORTANCE_DEFAULT, false }, { - "customtabs_string_invalid", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS, + "androidx_string_invalid", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, "invalid_string", NotificationManager.IMPORTANCE_DEFAULT, false @@ -156,16 +142,24 @@ public static Collection data() { }); } + public static class TestService extends Service { + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + } + @Before public void setUp() { - mContext = RuntimeEnvironment.application; + mContext = Robolectric.setupService(TestService.class); mPackageManager = mContext.getPackageManager(); mShadowPackageManager = shadowOf(mPackageManager); mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mShadowNotificationManager = shadowOf(mNotificationManager); if (mMetadataKey != null && mMetadataValue != null) { - setAppMetadata(mMetadataKey, mMetadataValue); + setServiceMetadata(mMetadataKey, mMetadataValue); } } @@ -240,24 +234,27 @@ public void getNotificationImportance_withNullContext() { assertFalse(NotificationUtils.shouldUseHighPriorityNotifications(null)); } - private void setAppMetadata(String key, Object value) { - PackageInfo packageInfo = mShadowPackageManager.getInternalMutablePackageInfo(mContext.getPackageName()); - if (packageInfo == null) { - packageInfo = new PackageInfo(); - packageInfo.packageName = mContext.getPackageName(); - packageInfo.applicationInfo = new ApplicationInfo(); - packageInfo.applicationInfo.packageName = mContext.getPackageName(); - mShadowPackageManager.addPackage(packageInfo); - } - if (packageInfo.applicationInfo.metaData == null) { - packageInfo.applicationInfo.metaData = new Bundle(); - } + @Test + public void getNotificationImportance_withNonServiceContext() { + Context appContext = RuntimeEnvironment.application; + assertEquals(NotificationManager.IMPORTANCE_DEFAULT, + NotificationUtils.getNotificationImportance(appContext)); + assertFalse(NotificationUtils.shouldUseHighPriorityNotifications(appContext)); + } + + private void setServiceMetadata(String key, Object value) { + ComponentName componentName = new ComponentName(mContext, mContext.getClass()); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = mContext.getPackageName(); + serviceInfo.name = componentName.getClassName(); + serviceInfo.metaData = new Bundle(); if (value instanceof Boolean) { - packageInfo.applicationInfo.metaData.putBoolean(key, (Boolean) value); + serviceInfo.metaData.putBoolean(key, (Boolean) value); } else if (value instanceof String) { - packageInfo.applicationInfo.metaData.putString(key, (String) value); + serviceInfo.metaData.putString(key, (String) value); } else if (value instanceof Integer) { - packageInfo.applicationInfo.metaData.putInt(key, (Integer) value); + serviceInfo.metaData.putInt(key, (Integer) value); } + mShadowPackageManager.addOrUpdateService(serviceInfo); } } From acc2490d666f55604cde578e4507a5cd85c42a87 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Tue, 11 Aug 2026 10:44:56 -0700 Subject: [PATCH 14/24] Address comments --- .../androidbrowserhelper/trusted/NotificationUtils.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 cce47484..dc115d99 100644 --- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java +++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/NotificationUtils.java @@ -23,6 +23,7 @@ import android.content.pm.ServiceInfo; import android.os.Build; import android.os.Bundle; +import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationManagerCompat; import java.util.Locale; @@ -54,7 +55,8 @@ public static boolean areNotificationsEnabled(Context context, String channelNam /** * Checks if high-priority notifications are configured in the manifest metadata. */ - public static boolean shouldUseHighPriorityNotifications(Context context) { + @VisibleForTesting + static boolean shouldUseHighPriorityNotifications(Context context) { if (!(context instanceof Service)) return false; try { Service service = (Service) context; @@ -127,7 +129,8 @@ public static void createNotificationChannel(Context context, String channelName * Generates a notification channel id from a channel name. * TODO: Remove this when we can use the method defined in AndroidX instead. */ - public static String channelNameToId(String name) { + @VisibleForTesting + static String channelNameToId(String name) { return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id"; } } From 795fd34cac4c288b88658329ba2d2cf3d2643342 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Tue, 11 Aug 2026 20:00:15 -0700 Subject: [PATCH 15/24] Tests --- .../NotificationUtilsParameterizedTest.java | 187 ++++++++++++++++++ .../trusted/NotificationUtilsTest.java | 151 +------------- 2 files changed, 190 insertions(+), 148 deletions(-) create mode 100644 androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java diff --git a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java new file mode 100644 index 00000000..2da76f43 --- /dev/null +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsParameterizedTest.java @@ -0,0 +1,187 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// 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 +// +// http://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.trusted; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.robolectric.Shadows.shadowOf; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; + +import androidx.annotation.Nullable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.internal.DoNotInstrument; +import org.robolectric.shadows.ShadowPackageManager; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Parameterized tests for {@link NotificationUtils}. + */ +@RunWith(ParameterizedRobolectricTestRunner.class) +@DoNotInstrument +@Config(sdk = {Build.VERSION_CODES.O_MR1}) +public class NotificationUtilsParameterizedTest { + @Parameter(0) + public String mTestName; + + @Parameter(1) + public String mMetadataKey; + + @Parameter(2) + public Object mMetadataValue; + + @Parameter(3) + public int mExpectedImportance; + + @Parameter(4) + public boolean mExpectedShouldUseHighPriority; + + private Context mContext; + private ShadowPackageManager mShadowPackageManager; + private NotificationManager mNotificationManager; + + private static final String CHANNEL_NAME = "General Notifications"; + private static final String EXPECTED_CHANNEL_ID = "general_notifications_channel_id"; + + @Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + { + "default_unspecified", + null, + null, + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "androidx_boolean_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + true, + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "androidx_string_true", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + "true", + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "androidx_string_true_uppercase", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + "TRUE", + NotificationManager.IMPORTANCE_HIGH, + true + }, + { + "androidx_boolean_false", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + false, + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "androidx_string_false", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + "false", + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + { + "androidx_string_invalid", + NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, + "invalid_string", + NotificationManager.IMPORTANCE_DEFAULT, + false + }, + }); + } + + public static class TestService extends Service { + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + } + + @Before + public void setUp() { + mContext = Robolectric.setupService(TestService.class); + mShadowPackageManager = shadowOf(mContext.getPackageManager()); + mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + if (mMetadataKey != null && mMetadataValue != null) { + setServiceMetadata(mMetadataKey, mMetadataValue); + } + } + + @Test + public void getNotificationImportance_matchesExpectedImportance() { + assertEquals(mExpectedImportance, NotificationUtils.getNotificationImportance(mContext)); + } + + @Test + public void shouldUseHighPriorityNotifications_matchesExpectedBoolean() { + assertEquals(mExpectedShouldUseHighPriority, + NotificationUtils.shouldUseHighPriorityNotifications(mContext)); + } + + @Test + public void createNotificationChannel_resolvesImportanceCorrectly() { + NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME); + + NotificationChannel channel = mNotificationManager.getNotificationChannel(EXPECTED_CHANNEL_ID); + assertNotNull(channel); + assertEquals(CHANNEL_NAME, channel.getName().toString()); + assertEquals(mExpectedImportance, channel.getImportance()); + } + + private void setServiceMetadata(String key, Object value) { + ComponentName componentName = new ComponentName(mContext, mContext.getClass()); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = mContext.getPackageName(); + serviceInfo.name = componentName.getClassName(); + serviceInfo.metaData = new Bundle(); + if (value instanceof Boolean) { + serviceInfo.metaData.putBoolean(key, (Boolean) value); + } else if (value instanceof String) { + serviceInfo.metaData.putString(key, (String) value); + } else if (value instanceof Integer) { + serviceInfo.metaData.putInt(key, (Integer) value); + } + mShadowPackageManager.addOrUpdateService(serviceInfo); + } +} 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 d99ddc32..52ea88ed 100644 --- a/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java +++ b/androidbrowserhelper/src/test/java/com/google/androidbrowserhelper/trusted/NotificationUtilsTest.java @@ -20,147 +20,39 @@ import static org.junit.Assert.assertTrue; import static org.robolectric.Shadows.shadowOf; -import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.Service; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; - -import androidx.annotation.Nullable; -import androidx.core.app.NotificationManagerCompat; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.ParameterizedRobolectricTestRunner; -import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; -import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; -import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.internal.DoNotInstrument; import org.robolectric.shadows.ShadowNotificationManager; -import org.robolectric.shadows.ShadowPackageManager; - -import java.util.Arrays; -import java.util.Collection; /** - * Parameterized tests for {@link NotificationUtils}. + * Tests for {@link NotificationUtils}. */ -@RunWith(ParameterizedRobolectricTestRunner.class) +@RunWith(RobolectricTestRunner.class) @DoNotInstrument @Config(sdk = {Build.VERSION_CODES.O_MR1}) public class NotificationUtilsTest { - @Parameter(0) - public String mTestName; - - @Parameter(1) - public String mMetadataKey; - - @Parameter(2) - public Object mMetadataValue; - - @Parameter(3) - public int mExpectedImportance; - - @Parameter(4) - public boolean mExpectedShouldUseHighPriority; - private Context mContext; - private PackageManager mPackageManager; - private ShadowPackageManager mShadowPackageManager; private NotificationManager mNotificationManager; private ShadowNotificationManager mShadowNotificationManager; private static final String CHANNEL_NAME = "General Notifications"; private static final String EXPECTED_CHANNEL_ID = "general_notifications_channel_id"; - @Parameters(name = "{0}") - public static Collection data() { - return Arrays.asList(new Object[][] { - { - "default_unspecified", - null, - null, - NotificationManager.IMPORTANCE_DEFAULT, - false - }, - { - "androidx_boolean_true", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - true, - NotificationManager.IMPORTANCE_HIGH, - true - }, - { - "androidx_string_true", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - "true", - NotificationManager.IMPORTANCE_HIGH, - true - }, - { - "androidx_string_true_uppercase", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - "TRUE", - NotificationManager.IMPORTANCE_HIGH, - true - }, - { - "androidx_boolean_false", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - false, - NotificationManager.IMPORTANCE_DEFAULT, - false - }, - { - "androidx_string_false", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - "false", - NotificationManager.IMPORTANCE_DEFAULT, - false - }, - { - "androidx_string_invalid", - NotificationUtils.METADATA_USE_HIGH_PRI_NOTIFICATIONS_ANDROIDX, - "invalid_string", - NotificationManager.IMPORTANCE_DEFAULT, - false - }, - }); - } - - public static class TestService extends Service { - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - } - @Before public void setUp() { - mContext = Robolectric.setupService(TestService.class); - mPackageManager = mContext.getPackageManager(); - mShadowPackageManager = shadowOf(mPackageManager); + mContext = RuntimeEnvironment.application; mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mShadowNotificationManager = shadowOf(mNotificationManager); - - if (mMetadataKey != null && mMetadataValue != null) { - setServiceMetadata(mMetadataKey, mMetadataValue); - } } @Test @@ -173,27 +65,6 @@ public void channelNameToId_replacesSpacesAndAppendsSuffix() { NotificationUtils.channelNameToId("Test Channel Name")); } - @Test - public void getNotificationImportance_matchesExpectedImportance() { - assertEquals(mExpectedImportance, NotificationUtils.getNotificationImportance(mContext)); - } - - @Test - public void shouldUseHighPriorityNotifications_matchesExpectedBoolean() { - assertEquals(mExpectedShouldUseHighPriority, - NotificationUtils.shouldUseHighPriorityNotifications(mContext)); - } - - @Test - public void createNotificationChannel_resolvesImportanceCorrectly() { - NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME); - - NotificationChannel channel = mNotificationManager.getNotificationChannel(EXPECTED_CHANNEL_ID); - assertNotNull(channel); - assertEquals(CHANNEL_NAME, channel.getName().toString()); - assertEquals(mExpectedImportance, channel.getImportance()); - } - @Test public void createNotificationChannel_explicitImportanceOverridesMetadata() { NotificationUtils.createNotificationChannel(mContext, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); @@ -241,20 +112,4 @@ public void getNotificationImportance_withNonServiceContext() { NotificationUtils.getNotificationImportance(appContext)); assertFalse(NotificationUtils.shouldUseHighPriorityNotifications(appContext)); } - - private void setServiceMetadata(String key, Object value) { - ComponentName componentName = new ComponentName(mContext, mContext.getClass()); - ServiceInfo serviceInfo = new ServiceInfo(); - serviceInfo.packageName = mContext.getPackageName(); - serviceInfo.name = componentName.getClassName(); - serviceInfo.metaData = new Bundle(); - if (value instanceof Boolean) { - serviceInfo.metaData.putBoolean(key, (Boolean) value); - } else if (value instanceof String) { - serviceInfo.metaData.putString(key, (String) value); - } else if (value instanceof Integer) { - serviceInfo.metaData.putInt(key, (Integer) value); - } - mShadowPackageManager.addOrUpdateService(serviceInfo); - } } From 6a6ece4e87fedbb4c01986fc11bed6309f579bff Mon Sep 17 00:00:00 2001 From: David Tonderski Date: Wed, 12 Aug 2026 16:52:39 +0000 Subject: [PATCH 16/24] androidbrowserhelper version bump to 2.7.3 --- README.md | 2 +- androidbrowserhelper/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9caaf18..4683cf1f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Android Browser helper is available on the Google Maven. To use it, modify your ```gradle dependencies { //... - implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.7.2' + implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.7.3' } ``` diff --git a/androidbrowserhelper/build.gradle b/androidbrowserhelper/build.gradle index c56b9429..5c3a3692 100644 --- a/androidbrowserhelper/build.gradle +++ b/androidbrowserhelper/build.gradle @@ -22,8 +22,8 @@ plugins { // Before 2.6.1, the version code was unused and was kept at 1. // This is now being used to report metrics, and should be bumped with // every version bump. -def VERSION = "2.7.2"; -def VERSION_CODE = 8; +def VERSION = "2.7.3"; +def VERSION_CODE = 9; android { namespace "com.google.androidbrowserhelper" From 2bc6a53c1437546b60be112d1f8de0cbddd6b4aa Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Thu, 13 Aug 2026 10:02:02 -0700 Subject: [PATCH 17/24] Migrate notification channels --- .../trusted/NotificationUtils.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) 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"; } } From 7613b8460c6e8c9e8b7c705f397bbbb6846f56e8 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal Date: Thu, 13 Aug 2026 10:02:02 -0700 Subject: [PATCH 18/24] Migrate notification channels --- .../NotificationUtilsParameterizedTest.java | 14 +++++++++++++- .../trusted/NotificationUtilsTest.java | 16 +++------------- 2 files changed, 16 insertions(+), 14 deletions(-) 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 From 2d58139333d09d204327f9e2ce7005f5ee95bec2 Mon Sep 17 00:00:00 2001 From: sashalukin Date: Thu, 13 Aug 2026 16:08:23 -0400 Subject: [PATCH 19/24] docs: Add Play Billing Library 8 migration guide - Created a new migration guide detailing the update to PBL 8 - Updated the playbilling README to warn developers and remove deprecated purchase history functionality --- playbilling/README.md | 9 +- .../docs/play-billing-library-8-migration.md | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 playbilling/docs/play-billing-library-8-migration.md diff --git a/playbilling/README.md b/playbilling/README.md index 24520421..d6d453ce 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) 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..57e18a46 --- /dev/null +++ b/playbilling/docs/play-billing-library-8-migration.md @@ -0,0 +1,84 @@ +# 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@latest + ``` +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 successfully 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). PBL 8 removed the legacy `querySkuDetailsAsync()` API that Android Browser Helper previously used to view subscriptions as a flat list. ABH now uses the newer `ProductDetails` API. + +Because the DGAPI specification still expects a flat structure (`ItemDetails`), Android Browser Helper now performs a "lossy conversion" for subscriptions: +- It uses the base-plan ID associated with the first eligible subscription offer returned by Play. +- It prefers an offer within that base plan when one exists. +- It flattens that specific offer's pricing phases into DGAPI's legacy `ItemDetails` fields (`price`, `subscriptionPeriod`, `freeTrialPeriod`, etc.). + +**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. + +## Additional resources + +* [Google Play Billing Library 8 release notes](https://developer.android.com/google/play/billing/release-notes#8-0) +* [Digital Goods API specification](https://github.com/WICG/digital-goods) From e2229706047fbead2adb7291e59f7dc37781f0c3 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal <93222189+Dp-Goog@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:57:59 -0700 Subject: [PATCH 20/24] Add TWA high priority notifications test site (#608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a sample test TWA that wraps https://dp-goog.github.io/notifications-api-testing/, so that SW notifications can be sent. This mostly uses the artifacts used in `twa-notification-delegation`, with the code changes being: - The new `androidx.browser.trusted.USE_HIGH_PRI_NOTIFICATIONS` field set. - The TWA launches `https://dp-goog.github.io/notifications-api-testing/` instead. - `onCreate()` and `onDestroy()` logs for notifications Verified that it works, based on the following logs: Screenshot 2026-08-06 at 4 37 27 PM --- .../twa-notification-high-priority/README.md | 8 + .../build.gradle | 32 +++ .../src/main/AndroidManifest.xml | 117 +++++++++++ .../NotificationDelegationService.java | 54 ++++++ .../drawable-anydpi/ic_notification_icon.xml | 25 +++ .../drawable-hdpi/ic_notification_icon.png | Bin 0 -> 307 bytes .../drawable-mdpi/ic_notification_icon.png | Bin 0 -> 237 bytes .../drawable-v24/ic_launcher_foreground.xml | 42 ++++ .../drawable-xhdpi/ic_notification_icon.png | Bin 0 -> 373 bytes .../src/main/res/drawable-xhdpi/splash.png | Bin 0 -> 1884 bytes .../drawable-xxhdpi/ic_notification_icon.png | Bin 0 -> 511 bytes .../res/drawable/ic_launcher_background.xml | 182 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 17 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../src/main/res/values/colors.xml | 21 ++ .../src/main/res/values/strings.xml | 24 +++ .../src/main/res/xml/filepaths.xml | 16 ++ settings.gradle | 1 + 28 files changed, 544 insertions(+) create mode 100644 demos/twa-notification-high-priority/README.md create mode 100644 demos/twa-notification-high-priority/build.gradle create mode 100644 demos/twa-notification-high-priority/src/main/AndroidManifest.xml create mode 100644 demos/twa-notification-high-priority/src/main/java/com/google/androidbrowserhelper/demos/twa_notification_high_priority/NotificationDelegationService.java create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-anydpi/ic_notification_icon.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-hdpi/ic_notification_icon.png create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-mdpi/ic_notification_icon.png create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/ic_notification_icon.png create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-xhdpi/splash.png create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable-xxhdpi/ic_notification_icon.png create mode 100644 demos/twa-notification-high-priority/src/main/res/drawable/ic_launcher_background.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/twa-notification-high-priority/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 demos/twa-notification-high-priority/src/main/res/values/colors.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/values/strings.xml create mode 100644 demos/twa-notification-high-priority/src/main/res/xml/filepaths.xml 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..5e2bca2e --- /dev/null +++ b/demos/twa-notification-high-priority/src/main/java/com/google/androidbrowserhelper/demos/twa_notification_high_priority/NotificationDelegationService.java @@ -0,0 +1,54 @@ +// 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 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 + 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 true; + } +} 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 0000000000000000000000000000000000000000..c44655cefb50404f974cd64ec37eba746ad8eb7d GIT binary patch literal 307 zcmV-30nGl1P)mnFakBKFVu5 zStb&R{$18u1+j;?+2!PNAKbe*Ks%dG6$j{ugC7uP0SJS+of;0%2?uu&8;Ck0OpQ4^ zd~P{F=K+Ycl@Ql2K-}Dt42LlmSvN3-`HAU)2>NuC08djq{os2MPt@j>1|b4%`y zhV6{$4fZLDkN?3&j_&SPwkx2CGdIHZg_Qq$;v3vjk002ovPDHLk FV1nlPc+da< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..241bdb8edd5ffe0217d873b8a81308f48bd01b92 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjTRdGHLn>}1CrGe1bN%`Mf4+D} zbA!b^AD&C6yH6L0DeU_*zo>wq8YqZH3NJ!rcO*{)J#awF_8Nt_9Eya(;8P+?JWxy j`6jx5epDsG1N6eZbyFJ7?M`q6x}U+*)z4*}Q$iB}g#uOT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ba11f0182a6519fac68951ed044cfef9d5a24e4f GIT binary patch literal 373 zcmV-*0gC>KP)0o)E^UnU79J?}gi# z`P?(kGxDMcf*=TjepNzvk@sKrj`vPY!;i@q;a@UCp&PzSK8g0|OipwFhN6AS zbpR&HaMJ~toYSwvGV@?2U z0@xmaQvls7;Nl8u0n8i};l0S;J#VUy0lYimrbX%%;N>0Hs1fe~wMB05E9D)aTI8nh z2`hkk3;&G*0002c09XJ30DlgUhu&66xS14YV~Zh*U|Wk7h&0&dE(Adk1cjg*EXpi6 TJcccy00000NkvXXu0mjf{0E%! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8de342115dc87051190cd3c269dc473597ab4166 GIT binary patch literal 1884 zcmc(gdrVVz6vxlp>`Dv5T2?Dcge{1VB7{1jU}0hj0u>xXEeeK9WOEN=Fa{_E71|57 z2t^(R1{5b!w%W~GOabK;uCK9HTok9gH(k&<9!h zdrt0EZjTCcV7M>i`{|-(;zQkB>?SExM*{1`Wxeei@$n5%n9%EoATxJ`9rq1 zK9TS*`JT(2X5<=D!d3p4Y&)7`YRV2c93z}KWQfF#r@)C1C3Gg1O$8QV?mWd}6sI#$ z+=+wY?!4Dt*!9hSAab@Xi@ejZ#CG}ZH5+wU)_Lc-I|MD8HV?Ewgs(Q||99)vEc1if z;JE-3{1;Qz)$_drBcmfNGjRbPhO(!>m}k}n;|_s|uW8G=boIUcrSGK9)`@kdNA|uZ zp=l?d+~~EBHJ(rvxx&Zfe{n-+>2C7g5B)^II?VX8cd*Wx&1ro;vu|*FK}`ck+n^!* zjR&8^eJ*uwOdAhnVb6LLn+g#Q0Z95ARDsaG>T!AAZ}LO~X=B2WlM}SsHx$+Go)c&gksgv07Y;#Z|mmJyvQbXXn?I z@8MYjjq%+t_Pl5K=(mUhooOZS^%T(9G<68mI-(PLqARoq7@Sc&8QUun^0CK- zSMaRaXl^ySb%0F?Qi)j#YW;x>LCtP5>A~E{$Cj3C=69UY8p$eqGHH>gJxO)c8hu?D zAwWvDHTRR8;=4V0yQZYhH-`|un5ziwB4>QY=bL$GiwNpuB%p$T_Tn3LMOIly_&my! z@gJ3x9^$#eIp!h`RuNQ~X&pB5EKDZy?%AWUrr=}`1C41_%L$(4<&ORHiha&v z)b-GMOZmZYf204)Bahe8&1IQnqtB$PncPI`Q=X{owoD`1$vJwI;m>u`xn2LjCX>g~ z40>FO7GviPq1@~GL(&>dH?6pf;?U%Q*R40G&Q658Yk%impq4|k$# YL1y)Fxpu z8{cE!_gv-COi=0biPTeqa*CcNr9R^bDs4BHdQU)1(Zjr^zqo?@wj-%asci%?TmRdm zUwShWbkkH&Woobv9KzE-dI~D&#~82qUWgF?4&gn0?8`_~K^hIJg>cIgWG?7Gm$+7l z*M@M*A+93mRwE=8Lfm5iK?PZyVe2b2iXhF5u+F%sf(|{v5dV_V1+^I>7DLoj&|Qd` zZYgA~LOyvxjYHgcnvlGpMi~?6X+hG0_J(+%Tb1Mm)rNS$n_U$__kkZ*gnV4mDM2wn z5P}etzMv(75QHEEMHKXaAOs-@K?q_d9RrmGREvxSsVu>pWi06MMVwyJDnmIs%YeP0 zvD+=a&IR3t + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 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 P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..61da551c5594a1f9d26193983d2cd69189014603 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b216f2d313cc673d8b8c4da591c174ebed52795c GIT binary patch literal 11873 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 literal 0 HcmV?d00001 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/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' From 57db71de6ae39a478a23436b8c20f84ea40df691 Mon Sep 17 00:00:00 2001 From: sashalukin Date: Tue, 18 Aug 2026 10:39:37 -0400 Subject: [PATCH 21/24] docs: Apply user edits to migration guide and README --- playbilling/README.md | 2 +- .../docs/play-billing-library-8-migration.md | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/playbilling/README.md b/playbilling/README.md index d6d453ce..ed1d5c30 100644 --- a/playbilling/README.md +++ b/playbilling/README.md @@ -11,7 +11,7 @@ The Play Billing module provides capabilities for your TWA app to connect with [ * Initialize a payment * Query product details -The module uses [Version 8.3.0](https://developer.android.com/google/play/billing/release-notes#8-0) 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 index 57e18a46..b079f0dd 100644 --- a/playbilling/docs/play-billing-library-8-migration.md +++ b/playbilling/docs/play-billing-library-8-migration.md @@ -63,22 +63,12 @@ When your JavaScript calls: const history = await service.listPurchaseHistory(); console.log(history); // [] ``` -It will now always successfully return an empty list (`[]`). +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). PBL 8 removed the legacy `querySkuDetailsAsync()` API that Android Browser Helper previously used to view subscriptions as a flat list. ABH now uses the newer `ProductDetails` API. +Google Play's subscription model is hierarchical (Subscriptions > Base Plans > Offers), but the DGAPI specification expects a single flat structure (`ItemDetails`). Because of this, Android Browser Helper now performs a **lossy conversion** for subscriptions and returns the **first eligible element**. -Because the DGAPI specification still expects a flat structure (`ItemDetails`), Android Browser Helper now performs a "lossy conversion" for subscriptions: -- It uses the base-plan ID associated with the first eligible subscription offer returned by Play. -- It prefers an offer within that base plan when one exists. -- It flattens that specific offer's pricing phases into DGAPI's legacy `ItemDetails` fields (`price`, `subscriptionPeriod`, `freeTrialPeriod`, etc.). - -**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. - -## Additional resources - -* [Google Play Billing Library 8 release notes](https://developer.android.com/google/play/billing/release-notes#8-0) -* [Digital Goods API specification](https://github.com/WICG/digital-goods) +**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. From 200d5f89a3ef62359d944eb4d4bda6a697669c87 Mon Sep 17 00:00:00 2001 From: sashalukin Date: Tue, 18 Aug 2026 11:21:57 -0400 Subject: [PATCH 22/24] docs: clarify lossy conversion and backwards compatible plans --- playbilling/docs/play-billing-library-8-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playbilling/docs/play-billing-library-8-migration.md b/playbilling/docs/play-billing-library-8-migration.md index b079f0dd..50d8fe97 100644 --- a/playbilling/docs/play-billing-library-8-migration.md +++ b/playbilling/docs/play-billing-library-8-migration.md @@ -20,7 +20,7 @@ Bubblewrap version `1.25.0` updates the default billing dependency to `1.2.0` (a 1. Update your Bubblewrap CLI: ```bash - npm i -g @bubblewrap/cli@latest + npm i -g @bubblewrap/cli@1.25.0 ``` 2. Navigate to your TWA project directory and update the project: ```bash @@ -69,6 +69,6 @@ It will now always return an empty list (`[]`). ### `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, Android Browser Helper now performs a **lossy conversion** for subscriptions and returns the **first eligible element**. +Google Play's subscription model is hierarchical (Subscriptions > Base Plans > Offers), but the DGAPI specification expects a single flat structure (`ItemDetails`). Previously, users could mark a certain plan or offer as "backwards compatible" in the Play Console, and the legacy `querySkuDetailsAsync()` API would respect that. This is no longer the case, and Android Browser Helper now performs a **lossy conversion** for subscriptions, returning the **first eligible element** it can find. **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. From 67933b2df999c22110c0a72189e089e6cf0f43d6 Mon Sep 17 00:00:00 2001 From: sashalukin Date: Tue, 18 Aug 2026 11:51:07 -0400 Subject: [PATCH 23/24] docs: format backwards compatibility note --- playbilling/docs/play-billing-library-8-migration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playbilling/docs/play-billing-library-8-migration.md b/playbilling/docs/play-billing-library-8-migration.md index 50d8fe97..f6a44a66 100644 --- a/playbilling/docs/play-billing-library-8-migration.md +++ b/playbilling/docs/play-billing-library-8-migration.md @@ -69,6 +69,8 @@ It will now always return an empty list (`[]`). ### `getDetails()` for subscriptions -Google Play's subscription model is hierarchical (Subscriptions > Base Plans > Offers), but the DGAPI specification expects a single flat structure (`ItemDetails`). Previously, users could mark a certain plan or offer as "backwards compatible" in the Play Console, and the legacy `querySkuDetailsAsync()` API would respect that. This is no longer the case, and Android Browser Helper now performs a **lossy conversion** for subscriptions, returning the **first eligible element** it can find. +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. From 571891cca2afc1d72eb46269a0db1bce01809792 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal <93222189+Dp-Goog@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:21:57 -0700 Subject: [PATCH 24/24] Update DelegationService to override default notification channel creation for TWAs (#615) --- .../trusted/DelegationService.java | 46 ++++++++++++++++++- .../NotificationDelegationService.java | 12 ++++- 2 files changed, 56 insertions(+), 2 deletions(-) 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/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 index 5e2bca2e..76e65760 100644 --- 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 @@ -18,6 +18,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; import com.google.androidbrowserhelper.trusted.DelegationService; import com.google.androidbrowserhelper.trusted.NotificationUtils; @@ -41,6 +42,7 @@ public void onDestroy() { } @Override + @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) public boolean onNotifyNotificationWithChannel( @NonNull String platformTag, int platformId, @@ -49,6 +51,14 @@ public boolean onNotifyNotificationWithChannel( Log.i(TAG, "Notification triggered for channel: " + channelName + " with importance: " + NotificationUtils.getNotificationImportance(this)); - return true; + 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; } }