[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
This commit is contained in:
Serg
2026-04-21 22:58:42 +01:00
committed by GitHub
parent 8aac786576
commit 82eaeebda3
4 changed files with 67 additions and 6 deletions
@@ -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
@@ -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();
@@ -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.
*
* <p>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.
@@ -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)