Skip to content

Release Notes

Highlights and behavior changes per release. The full changelog ships as CHANGELOG.md in the published repository.

Pin the SDK exactly and bump deliberately:

implementation("ad.elo:elo-ads-android:0.6.2")
Latest GitHub release
  • New The SDK requests a creative’s device-side tracking URLs. Some demand sources count a render, impression, or click only when the device that showed the ad requests their URL. When a creative carries such URLs, the SDK now requests them itself, at most once per showing, as the iOS SDK has since 0.6.0. The requests carry none of the SDK’s headers or session data, only the device’s default user agent, and their outcome does not affect Elo’s own tracking or any callback your app receives. No public API changes. See Privacy & Consent.
GitHub release
  • Change: the card is set like a feed ad. A header row holds the brand mark beside the brand name with the disclosure under it, the description follows as body copy at the card’s full width (up to three lines, no scrolling), and the CTA pill sits on a row of its own at the end edge. The card is taller than before and its height depends on the copy, so a bounded slot now needs a maxHeight of at least 192dp to get it; shorter slots get the strip, as does the loading placeholder. EloAdStyle.descriptionOverflow now applies to the strip only. See Ad Formats.
  • New EloDebugEvent.AdOperation.tapSurface. The operation reports the ad’s presentation.tap_surface value (cta or full) next to the other presentation values; null when not sent or on a no-fill. Kotlin callers that use AdOperation.copy or destructure it need to recompile.
Breaking changes GitHub release
  • New ChatMessage gains optional id and createdAt, and both are sent to Elo. The ad request carries id as the message’s id and createdAt (a java.time.Instant) as its time, so Elo knows when each turn happened and can connect a turn to a Search API call that sends the same id as X-Elo-Message-Id. Set them once, when the turn is created, and reuse them on every request; a MessageRole.SUMMARY message should carry the id and time of the turn it summarizes. Without createdAt, Elo records the time it received the message. An id longer than 256 Unicode code points or containing non-printable characters, and a time before 2020 or more than a day ahead, are left out with a warning. Neither affects the preload cache. The two-argument constructor is unchanged for Kotlin and Java alike; Kotlin callers compiled against an earlier version that use ChatMessage.copy or destructure a message need to recompile, because both now cover the new properties. See ChatMessage.
  • New Report your own in-app browser. If you open ad clicks in your own browser, call Elo.trackBrowserOpened(ad) when it opens and Elo.trackBrowserClosed(ad) when it closes. The SDK sends a browser_open event, then a browser_close event with the time in between as dwell_ms, so Elo learns how long the user stayed on the advertiser’s page. The time is measured on the device, counts time the app spent in the background or the device spent asleep, and is capped at 24 hours. A second open before the close, a close without an open, and a call before configure send nothing. Both events are analytics only and never billed. Your debug observer receives them as the new EloTrackingEvent.BROWSER_OPEN and BROWSER_CLOSE values, so an exhaustive when over that enum needs the new branches. Declare it as App interactions on your Data safety form; see Privacy & Consent and Your own in-app browser.
  • New The SDK reports where the ad was on screen when it became viewable. Once an Elo-served ad has been at least 50% visible for one second, EloAdView posts one analytics event with the ad view’s size and position in the window, the window size, the visible fraction, and the time since render, so Elo can verify that ads render in the SDK’s own view. Nothing about the surrounding app is sent, and the impression is recorded separately. It reaches your debug observer as the new EloTrackingEvent.VIEWABLE value. Declare it as App interactions on your Data safety form; see Privacy & Consent.
  • New maxHeight and maxWidth describe the ad slot, and the server picks the format. Elo.loadAd, Elo.preloadAd, and EloAdView(messages = ...) take maxHeight: Dp? and maxWidth: Dp?, both null (unbounded) by default. The SDK sends them as the request’s dimensions in whole dp, and the ad arrives in the format that fits: the card for an unbounded slot or one at least 80dp tall, or the strip for a slot from 72dp to 79dp (below 72dp there is no fill). EloAdView(result = ...) and EloAdView(ad = ...) render that format, so there is nothing to choose on your side. The loading placeholder follows maxHeight, and a preloaded ad is only served to a load with the same maxHeight and maxWidth. EloKeyboardBannerAd sends the strip’s 76dp height. Kotlin callers compiled against an earlier version need to recompile, because the default-argument methods of loadAd and preloadAd changed; Java callers keep the same loadAd and preloadAd methods. See Ad Formats.
  • Breaking EloAdLayout, every layout parameter, and AdRenderOptions.layout are removed. Pass maxHeight (and maxWidth) for the slot instead. A slot that used EloAdLayout.InlineBanner passes its height, below 80dp; a chat-row card needs no argument.
  • Change: requests no longer send display_position. The request’s dimensions replace it.
  • Breaking Elo.trackRender, Elo.trackImpression, and Elo.trackClick are removed. EloAdView sends render, impression, and click tracking itself, so render ads with EloAdView or EloKeyboardBannerAd. EloAdListener callbacks are unchanged.
  • Breaking AdTracker and EloAd.copy are no longer public. The public EloAd constructor drops its tracker and onRelease parameters and builds a display-only ad that sends no events, for previews and tests.
  • Breaking On-device diagnostics are replaced by a debug event observer. Elo.diagnosticsSnapshot(), Elo.mediationDebugSnapshot(), and Elo.setRequestPayloadCaptureEnabled() are removed, along with the EloDiagnosticsSnapshot, EloDiagnosticsEntry, EloTrackingDiagnosticsEntry, EloTrackingTotals, EloIntegrationStatus, and EloMediationDebugSnapshot types. Elo.setDebugObserver(observer, capturePayloads) replaces all of them: the SDK reports each EloDebugEvent as it happens (load and preload outcomes, and tracking attempts and their outcomes) and keeps no history of its own. An app that wants a recent-loads list or tracking totals records the events itself. With no observer attached, the SDK stores no load history, no tracking history, and no request payloads. Pass capturePayloads = true to also receive the redacted request payload and decoded ad response that setRequestPayloadCaptureEnabled(true) used to enable. See Debug observer.
  • New EloDebugEvent.AdOperation reports the slot and the ad’s format. It gains format, maxHeight, maxWidth, ctaEnabled, ctaVariant, and tapInset, each null when it does not apply.
  • Breaking AdMob mediation and the client-side auction are removed. The ad.elo:elo-android-mediation-admob adapter (last published as 0.1.4) is no longer built or published, and the SDK no longer runs an auction: every request goes straight to Elo’s server. If you also run another network, request Elo first and, on NoFill or Error, hand the slot to the other network in your own code; see Falling back to another network and Other Ad Networks. What changed on the public surface:
    • The AdNetworkAdapter, AdBid, AdBidRequest, AdConsent, AdAdapterError, AdMediator, and ParallelAuctionMediator types are removed, as is EloConfiguration.adapters.
    • AdResult.Loaded is now Loaded(ad, eCpm). The networkId field is gone because every fill is Elo’s.
    • NoFillReason.AuctionTimeout is renamed NoFillReason.Timeout (the request did not complete within the SDK’s deadline). NoBids now means the server returned no ad, and NetworkError means the request failed with a transport or server error.
    • The AuctionStarted, AdapterStartStarted, AdapterStartFinished, AdapterStartFailed, AdapterOutcome, and AuctionFinished debug events are removed, along with EloAdapterAuctionOutcome, EloAuctionOutcome, and EloErrorCategory.Adapter. EloAdLoadOutcome.Loaded carries only eCpm, and EloDebugEvent.Configured reports requestTimeoutMs instead of adapterIds and auctionTimeoutMs.
  • Breaking The deprecated Elo.initialize(...) is removed. It called configure and has logged a deprecation warning since 0.1.9. Use Elo.configure(context, publisherId, adUnitId), or build an EloConfiguration and call Elo.configure(context, configuration) for COPPA/TFUA, logLevel, or baseUrl.
  • Breaking The deprecated EloAd.release() alias is removed. Call releaseResources(), which release() already called.
  • New EloAdStyle.fontFamily. Draws the headline, description, CTA label, and disclosure badge in your typeface, at the SDK’s own sizes and weights, so the ad’s height does not change.
  • New EloAdStyle.chrome and EloAdChrome.Bare. A bare ad draws no background, border, or rounded corners, so it can sit inside your own chat row. The card also drops its inner padding except a 4dp inset for the disclosure badge; the strip keeps its padding. The loading skeleton follows the same setting. See Styling.
  • Recompile against this version. EloAdStyle and ChatMessage gain fields, which changes their constructors and copy methods, and the default-argument methods of loadAd and preloadAd changed. Kotlin and Java code compiled against an earlier version and calling them fails with NoSuchMethodError until it is rebuilt; source code needs no change.
  • Change: the CTA label stays readable on a custom fill. When you set callToActionBackground without callToActionForeground, the label is black or white, whichever contrasts more with your color, instead of the theme’s onPrimary.
Breaking changes GitHub release
  • Breaking The SDK reports display_position, you no longer pass it. The parameter was a hint about a surface the SDK already knows: it renders the ad, so it knows whether the ad landed in the chat-row card or the keyboard strip. It now reports card for EloAdLayout.CompactHorizontal (the default EloAdView card) and banner for EloAdLayout.InlineBanner (what EloKeyboardBannerAd renders), and a request made through Elo.loadAd / Elo.preloadAd directly carries no position at all. Two consequences worth knowing: the position is out of the preload cache key, so a preloaded ad is served to whichever surface later loads the same messages instead of being stranded when the positions disagree, and AdBidRequest.displayPosition is no longer part of the adapter contract, so a third-party adapter that read it must drop the field. Reporting is unaffected: the values reaching your dashboards are the same ones the slots were already sending. Remove the argument and nothing else changes.
  • New Taps inside an Elo-served ad are reported to Elo analytics. EloAdView and EloKeyboardBannerAd now send an ad_tap event (endpoint sdk/events) for every tap inside an Elo-served card or strip: the region under the finger (thumbnail, headline, description, CTA, chevron, or chrome), the tap position as a fraction of the ad view’s own size, the view’s size in dp, whether the tap opened the destination, the time since the impression, and a per-showing tap index. Taps on the inert parts of a strip that carries a CTA are reported as not opened, which is what makes the missed-CTA rate and the tap heatmap measurable. Coordinates are relative to the ad view and never to the screen, at most fifty taps are reported per showing, and adapter-rendered fills are excluded. Nothing about how taps behave changed, and the event is analytics only: it is never billed and it does not affect click reporting. Diagnostics follow it as a TAP tracking event, so EloTrackingDiagnosticsEntry.Event has a new value; an exhaustive when over that enum needs the extra branch. Declare it as App interactions on your Data safety form if you have not already; see Privacy & Consent.
  • New Server-selected call-to-action treatments on the keyboard strip. An Elo-direct inline banner can now ask for one of three treatments beyond the existing control: a decorative arrow after the label, a single pop of the whole button, or one shimmer sweeping across the clipped CTA pill. The ad server picks it per creative; there is no publisher-facing setting, and a missing or unknown value renders the control CTA you see today. The arrow widens the button it is added to; the pop and the shimmer are drawn, so they leave the button’s bounds and touch target alone. Motion starts only after one continuous second at 50% visibility, repeats every few seconds while the CTA stays viewable, and rests at the static appearance in between; leaving view cancels it, and the device’s remove-animations setting keeps the static icon while skipping the motion. Click and accessibility behavior, compact cards, and adapter-rendered ads are unchanged.
  • The strip’s creative mark is a rounded square with no border. It keeps its 40dp size and its disclosure badge; the corner radius is now 10dp and the hairline ring introduced in 0.4.1 is gone. Ad servers pick a favicon or an apple-touch-icon for the creative image, and those are drawn for the app-icon silhouette, so a circular crop was cutting the ends off the artwork: measured across 90 days of served creatives, the disc removed 9.7% of the average mark and the new radius removes 2.9%. Both layouts now use a rounded square. Nothing in the layout moves.
Breaking changes GitHub release
  • Fix A retained ad view no longer re-reports a render or impression after a reconfigure. The one-render-one-impression-per-opportunity latch now lasts the life of the process, across both Elo.configure() and Elo.shutdown(), so remounting a view held from before cannot bill the same opportunity a second time. Until this release, shutdown() cleared that state and an ad shown before it became reportable again after the next configure.
  • Fix An ad hidden by app or native-host state no longer counts as an impression. The one-second dwell now requires a resumed app, a focused host window, and an attached, visible, non-transparent native host hierarchy. Backgrounding, opening another window, hiding or detaching the host, or making its native hierarchy effectively transparent cancels an armed dwell; returning to a visible state starts a fresh one. The tracker also re-checks these signals when the dwell finishes rather than trusting state measured a second earlier. Separately, a scaled or otherwise transformed Compose host now has its visible fraction measured entirely in window space, instead of comparing transformed pixels against an untransformed layout size.
  • New Elo.setUserIdentity(userIdentifier, userData): both halves of the identity in one write. The identifier and the user data are persisted as one encrypted record, so setting them with the two existing setters in sequence left an intermediate state on disk: an account switch stored (B, A's contact details) until the second call landed, and an ad request or a process kill in that window sent or retained one account’s PII under the other’s identifier. Prefer this call from your auth-state hook whenever both are changing, including sign-out (Elo.setUserIdentity(null, null)). setUserIdentifier and setUserData are unchanged and still correct when only one of the two is. See the API Reference.
  • The ad disclosure moved off the headline onto the creative mark. It used to lead the headline as Ad • Headline, costing the headline the width of the disclosure plus its separator on every card. It is now a small badge riding the top-end corner of the creative’s own mark, on both layouts, so the headline gets the full width of the text column and the disclosure still cannot be truncated: it is outside the text flow entirely rather than merely at the head of it. It hangs off the mark’s corner rather than sitting flush inside it, so it nests against the artwork instead of covering it. A creative whose image fails to load has nothing to mark, so there the badge falls back to the surface’s top-end corner and the row reserves its measured width on that edge, keeping a call-to-action button clear of it. Card height is unchanged. Two consequences: EloAdStyle.badgeColor now fills the badge (the copy is drawn over it in the card color), and a Compose test asserting the combined "Ad • Headline" node must be split — the disclosure and the headline are separate nodes now, so assert each directly instead of a startsWith on the merged text.
  • The badge is restyled, and sized to its own type. It is a near-white disc with a hairline black ring and black copy at medium weight, squared up from a capsule since the default copy is two characters; longer copy relaxes it back into a capsule rather than being squeezed. Those are the only fixed colors in the SDK, because the badge sits on creative artwork rather than on a surface the theme controls: a fill that followed light and dark would read against one creative and vanish into the next. EloAdDisclosure.color and EloAdStyle.badgeColor still override it, which takes the ring off and inverts the copy. The badge also no longer inherits the ambient body style’s 24sp line height, which made a 9sp capsule stand around 22dp tall and cover the artwork it is badged onto; its line box is trimmed to the glyphs, and it draws a step smaller on the keyboard strip (8sp) than on the card (9sp), whose mark is larger. EloAdDisclosure.fontSize still pins one size across both.
  • Breaking sponsoredLabel takes an EloAdDisclosure. The new value carries the disclosure copy plus an optional fontSize, fontWeight, color, and testTag, so you can restyle the badge and give Compose UI tests a stable handle on it. It is a value rather than a composable slot on purpose: EloAdView draws the badge itself, overlaid on the surface, which is what makes it untruncatable — a publisher-supplied composable would have to be laid out beside the headline, where a long headline can squeeze or clip it. AdRenderOptions.sponsoredLabel is unchanged and still a String: adapters build their own native views, so there is nothing there for a type or a tag to bind to. Mediated fills keep the look they have today, taking your disclosure copy and rendering it in the adapter’s own treatment. See EloAdDisclosure.
  • Breaking callToActionLabel is gone. Call-to-action copy is backend-managed now: it arrives on the creative as EloAd.ctaText and labels the pill both layouts draw. The publisher-supplied parameter only ever reached renderer-backed fills, and no bundled adapter read it, so nothing that shipped ever rendered it. Drop the argument from your call sites; AdRenderOptions.callToActionLabel and its deprecated ctaLabel alias are removed with it.
  • Breaking openLinkAccessibilityLabel is gone, along with the TalkBack click label it carried. See the accessibility callout above.
  • New EloAd.ctaText: some demand sources send a call-to-action label (Install, Learn more) alongside the creative. Where one arrives, both layouts draw it as a pill in the trailing slot in place of the disclosure chevron, colored by EloAdStyle.callToActionBackground / callToActionForeground. See EloAd.
  • The two layouts take deliberately different click rules. The in-chat card stays one click target from edge to edge whether or not the creative sends a label: in a transcript the card reads as a single object and there is no neighbouring control for a stray tap to hit, so its pill is decoration rather than a second button. The keyboard banner does the opposite — when the creative sends a label, only that button clicks, because the strip sits under the reader’s thumb beside the composer. A strip without a label keeps its whole row clickable behind the chevron. Either way the click opens the same destination, and render and impression tracking stay on the container, so viewability is unaffected.
  • EloKeyboardBannerAd shows its loading skeleton by default. showLoadingPlaceholder now defaults to true, matching EloAdView. The skeleton stands at the strip’s own size, so the composer above keeps its place when an ad arrives and the slot reads as loading rather than as empty. Pass false to keep the keyboard edge clear until an ad fills.
  • Fix A slow creative image no longer holds the whole ad in its skeleton. The thumbnail now carries its own shimmer and swaps in when the image finishes, so the headline and description render as soon as the ad does.
  • The keyboard banner’s strip was retuned to fit real creatives: a circular brand mark, tighter gutters, and a smaller type scale on the attribution line. At the previous size an ordinary advertiser name ellipsized before the strip had drawn anything else. The mark ends this release at 40dp, up from 30dp, without the strip changing height: the row is pinned to the call-to-action button’s 48dp interactive minimum, so the mark grew into height the row already paid for. It now carries a hairline grey ring, so a white or near-white logo has an edge of its own instead of dissolving into the surface at that size. The card’s mark is unchanged at 56dp and needs no ring at that size; the card’s row is pinned to the mark itself, so a larger one would have come out of the card’s own padding and crowded the text column. The card stays 80dp tall. The card’s mark stays a rounded square and the strip’s stays a circle: creative artwork is a brand mark far more often than photography, and marks are routinely wide wordmarks, which a circular crop takes the ends off, while the strip’s mark is small enough to read as an avatar, where a wordmark is unreadable either way.
  • Fix The ad slot no longer changes height when the request lands. The strip’s row was sized by whatever filled its trailing slot (a call-to-action button carries a 48dp interactive minimum, a chevron is a fraction of that), so a CTA fill grew the slot on arrival while the loading placeholder matched only the chevron case. Both layouts and the placeholder now reserve the same row height, 48dp on the strip and 56dp on the card, which also fixes the card’s mirror image: a creative whose image failed to load dropped its tile and shrank the card below the placeholder.
  • Fix Creative artwork is fitted explicitly, at high filter quality, so a mark that is not square is scaled to fit its tile rather than cropped or resampled coarsely.
  • New Elo.setUserData(userData): apps that know their signed-in user can share age, gender, email, and phone on ad requests. The fields are recorded for upcoming targeting features and have no effect on ad selection today. Pass contact details as they are — they travel over HTTPS and are SHA-256 hashed at the ad server before anything is stored, so Elo never persists a plain email address or phone number and you don’t hash them yourself. Give a phone number its country code (E.164) or the server discards it. The SDK does not validate what you set: the server normalizes each field and drops what it can’t use. Each call replaces the whole object. The ad server discards user data entirely on requests flagged COPPA or TFUA, and on any request where GDPR applies. Sharing contact details is a disclosure obligation: see the API Reference and Privacy & Consent.
  • Publisher-supplied identity now survives an app restart. The identifier from setUserIdentifier and the data from setUserData are stored as one record, sealed with AES-GCM under a non-exportable Android Keystore key and kept as ciphertext in the SDK’s own SharedPreferences, then read back at launch. An app that keeps people signed in between launches used to report as anonymous on every relaunch until the user happened to sign in again; it no longer does. Set both from a single auth-state hook: that is the call site that covers sign-in, sign-out, and a mid-session account switch, and the one that keeps a stored copy from going stale when the user changes their email or phone. shutdown() erases the stored record. Nothing is ever written in plaintext — if the Keystore is unavailable the SDK keeps identity in memory for that process rather than falling back to an unprotected file. Storing contact details on the device is worth a line in your privacy policy and Data safety form.
  • Adapter authors EloAd gained ctaText in its primary constructor, which moves the JVM descriptors of that constructor and of copy; the previous descriptors are retained as hidden shims, so existing adapter bytecode still links across that change. The AdRenderOptions removals carry no such shim — see the breaking-changes callout.
Breaking changes GitHub release
  • Fix EloAdListener.onAdDidTrackImpression now fires only when the impression ping actually lands. It is documented as firing after the SDK fires the ping, but it was dispatched alongside the attempt, so it reported impressions the ad server rejected or never received. Two things caused that, both fixed: the callback did not wait for the ping, and the ping itself reported success unconditionally. Render and impression pings now surface non-2xx responses and transport failures, which is also what lets EloDiagnosticsSnapshot.trackingTotals report a non-zero failed count. onAdDidReceiveClick is unchanged: it fires on the tap, before the click URL opens.
  • Fix Calling Elo.trackRender(ad) or Elo.trackImpression(ad) yourself no longer double-counts. The one-render-one-impression-per-opportunity limit used to live in the view layer, so a fully custom layout calling the tracking hooks directly could report the same opportunity more than once. It now sits behind the public API and covers every path into it, so the hooks are safe to call on every recomposition. A repeat load that returns the same creative is a distinct opportunity and still records. A suppressed duplicate is silent: no ping, and no onAdDidTrackImpression callback.
  • Fix A render or impression is no longer consumed when the SDK cannot send it. Each ad opportunity records at most one render and one impression for the process lifetime. Calling the tracking hooks before configure (or after shutdown) marked the opportunity as counted even though nothing was sent, so the real ping could never follow. The SDK now checks that it can deliver before spending that budget.
  • New EloDiagnosticsSnapshot.trackingTotals: a per-event count of every render, impression, and click attempt since configure. trackingEntries is a bounded ring buffer, so it could not tell you how many pings failed once a session got past the newest twenty, and a ping the SDK gives up on is a lost impression it never retries. Each EloTrackingTotals carries attempted, delivered, failed, unobservable, and inFlight, is never evicted, and appears in asExportableText(). Cleared on configure and shutdown(), like the rest of diagnostics. Removed in 0.6.0; see Debug observer.
  • Fix An ad card no longer stays without its image when the same creative comes back in a later ad. The card hides its thumbnail when a creative’s image cannot be loaded, so a broken URL leaves text rather than a blank square. That was remembered against the creative rather than the ad, so while the card stayed in composition, one failure also suppressed the image on every later ad serving that creative. It now applies only to the ad it was recorded for.
  • Render tracking moved into the impression-tracking modifier. EloAdView and the adapter-rendered path each used to fire their own render ping next to the impression modifier they already applied; both now come from the modifier, so an ad surface cannot attach one and forget the other. No change to when a render fires or how it is deduped.
  • Adapter authors the EloAd constructor now takes both id and creativeId, and id must be unique per fill. Pass your network’s per-response id if it has one, or mint a UUID.randomUUID().toString(). Passing a creative id there collapses every showing of that creative into a single tracked ad, so only the first reports a render and an impression. The AdMob adapter mints one per fill as of its next release, which fixes exactly that under-reporting on AdMob native fills.
GitHub release
  • New Elo.setUserIdentifier(identifier): apps with a sign-in can send their own user ID as the visitor_id on ad requests and tracking pings in place of the SDK’s anonymous per-install ID, so delivery, frequency capping, and reporting follow the user across installs and devices. Memory-only: re-set it on each launch and pass null on sign-out to restore the anonymous ID. shutdown() clears it; a re-configure does not. An ad keeps whichever identity it was requested under for its whole lifetime. See the API Reference.
  • A scrolling ad description now makes three passes and then settles into an ellipsized line instead of animating for as long as the ad is on screen (the same pass budget as iOS). The endless marquee kept the host app’s UI from ever going idle, which stalls automation frameworks that wait for idle before each interaction. Scroll speed, end holds, travel distance, and row height are unchanged, and a new creative gets its own passes.
  • The loading placeholder’s shimmer sweeps a few times and then rests as a plain skeleton, and stays still when animations are disabled, instead of repeating for as long as the placeholder is on screen.
  • Creatives whose image is a .ico now show their thumbnail. Publishers commonly supply a site favicon, and Android can’t decode ICO natively, so these ads rendered with no thumbnail at all. The SDK’s image loader now decodes ICO itself (both PNG-embedded and uncompressed-DIB forms, picking the largest image in the file), closing the parity gap with iOS. A creative image that fails to load also logs a warning with the failing URL and cause under the Elo tag, instead of dropping the thumbnail silently.
  • A creative that wins more than once now records a render and an impression every time it’s shown, with re-appearances of the same opportunity (recomposition, lazy-list re-entry, configuration changes) still reporting exactly once. One consequence worth knowing: Elo.shutdown() now clears render dedup state as well as impression state, so an ad shown before shutdown can report again after a re-configure.
GitHub release
  • The ad disclosure now leads the attribution line: Ad • Headline (previously Headline · Sponsored). Leading the disclosure makes it structurally impossible for a long headline to truncate it away; the separator is now • and sponsoredLabel’s default changed from "Sponsored" to "Ad". TalkBack announces the disclosure first. The parameter name is unchanged — keep passing a localized string for non-English surfaces.
  • Creative descriptions are now a single line that marquee-scrolls when the copy doesn’t fit, so the whole line stays readable instead of wrapping. Motion pauses while the ad is off screen and falls back to a static ellipsis when reduced-motion or disabled animations are set. Opt out with the new EloAdStyle.descriptionOverflow = EloAdDescriptionOverflow.Truncate.
  • An ad image that fails to load now hides the thumbnail instead of leaving a blank tile; the text takes the reclaimed space. AdMob-rendered fills apply the same rule to a missing icon asset.
  • Requires the matching AdMob adapter 0.1.4 for AdMob fills to render the new Ad • Headline disclosure order.
GitHub release

First release of ad.elo:elo-ads-android, published in lockstep with the iOS SDK 0.1.8. Highlights:

  • Privacy Visitor IDs are always anonymous. The SDK generates a local anon_<UUID> and never derives the visitor ID from the Google Advertising ID. The Device-Name and System-Version HTTP headers are removed, and the User-Agent is now built from real device info with the actual SDK version.
  • Renamed for iOS parity (old names deprecated, not removed): EloAd.release() → EloAd.releaseResources(), and EloAdView’s ctaLabel parameter → callToActionLabel.
  • New public EloAdLayout enum (CompactHorizontal / InlineBanner) selected via the ad views’ layout parameter, plus CTA color style tokens EloAdStyle.callToActionBackground / callToActionForeground for renderer-backed fills.
  • Ad requests and same-origin tracking pings now carry the X-Elo-State session envelope header, matching the iOS and web SDKs. It is never attached to URLs off the configured API origin.
  • The mediation auction now runs one shared deadline spanning adapter start() + bid(), and the default auction timeout dropped from 5s to 3s — both matching iOS.
  • Render pings are deduped process-wide, so lazy-list re-entry can no longer re-fire trackRender for the same ad.
  • Fixed a native-resource leak: EloAdView(messages) and EloKeyboardBannerAd now release the owned ad when they leave composition (previously adapter-backed ads such as AdMob NativeAd leaked on navigation).
  • Preloaded ads now expire after 300 seconds, matching iOS.
  • The default API base URL is production in all build types — debug builds no longer point at the dev endpoint. Request retries follow the iOS retry policy and failures surface as typed EloErrors.
  • AdCreative decoding is lenient: source, offer_type, and format are optional, and unknown enum values no longer fail the whole response.
  • Elo-direct click POST delivery is temporarily disabled. Tapping still opens the creative destination and dispatches the local click callback.
  • New keyboard banner format: EloKeyboardBannerAd(messages) pins an inline banner strip above the software keyboard with one composable — see Keyboard banner.
  • Coordinates are never sent for coppa / tfua configurations, regardless of shareGeoLocation.
  • Ad requests now carry a settings-derived country (device.geo.country) and UTC offset (device.geo.utcoffset) — permission-free, from locale/time-zone settings, not GPS. The shareGeoLocation opt-out covers coordinates only.
  • Elo-rendered creatives drop the CTA pill; the whole card is tappable. callToActionLabel now only affects renderer-backed mediation fills, and EloAdLoadingView(showCtaPlaceholder:) is ignored.
  • The host-app privacy guide (PRIVACY.md) now ships in the repository, linked from the README.
  • Elo.initialize is deprecated: Elo.configure is the single entry point, with a new configure(context, publisherId, adUnitId, shareGeoLocation, geoLocationPrecision) convenience for Elo-only integrations. Migrating is a rename for most apps; coppa / tfua move onto EloConfiguration.
  • MessageRole documents the human-to-human mapping: device owner → USER, the other participant → ASSISTANT.