Skip to content

Add 300x250 ad placements and an ad placement priority system - #98

Open
arch10 wants to merge 8 commits into
mainfrom
claude/scrollable-ad-banners-f90yaa
Open

Add 300x250 ad placements and an ad placement priority system#98
arch10 wants to merge 8 commits into
mainfrom
claude/scrollable-ad-banners-f90yaa

Conversation

@arch10

@arch10 arch10 commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Adds larger 300x250 ad placements so their revenue can be compared against the existing 320x50 banners, and makes each screen show at most one ad.

Inline 300x250 experiment (History + Settings) — gated behind a new enable_inline_ads Remote Config flag, on top of enable_ads. New unit id keys history_inline_ad_id / settings_inline_ad_id default to empty, so nothing renders until real units are published.

  • History: the ad is a real list row injected after the 3rd entry (clamped to the list size), so it scrolls with the history. HistoryAdapter becomes multi-view-type and maps adapter positions back to history indices so the share/delete context menu still targets the right entry. When there is no history at all, the ad renders in the empty-state view instead, and relocates live if the list flips between empty and non-empty (e.g. clearing history while the screen is open).
  • Settings: the ad sits between the Appearance and App sections inside the NestedScrollView.

About page 300x250 — a separate placement, not part of the experiment: gated on enable_ads plus its own about_ad_id key, with no dependency on enable_inline_ads. Sits after the last content card. Its AdView is destroyed in onDestroyView, since unlike an activity this fragment's view is recreated when navigating back from Changelog / Open Source.

Ad placement priority system — History and Settings previously requested both an inline ad and the bottom banner, so both could show at once. resolveAdPlacement() now picks exactly one, in priority order:

Priority Placement Condition
0 Inline 300x250 enable_ads and enable_inline_ads and inline unit id set
1 Bottom banner enable_ads and banner unit id set
2 No ad otherwise

The choice is made from Remote Config before any request goes out, so a screen that picks the inline ad does not fall back to the banner when that request fails to fill — a session stays purely inline or purely banner, keeping the experiment readable. An inline ad whose unit id is unset is not a usable placement, so it falls through to the banner rather than blocking it. With enable_inline_ads off — today's production config — every screen still resolves to the bottom banner, so behaviour is unchanged until the experiment is switched on.

A 300x250 AdView reserves its full height even on an unfilled request, so every inline placement is only surfaced from onAdLoaded, never leaving a blank 250dp block.

Analytics stay mutually exclusive per screen: INLINE_ADS_ENABLED when the inline wins, ADS_ENABLED when the banner wins, ADS_DISABLED with a reason when neither is available.

CI — adds a PR Build workflow (unit tests, lint, debug APK uploaded as a run artifact; no tag, release, or Play upload) and workflow_dispatch on App Pre-Release so a pre-release APK can be built from any branch on demand. Also adds the lambda logEvent overload to BaseFragment, which BaseActivity already had.

Test plan

  • ./gradlew test and assembleDebug pass in CI
  • ./gradlew lint runs (reported, non-blocking — no lint baseline exists yet)
  • Verified on-device with test ad units: inline 300x250 renders on History and Settings
  • History with no entries: ad renders in the empty state; clearing history while the screen is open relocates the ad without a crash or flicker
  • History share/delete context menu targets the correct entry with the ad row present
  • About: 300x250 renders after the last card; navigating to Open Source and back does not stack a second ad
  • Priority: with enable_inline_ads on, each screen shows only the inline ad and no bottom banner (and no leftover bottom margin); with it off, only the bottom banner
  • With enable_ads off, no ads anywhere

🤖 Generated with Claude Code

https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq

Adds a second, larger ad placement to the two scrollable screens so the
larger format can be measured against the existing 320x50 banners.

- History: the ad is a real list row injected after the 3rd entry
  (clamped to the list size), so it scrolls with the history.
  HistoryAdapter becomes multi view type and maps adapter positions back
  to history indices for the share/delete context menu.
- Settings: the ad sits between the Appearance and App sections inside
  the NestedScrollView.

Both placements are gated behind a new enable_inline_ads Remote Config
flag and only render when enable_ads is true as well. The new ad unit
ids (history_inline_ad_id, settings_inline_ad_id) default to empty, so
the placements stay off until the real units are published.

A 300x250 AdView reserves its full height even on an unfilled request,
so each placement is only surfaced from onAdLoaded, and both AdViews are
destroyed with their activity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
Adds a PR Build workflow that runs the unit tests, lints, and attaches a
debug APK to the run as an artifact. It never tags, releases, or touches
Play, so a change can be installed and tested before it is merged.
Debug builds need no secrets since google-services.json is committed and
signing falls back to the debug keystore.

Lint is reported but not blocking, since the project has no lint baseline
yet and a first failing run would block every pull request.

Also adds workflow_dispatch to App Pre-Release so a pre-release APK can be
built from any branch without pushing to beta first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
setupObservables() only builds a HistoryAdapter when the list is
non-empty; with no history it swaps to the noHistory empty-state view
instead, so the loaded 300x250 ad had nowhere to attach and silently
never showed.

Adds a noHistoryAdContainer inside the empty state and routes the
already-loaded ad there via attachInlineAd(), tracking isHistoryEmpty
so the ad lands in the right place regardless of whether the ad or the
history list finishes loading first. Also re-routes the ad on every
list emission, since clearing history (or deleting the last item) via
the toolbar/context menu flips the list to empty while this screen is
still open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
Adds a 4th 320x50 banner placement (same size as main/history/settings)
to AboutFragment, after the last content card inside the scrolling
NestedScrollView so it appears once the user scrolls to the bottom.

Gated the same way as the existing three banners: enable_ads plus a
non-empty about_ad_id from Remote Config (new key, defaults to empty so
it stays off until a real unit is published) and UMP consent. The
AdView is destroyed in onDestroyView, since unlike an activity this
fragment's view can be recreated on back-stack navigation to and from
the Changelog/Open Source screens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
CI failed to compile: AboutFragment.kt's enableBannerAd() calls
logEvent(ADS_DISABLED) { param("reason", ...) }, following the same
pattern History/SettingsActivity already use, but BaseFragment only
had the single-Bundle overload, not BaseActivity's lambda-based one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
Replaces the small 320x50 banner on About with the same 300x250 MREC
used on History and Settings, and folds it into that same experiment
instead of keeping it a separate always-on placement: it's now gated
by enable_inline_ads (in addition to enable_ads) and reuses the shared
resolveInlineAd()/createInlineAdView() helpers from util/InlineAd.kt,
matching SettingsActivity's implementation.

Drops the now-unused about_ad_id Remote Config key in favor of
about_inline_ad_id, consistent with history_inline_ad_id /
settings_inline_ad_id. The ad container is hidden until onAdLoaded,
same as the other two placements, so an unfilled request never
reserves a blank 250dp block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
The About page's 300x250 ad is a separate placement from the
enable_inline_ads experiment on History/Settings, not a third arm of
it. Gates it on enable_ads alone again, and swaps the Remote Config
key back to about_ad_id (dropping about_inline_ad_id).

Still uses createInlineAdView() for the MREC creation/reveal-on-load
behavior, since that part is just shared AdView boilerplate — only the
gating decision (resolveInlineAd(), tied to enable_inline_ads) no
longer applies here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
History and Settings were each requesting both an inline 300x250 and
the anchored bottom banner, so both could show at once. Replaces the
two independent ad paths with a single resolveAdPlacement() that picks
one placement in priority order: the inline 300x250, then the bottom
banner, then nothing.

The choice is made from Remote Config before any request goes out, so
a screen that picks the inline ad does not fall back to the banner
when the inline request fails to fill — a session stays purely inline
or purely banner, which keeps the experiment readable. An inline ad
whose unit id is unset is not a usable placement, so it falls through
to the banner rather than blocking it.

Analytics stay mutually exclusive per screen: INLINE_ADS_ENABLED when
the inline wins, ADS_ENABLED when the banner wins, ADS_DISABLED with a
reason when neither is available. Drops the now-redundant
INLINE_ADS_DISABLED event, and renames util/InlineAd.kt to
util/AdPlacement.kt since it now owns the whole decision.

With enable_inline_ads off — today's production config — every screen
still resolves to the bottom banner, so behaviour is unchanged until
the experiment is switched on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW79CouiX84JP9fgbMhPwq
@arch10 arch10 changed the title Add 300x250 inline ad placements to history and settings Add 300x250 ad placements and an ad placement priority system Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants