From 82eaeebda3346001157a44ee8551c5a99fea040b Mon Sep 17 00:00:00 2001 From: Serg Date: Tue, 21 Apr 2026 17:58:42 -0400 Subject: [PATCH] [Android] Suppress engagement notifications and promos for Origin users (#35737) Brave Origin subscribers on Android currently receive general-engagement notifications and unbidden promo dialogs that are not tied to any Origin-disabled feature. Suppress them under the product stance that Origin users get a cleaner, promo-free experience. Surfaces gated: - RetentionNotificationPublisher: HOUR_3, HOUR_24, DAY_6, EVERY_SUNDAY, and DORMANT_USERS_DAY_14/25/40 notification fire-time. - BraveActivity: showDormantUsersEngagementDialog (cold-start and in-foreground tap paths), India ad-free callout, YouTube-in-Brave dialog. The existing getIsSubscriptionActive() pref only reflects Play Store purchases, so it misses desktop-linked subscribers. Introduce a new Java-side cached boolean BRAVE_ORIGIN_CREDENTIAL_SUMMARY_CACHED, written from two authoritative any-source signals: - fetchOrderCredentials success (primes the cache immediately on a successful Play Store purchase). - requestCredentialSummary callback (covers Play Store and desktop-linked paths; the Skus service resolves both internally). BraveActivity.finishNativeInitialization now calls requestCredentialSummary on every app start to keep the cache fresh. Resolves: https://github.com/brave/brave-browser/issues/54791 --- .../chromium/base/BravePreferenceKeys.java | 3 ++ .../chrome/browser/app/BraveActivity.java | 16 ++++++- .../BraveOriginSubscriptionPrefs.java | 42 ++++++++++++++++++- .../RetentionNotificationPublisher.java | 12 +++++- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/android/java/org/chromium/base/BravePreferenceKeys.java b/android/java/org/chromium/base/BravePreferenceKeys.java index 6755faf2287..fda2939d132 100644 --- a/android/java/org/chromium/base/BravePreferenceKeys.java +++ b/android/java/org/chromium/base/BravePreferenceKeys.java @@ -121,6 +121,9 @@ public final class BravePreferenceKeys { public static final String SEARCH_CHOICE_SCREEN_INSTALL = "search_choice_screen_install"; + public static final String BRAVE_ORIGIN_CREDENTIAL_SUMMARY_CACHED = + "brave_origin_credential_summary_cached"; + /* * Checks if preference key is used in Brave. * It's no op currently. We might reconsider diff --git a/android/java/org/chromium/chrome/browser/app/BraveActivity.java b/android/java/org/chromium/chrome/browser/app/BraveActivity.java index 51fe3e81a9c..aa7c4754986 100644 --- a/android/java/org/chromium/chrome/browser/app/BraveActivity.java +++ b/android/java/org/chromium/chrome/browser/app/BraveActivity.java @@ -1371,6 +1371,13 @@ public abstract class BraveActivity extends ChromeActivity && !BraveVpnPolicy.isDisabledByPolicy(mTabModelProfileSupplier.get())) { showLinkVpnSubscriptionDialog(); } + if (ChromeFeatureList.isEnabled(BraveFeatureList.BRAVE_ORIGIN)) { + // Refresh the cached Skus credential summary so sync "is Origin active" readers + // (promo/engagement gates) see up-to-date status. Local-first; only hits the + // backend when credentials are past their expiry window. + BraveOriginSubscriptionPrefs.requestCredentialSummary( + mTabModelProfileSupplier.get(), null); + } if (isFirstInstall && (OnboardingPrefManager.getInstance().isDormantUsersEngagementEnabled() || getPackageName().equals(BraveConstants.BRAVE_PRODUCTION_PACKAGE_NAME))) { @@ -1394,7 +1401,8 @@ public abstract class BraveActivity extends ChromeActivity .readBoolean(BravePreferenceKeys.BRAVE_OPENED_YOUTUBE, false) || ChromeSharedPreferences.getInstance() .readInt(BravePreferenceKeys.BRAVE_APP_OPEN_COUNT) - >= 7)) { + >= 7) + && !BraveOriginSubscriptionPrefs.getIsCredentialSummaryActiveCached()) { showAdFreeCalloutDialog(); } @@ -1455,7 +1463,8 @@ public abstract class BraveActivity extends ChromeActivity if (!isFirstInstall && !BravePrefServiceBridge.getInstance().getPlayYTVideoInBrowserEnabled() && ChromeSharedPreferences.getInstance() - .readBoolean(BravePreferenceKeys.OPEN_YT_IN_BRAVE_DIALOG, true)) { + .readBoolean(BravePreferenceKeys.OPEN_YT_IN_BRAVE_DIALOG, true) + && !BraveOriginSubscriptionPrefs.getIsCredentialSummaryActiveCached()) { openYtInBraveDialog(); ChromeSharedPreferences.getInstance() .writeBoolean(BravePreferenceKeys.OPEN_YT_IN_BRAVE_DIALOG, false); @@ -2272,6 +2281,9 @@ public abstract class BraveActivity extends ChromeActivity } public void showDormantUsersEngagementDialog(String notificationType) { + if (BraveOriginSubscriptionPrefs.getIsCredentialSummaryActiveCached()) { + return; + } if (!BraveSetDefaultBrowserUtils.isBraveSetAsDefaultBrowser(BraveActivity.this)) { DormantUsersEngagementDialogFragment dormantUsersEngagementDialogFragment = new DormantUsersEngagementDialogFragment(); diff --git a/android/java/org/chromium/chrome/browser/brave_origin/BraveOriginSubscriptionPrefs.java b/android/java/org/chromium/chrome/browser/brave_origin/BraveOriginSubscriptionPrefs.java index aac730fccb8..b9b53bb9e12 100644 --- a/android/java/org/chromium/chrome/browser/brave_origin/BraveOriginSubscriptionPrefs.java +++ b/android/java/org/chromium/chrome/browser/brave_origin/BraveOriginSubscriptionPrefs.java @@ -15,6 +15,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.chromium.base.BraveFeatureList; +import org.chromium.base.BravePreferenceKeys; import org.chromium.base.Callback; import org.chromium.base.ContextUtils; import org.chromium.base.Log; @@ -32,6 +33,7 @@ import org.chromium.chrome.browser.billing.PurchaseModel; import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.policy.BravePolicyConstants; import org.chromium.chrome.browser.preferences.BravePref; +import org.chromium.chrome.browser.preferences.ChromeSharedPreferences; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.settings.BraveOriginPreferences; import org.chromium.chrome.browser.settings.SettingsNavigationFactory; @@ -166,10 +168,17 @@ public class BraveOriginSubscriptionPrefs { } /** - * Gets the Origin subscription active status for the given profile. + * Gets the Play Store Origin subscription active status for the given profile. + * + *

Note: this only reflects Play Store purchases recorded in {@code + * BRAVE_ORIGIN_SUBSCRIPTION_ACTIVE_ANDROID} by the in-app billing callback. Users whose Origin + * subscription was purchased on desktop and linked via their Brave account will read as + * inactive here. For an any-source sync check suitable for UI gating, call {@link + * #getIsCredentialSummaryActiveCached}. For the authoritative fresh value, call {@link + * #requestCredentialSummary} (async, via the Skus mojo service). * * @param profile The profile to use for preference retrieval - * @return The subscription active status, or false if profile is null + * @return The Play Store subscription active status, or false if profile is null */ public static boolean getIsSubscriptionActive(@Nullable Profile profile) { if (profile == null) { @@ -379,6 +388,10 @@ public class BraveOriginSubscriptionPrefs { // Store the order ID UserPrefs.get(profile) .setString(BravePref.BRAVE_ORIGIN_ORDER_ID_ANDROID, orderId); + // A successful Play Store order fetch is an authoritative "Origin is + // active" signal; prime the sync cache immediately so promo gates honor + // it without waiting for the next credential summary refresh. + setIsCredentialSummaryActiveCached(true); success = true; } finally { skusService.close(); @@ -429,6 +442,7 @@ public class BraveOriginSubscriptionPrefs { PostTask.postTask( TaskTraits.UI_DEFAULT, () -> { + setIsCredentialSummaryActiveCached(isActive); if (callback != null) { callback.onResult(isActive); } @@ -440,6 +454,30 @@ public class BraveOriginSubscriptionPrefs { }); } + /** + * Caches the authoritative any-source Origin active status returned by the most recent {@link + * #requestCredentialSummary} callback. Unlike the Play-Store-only {@link + * #getIsSubscriptionActive}, the Skus credential summary covers both Play Store purchases and + * desktop-linked subscriptions, so this cache is the correct source for sync "is Origin active" + * gating on Android. + */ + private static void setIsCredentialSummaryActiveCached(boolean value) { + ChromeSharedPreferences.getInstance() + .writeBoolean(BravePreferenceKeys.BRAVE_ORIGIN_CREDENTIAL_SUMMARY_CACHED, value); + } + + /** + * Synchronous "is Origin effectively active" check for UI-thread gating of promo surfaces. + * Returns the value written by the most recent {@link #requestCredentialSummary} callback, or + * false if no refresh has run yet. Callers that need the authoritative fresh value should call + * {@link #requestCredentialSummary} directly; this getter is intended for hot paths that need a + * cheap sync answer and tolerate state as of the last refresh. + */ + public static boolean getIsCredentialSummaryActiveCached() { + return ChromeSharedPreferences.getInstance() + .readBoolean(BravePreferenceKeys.BRAVE_ORIGIN_CREDENTIAL_SUMMARY_CACHED, false); + } + /** * Parses the credential summary result on a background thread. This avoids potential UI * blocking for JSON parsing operations. diff --git a/android/java/org/chromium/chrome/browser/notifications/retention/RetentionNotificationPublisher.java b/android/java/org/chromium/chrome/browser/notifications/retention/RetentionNotificationPublisher.java index fe12f4ebcf5..57af678c8fe 100644 --- a/android/java/org/chromium/chrome/browser/notifications/retention/RetentionNotificationPublisher.java +++ b/android/java/org/chromium/chrome/browser/notifications/retention/RetentionNotificationPublisher.java @@ -17,6 +17,7 @@ import org.chromium.base.ThreadUtils; import org.chromium.base.task.AsyncTask; import org.chromium.chrome.browser.BraveRewardsNativeWorker; import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.brave_origin.BraveOriginSubscriptionPrefs; import org.chromium.chrome.browser.onboarding.OnboardingPrefManager; import org.chromium.chrome.browser.set_default_browser.BraveSetDefaultBrowserUtils; import org.chromium.chrome.browser.tab.TabLaunchType; @@ -84,7 +85,9 @@ public class RetentionNotificationPublisher extends BroadcastReceiver { case RetentionNotificationUtil.HOUR_3: case RetentionNotificationUtil.HOUR_24: case RetentionNotificationUtil.DAY_6: - createNotification(context, intent); + if (!BraveOriginSubscriptionPrefs.getIsCredentialSummaryActiveCached()) { + createNotification(context, intent); + } break; case RetentionNotificationUtil.DAY_10: case RetentionNotificationUtil.DAY_30: @@ -107,13 +110,18 @@ public class RetentionNotificationPublisher extends BroadcastReceiver { } break; case RetentionNotificationUtil.EVERY_SUNDAY: - if (OnboardingPrefManager.getInstance().isBraveStatsNotificationEnabled()) { + if (OnboardingPrefManager.getInstance().isBraveStatsNotificationEnabled() + && !BraveOriginSubscriptionPrefs + .getIsCredentialSummaryActiveCached()) { createNotification(context, intent); } break; case RetentionNotificationUtil.DORMANT_USERS_DAY_14: case RetentionNotificationUtil.DORMANT_USERS_DAY_25: case RetentionNotificationUtil.DORMANT_USERS_DAY_40: + if (BraveOriginSubscriptionPrefs.getIsCredentialSummaryActiveCached()) { + break; + } if (System.currentTimeMillis() > OnboardingPrefManager.getInstance() .getDormantUsersNotificationTime(notificationType)