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")- 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.
- 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
maxHeightof at least 192dp to get it; shorter slots get the strip, as does the loading placeholder.EloAdStyle.descriptionOverflownow applies to the strip only. See Ad Formats. - New
EloDebugEvent.AdOperation.tapSurface. The operation reports the ad’spresentation.tap_surfacevalue (ctaorfull) next to the other presentation values;nullwhen not sent or on a no-fill. Kotlin callers that useAdOperation.copyor destructure it need to recompile.
- New
ChatMessagegains optionalidandcreatedAt, and both are sent to Elo. The ad request carriesidas the message’s id andcreatedAt(ajava.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 asX-Elo-Message-Id. Set them once, when the turn is created, and reuse them on every request; aMessageRole.SUMMARYmessage should carry the id and time of the turn it summarizes. WithoutcreatedAt, 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 useChatMessage.copyor 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 andElo.trackBrowserClosed(ad)when it closes. The SDK sends abrowser_openevent, then abrowser_closeevent with the time in between asdwell_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 beforeconfiguresend nothing. Both events are analytics only and never billed. Your debug observer receives them as the newEloTrackingEvent.BROWSER_OPENandBROWSER_CLOSEvalues, so an exhaustivewhenover 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,
EloAdViewposts 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 newEloTrackingEvent.VIEWABLEvalue. Declare it as App interactions on your Data safety form; see Privacy & Consent. - New
maxHeightandmaxWidthdescribe the ad slot, and the server picks the format.Elo.loadAd,Elo.preloadAd, andEloAdView(messages = ...)takemaxHeight: Dp?andmaxWidth: Dp?, bothnull(unbounded) by default. The SDK sends them as the request’sdimensionsin 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 = ...)andEloAdView(ad = ...)render that format, so there is nothing to choose on your side. The loading placeholder followsmaxHeight, and a preloaded ad is only served to a load with the samemaxHeightandmaxWidth.EloKeyboardBannerAdsends the strip’s 76dp height. Kotlin callers compiled against an earlier version need to recompile, because the default-argument methods ofloadAdandpreloadAdchanged; Java callers keep the sameloadAdandpreloadAdmethods. See Ad Formats. - Breaking
EloAdLayout, everylayoutparameter, andAdRenderOptions.layoutare removed. PassmaxHeight(andmaxWidth) for the slot instead. A slot that usedEloAdLayout.InlineBannerpasses its height, below 80dp; a chat-row card needs no argument. - Change: requests no longer send
display_position. The request’sdimensionsreplace it. - Breaking
Elo.trackRender,Elo.trackImpression, andElo.trackClickare removed.EloAdViewsends render, impression, and click tracking itself, so render ads withEloAdVieworEloKeyboardBannerAd.EloAdListenercallbacks are unchanged. - Breaking
AdTrackerandEloAd.copyare no longer public. The publicEloAdconstructor drops itstrackerandonReleaseparameters 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(), andElo.setRequestPayloadCaptureEnabled()are removed, along with theEloDiagnosticsSnapshot,EloDiagnosticsEntry,EloTrackingDiagnosticsEntry,EloTrackingTotals,EloIntegrationStatus, andEloMediationDebugSnapshottypes.Elo.setDebugObserver(observer, capturePayloads)replaces all of them: the SDK reports eachEloDebugEventas 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. PasscapturePayloads = trueto also receive the redacted request payload and decoded ad response thatsetRequestPayloadCaptureEnabled(true)used to enable. See Debug observer. - New
EloDebugEvent.AdOperationreports the slot and the ad’s format. It gainsformat,maxHeight,maxWidth,ctaEnabled,ctaVariant, andtapInset, eachnullwhen it does not apply. - Breaking AdMob mediation and the client-side auction are removed. The
ad.elo:elo-android-mediation-admobadapter (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, onNoFillorError, 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, andParallelAuctionMediatortypes are removed, as isEloConfiguration.adapters. AdResult.Loadedis nowLoaded(ad, eCpm). ThenetworkIdfield is gone because every fill is Elo’s.NoFillReason.AuctionTimeoutis renamedNoFillReason.Timeout(the request did not complete within the SDK’s deadline).NoBidsnow means the server returned no ad, andNetworkErrormeans the request failed with a transport or server error.- The
AuctionStarted,AdapterStartStarted,AdapterStartFinished,AdapterStartFailed,AdapterOutcome, andAuctionFinisheddebug events are removed, along withEloAdapterAuctionOutcome,EloAuctionOutcome, andEloErrorCategory.Adapter.EloAdLoadOutcome.Loadedcarries onlyeCpm, andEloDebugEvent.ConfiguredreportsrequestTimeoutMsinstead ofadapterIdsandauctionTimeoutMs.
- The
- Breaking The deprecated
Elo.initialize(...)is removed. It calledconfigureand has logged a deprecation warning since 0.1.9. UseElo.configure(context, publisherId, adUnitId), or build anEloConfigurationand callElo.configure(context, configuration)for COPPA/TFUA,logLevel, orbaseUrl. - Breaking The deprecated
EloAd.release()alias is removed. CallreleaseResources(), whichrelease()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.chromeandEloAdChrome.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.
EloAdStyleandChatMessagegain fields, which changes their constructors andcopymethods, and the default-argument methods ofloadAdandpreloadAdchanged. Kotlin and Java code compiled against an earlier version and calling them fails withNoSuchMethodErroruntil it is rebuilt; source code needs no change. - Change: the CTA label stays readable on a custom fill. When you set
callToActionBackgroundwithoutcallToActionForeground, the label is black or white, whichever contrasts more with your color, instead of the theme’sonPrimary.
- 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 reportscardforEloAdLayout.CompactHorizontal(the defaultEloAdViewcard) andbannerforEloAdLayout.InlineBanner(whatEloKeyboardBannerAdrenders), and a request made throughElo.loadAd/Elo.preloadAddirectly 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, andAdBidRequest.displayPositionis 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.
EloAdViewandEloKeyboardBannerAdnow send anad_tapevent (endpointsdk/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 aTAPtracking event, soEloTrackingDiagnosticsEntry.Eventhas a new value; an exhaustivewhenover 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.
- 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()andElo.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)).setUserIdentifierandsetUserDataare 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.badgeColornow 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 astartsWithon 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.colorandEloAdStyle.badgeColorstill 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.fontSizestill pins one size across both. - Breaking
sponsoredLabeltakes anEloAdDisclosure. The new value carries the disclosure copy plus an optionalfontSize,fontWeight,color, andtestTag, 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:EloAdViewdraws 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.sponsoredLabelis unchanged and still aString: 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
callToActionLabelis gone. Call-to-action copy is backend-managed now: it arrives on the creative asEloAd.ctaTextand 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.callToActionLabeland its deprecatedctaLabelalias are removed with it. - Breaking
openLinkAccessibilityLabelis 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 byEloAdStyle.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.
EloKeyboardBannerAdshows its loading skeleton by default.showLoadingPlaceholdernow defaults totrue, matchingEloAdView. 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. Passfalseto 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
setUserIdentifierand the data fromsetUserDataare stored as one record, sealed with AES-GCM under a non-exportable Android Keystore key and kept as ciphertext in the SDK’s ownSharedPreferences, 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
EloAdgainedctaTextin its primary constructor, which moves the JVM descriptors of that constructor and ofcopy; the previous descriptors are retained as hidden shims, so existing adapter bytecode still links across that change. TheAdRenderOptionsremovals carry no such shim — see the breaking-changes callout.
- Fix
EloAdListener.onAdDidTrackImpressionnow 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 letsEloDiagnosticsSnapshot.trackingTotalsreport a non-zerofailedcount.onAdDidReceiveClickis unchanged: it fires on the tap, before the click URL opens. - Fix Calling
Elo.trackRender(ad)orElo.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 noonAdDidTrackImpressioncallback. - 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 aftershutdown) 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.trackingEntriesis 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. EachEloTrackingTotalscarriesattempted,delivered,failed,unobservable, andinFlight, is never evicted, and appears inasExportableText(). Cleared on configure andshutdown(), 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.
EloAdViewand 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
EloAdconstructor now takes bothidandcreativeId, andidmust be unique per fill. Pass your network’s per-response id if it has one, or mint aUUID.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.
- New
Elo.setUserIdentifier(identifier): apps with a sign-in can send their own user ID as thevisitor_idon 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 passnullon 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
.iconow 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 theElotag, 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.
- The ad disclosure now leads the attribution line:
Ad • Headline(previouslyHeadline · Sponsored). Leading the disclosure makes it structurally impossible for a long headline to truncate it away; the separator is now•andsponsoredLabel’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 • Headlinedisclosure order.
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. TheDevice-NameandSystem-VersionHTTP headers are removed, and theUser-Agentis now built from real device info with the actual SDK version. - Renamed for iOS parity (old names deprecated, not removed):
EloAd.release()→EloAd.releaseResources(), andEloAdView’sctaLabelparameter →callToActionLabel. - New public
EloAdLayoutenum (CompactHorizontal/InlineBanner) selected via the ad views’layoutparameter, plus CTA color style tokensEloAdStyle.callToActionBackground/callToActionForegroundfor renderer-backed fills. - Ad requests and same-origin tracking pings now carry the
X-Elo-Statesession 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
trackRenderfor the same ad. - Fixed a native-resource leak:
EloAdView(messages)andEloKeyboardBannerAdnow release the owned ad when they leave composition (previously adapter-backed ads such as AdMobNativeAdleaked 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. AdCreativedecoding is lenient:source,offer_type, andformatare 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/tfuaconfigurations, regardless ofshareGeoLocation. - 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. TheshareGeoLocationopt-out covers coordinates only. - Elo-rendered creatives drop the CTA pill; the whole card is tappable.
callToActionLabelnow only affects renderer-backed mediation fills, andEloAdLoadingView(showCtaPlaceholder:)is ignored. - The host-app privacy guide (
PRIVACY.md) now ships in the repository, linked from the README. Elo.initializeis deprecated:Elo.configureis the single entry point, with a newconfigure(context, publisherId, adUnitId, shareGeoLocation, geoLocationPrecision)convenience for Elo-only integrations. Migrating is a rename for most apps;coppa/tfuamove ontoEloConfiguration.MessageRoledocuments the human-to-human mapping: device owner →USER, the other participant →ASSISTANT.