Skip to content

Android IntegrationANDROID SDK

Environment Minimum Requirement
Android Studio Android Studio Giraffe (2023.1) or later
Gradle / AGP Gradle 9.4.1 / Android Gradle Plugin 9.2.x
JDK 17 (minimum for AGP 9.2.x; SDK source compatibility is Java 11)
compileSdkVersion 36 (Android API 36 / 36.1 platform required)
Android SDK Build Tools 36.0.0
minSdkVersion 24 (Android 7.0)
targetSdkVersion 36 recommended; not enforced at build time—set according to your app’s release policy
Language Kotlin or Java (examples use Kotlin)

Before integrating the SDK, you need a HeheBid Platform developer account. Register your app, create Placement IDs, and obtain the required credentials: App Key and Placement ID.

App Key: A unique identifier assigned to your app. You can obtain it after creating the app in the developer console.

Placement ID: A unique identifier for each ad slot. You can create multiple Placement IDs. View all Placement IDs under Traffic Management → Ad Slot Management.


Public distribution is GitHub hehesdk/android-release + JitPack (prebuilt AARs).
Use a published git tag for the version (latest public: 0.2.6).

Section titled “Option 1: Remote Maven Dependency (Recommended)”

If your project uses Gradle dependencyResolutionManagement, configure repositories in settings.gradle.kts:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Add the remote dependency in your App module’s build.gradle.kts:

dependencies {
// Module artifact under the android-release repo — one colon before the version
implementation("com.github.hehesdk.android-release:hehesdk:0.2.6")
// Media3 is not declared in the remote POM. Add it explicitly if you want the SDK
// to prefer Media3 for video ads. Without it, the SDK falls back to system MediaPlayer.
implementation("androidx.media3:media3-exoplayer:1.10.1")
}

Copy the release artifact hehesdk-release.aar into your app’s app/libs/ directory (provided by Hehe, or built via :hehesdk:assembleRelease).

In your App module’s build.gradle.kts:

dependencies {
implementation(files("libs/hehesdk-release.aar"))
// Flat AARs do not transitively resolve dependencies — declare them explicitly
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.core:core-ktx:1.18.0")
implementation("com.google.android.material:material:1.13.0")
// Optional: add when the host needs the Media3 playback path
implementation("androidx.media3:media3-exoplayer:1.10.1")
}

As an Android Library, the SDK automatically declares the following permissions in the merged Manifest. Integrators generally do not need to add them again (if stripped by tooling, ensure they are retained):

Permission Purpose
android.permission.INTERNET Network requests
android.permission.ACCESS_NETWORK_STATE Network state
android.permission.READ_PHONE_STATE Device identifier (collected per protocol)
android.permission.READ_BASIC_PHONE_STATE Device identifier (API 33+)
com.google.android.gms.permission.AD_ID Google Advertising ID (GAID)

When integrating via a mediation adapter (AdMob / MAX / TopOn / TradPlus), also set the App Key in Manifest (the adapter auto-inits Hehe — do not call AdSDK.init again):

<meta-data
android:name="com.hehessp.sdk.APP_KEY"
android:value="YOUR_HEHE_APP_KEY" />

For direct SDK APIs on this page (BannerAd / InterstitialAd, etc.), pass the App Key via AdConfig instead.


After SDK integration, you must call AdSDK.init before loading any ads. We recommend calling it in Application.onCreate or in the first Activity.onCreate that uses ads.

import com.hehessp.sdk.AdSDK
import com.hehessp.sdk.InitCallback
import com.hehessp.sdk.core.config.AdConfig
val config = AdConfig(appKey = "YOUR_APP_KEY")
AdSDK.init(applicationContext, config, object : InitCallback {
override fun onSuccess() {
// Server init request succeeded; endpoint, sdk_enabled, etc. are applied internally
// Call loadAd after this, or when getServerInitState() == SUCCESS
}
override fun onFailure(code: Int, message: String) {
// Init failed: see Error Codes for code; message is the error constant name (e.g. NETWORK_TIMEOUT)
}
})
val info = AdSDK.getVersionInfo()
val name = info.sdkName // e.g. "hehessp"
val version = info.sdkVersion // aligned with the release tag, e.g. "0.2.6"
  • Create a Banner ad slot in the console and obtain the placementId.
  • Complete AdSDK.init.
  • Reserve an ad container in your layout (e.g. FrameLayout).

Step 1: Create a BannerAd Instance with Callbacks

Section titled “Step 1: Create a BannerAd Instance with Callbacks”
private var bannerAd: BannerAd? = null
// In Activity
val container = findViewById<FrameLayout>(R.id.banner_root)
bannerAd = BannerAd(
activity = this,
container = container,
config = BannerAdConfig(
placementId = "YOUR_BANNER_PLACEMENT_ID"
),
listener = object : BannerAdListener {
override fun onAdLoaded() {
// Ad loaded and automatically attached to the container
}
override fun onAdLoadFailed(code: Int, message: String) {
// Load failed
}
override fun onAdDisplayed(ad: Ad) {
// Ad displayed and impression recorded
}
override fun onAdDisplayFailed(code: Int, message: String) {
// Display failed
}
override fun onAdClicked(ad: Ad) {
// Click
}
override fun onAdHidden(ad: Ad) {
// Closed (if creative supports close, or destroy triggers session release)
}
}
)
bannerAd?.loadAd()

Banner size rules:

  • The SDK prefers w/h (in dp) from init placements that match the ad response placementId.
  • After converting w/h to px, the SDK sets a fixed width/height on the ad View. With a FrameLayout container, the ad View is horizontally centered and is not scaled to the host container width.
  • If init does not provide a valid size, the SDK falls back to BannerAdConfig.bannerSize. Default is SMALL=320x50; also supports MEDIUM=320x100 and LARGE=300x200.
  • bannerSize is also used for the request’s container_width/container_height (primarily for creative selection). Integrators usually do not need to pass it explicitly.

Layout example (use FrameLayout for banner_root; width fills the available area, height wraps the fixed-size ad View mounted by the SDK):

<FrameLayout
android:id="@+id/banner_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />

Ensure the page can accommodate the dp width/height configured for that Banner placement in the console. The current SDK does not automatically shrink an ad View that exceeds the container.

Release resources in Activity.onDestroy to avoid memory leaks:

override fun onDestroy() {
bannerAd?.destroy()
bannerAd = null
super.onDestroy()
}

  • Create an Interstitial ad slot in the console and obtain the placementId.
  • Complete AdSDK.init.
private var interstitialAd: InterstitialAd? = null
InterstitialAd.load(
placementId = "YOUR_INTERSTITIAL_PLACEMENT_ID",
listener = object : InterstitialAdListener {
override fun onAdLoaded(ad: InterstitialAd) {
// Ad loaded; save the instance and call show(activity) at the right time
interstitialAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// Load failed (including video/playable prefetch failure)
}
override fun onAdDisplayed(ad: Ad) {
// Ad displayed and impression recorded
}
override fun onAdDisplayFailed(code: Int, message: String) {
// Display failed
}
override fun onAdClicked(ad: Ad) {
// Click
}
override fun onAdHidden(ad: Ad) {
// User closed interstitial; finish Activity or preload the next ad here
}
}
)

Java integration example:

InterstitialAd.load("YOUR_INTERSTITIAL_PLACEMENT_ID", new InterstitialAdListener() {
@Override
public void onAdLoaded(@NonNull InterstitialAd ad) {
interstitialAd = ad;
}
// Override other callbacks as needed
});

After receiving the instance in onAdLoaded, or when the user triggers display:

val ok = interstitialAd?.show(this) ?: false
if (!ok) {
// Not ready, destroyed, expired, etc.; failure reason is also returned via onAdDisplayFailed
}

You can also check interstitialAd?.isReady() before showing.

After a successful show(activity), the current ad is consumed and isReady() returns false. To show another interstitial, call InterstitialAd.load(...) again. Prefer passing the current page Activity. For SDK-managed full-screen interstitials, show returns false if the Activity is already destroyed. If the Activity is finishing but not yet destroyed, the SDK still attempts to launch the full-screen host; launch failures are reported via onAdDisplayFailed.

The SDK automatically calls destroy() after terminal callbacks such as onAdLoadFailed, onAdDisplayFailed, and onAdHidden. No manual destroy is needed after those states.

Manually call destroy() as a fallback in these cases:

  • You received an instance in onAdLoaded but the user leaves the page before show
  • The host Activity.onDestroy runs while still holding an InterstitialAd reference—handle carefully to avoid memory leaks
override fun onDestroy() {
interstitialAd?.destroy()
interstitialAd = null
super.onDestroy()
}

Note: destroy() does not forcibly close an open full-screen interstitial Activity. Typically, finish the current page after onAdHidden and call InterstitialAd.load(...) again to load the next ad.


  • Create a Native ad slot in the console and obtain the placementId.
  • Complete AdSDK.init.
  • Use HeHeNativeContainer as the native ad root container in your layout, and declare required widgets with the SDK’s fixed ids.
private var nativeAd: NativeAd? = null
NativeAd.load(
placementId = "YOUR_NATIVE_PLACEMENT_ID",
listener = object : NativeAdListener {
override fun onAdLoaded(ad: NativeAd) {
// Ad loaded; save the instance and call show(activity, container) at the right time
nativeAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// Load failed (including icon/image asset prefetch failure)
}
override fun onAdDisplayed(ad: Ad) {
// Container visible and impression recorded
}
override fun onAdDisplayFailed(code: Int, message: String) {
// Display failed (e.g. required View missing, incomplete native_layout)
}
override fun onAdClicked(ad: Ad) {
// Full-card click
}
}
)

Native ads use a fixed XML template: you customize the layout and styling inside HeHeNativeContainer, while the SDK handles request, asset prefetch, View binding, impression, click, and landing-page navigation.

Layout example (root must be HeHeNativeContainer; child Views use SDK fixed ids):

<com.hehessp.sdk.ad.nativead.HeHeNativeContainer
android:id="@+id/native_root"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/hehe_native_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/hehe_native_icon"
android:layout_width="100dp"
android:layout_height="100dp" />
<TextView
android:id="@+id/hehe_native_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/hehe_native_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true" />
<TextView
android:id="@+id/hehe_native_cta"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<RatingBar
android:id="@+id/hehe_native_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/ratingBarStyleSmall" />
</LinearLayout>
</com.hehessp.sdk.ad.nativead.HeHeNativeContainer>

View id contract (ids are defined in the SDK’s res/values/ids.xml; your layout must use the same names):

View id Type Required
hehe_native_title TextView Required
hehe_native_icon ImageView Required
hehe_native_cta TextView / Button Required
hehe_native_desc TextView Optional
hehe_native_image ImageView Optional
hehe_native_rating TextView / RatingBar Optional
  • If a required View is missing or has the wrong type, show() returns false and callbacks onAdDisplayFailed(code=1210, message="RENDER_FAIL").
  • Missing optional Views do not cause errors; the SDK hides a View when the corresponding asset is empty.

In onAdLoaded or when the user triggers display, call:

val container = findViewById<HeHeNativeContainer>(R.id.native_root)
val ok = nativeAd?.show(this, container) ?: false
if (!ok) {
// Not ready, destroyed, expired, invalid Activity, etc.; failure reason is also returned via onAdDisplayFailed
}

You can also check nativeAd?.isReady() before showing.

Display notes:

  • show(activity, container) must receive a valid, non-destroyed Activity for landing-page navigation. Do not use ApplicationContext or FLAG_ACTIVITY_NEW_TASK for navigation.
  • Orientation is resolved automatically from the SDK’s appContext at init; you do not need to pass it.
  • The SDK prefers the legacy native fields and is compatible with server native_layout. title, icon.url, and cta_text are required assets. For older versions, missing cta_text still displays "OK". If image.url is present, it must be available at load time, otherwise onAdLoadFailed is called.
  • Impression triggers onAdDisplayed once after the container is attached, visible, and getGlobalVisibleRect() has a visible area.
  • The full-card HeHeNativeContainer is clickable; clicks trigger onAdClicked and landing-page navigation.
  • The SDK automatically mounts an ad disclosure badge at the bottom-left of the container.
  • Unlike interstitial, the same NativeAd instance can call show() multiple times (show does not consume the creative); impression is reported only once.

Mediation Platforms & Custom Rendering API

Section titled “Mediation Platforms & Custom Rendering API”

The legacy NativeAd.load(placementId, listener) behavior is unchanged. When you need independent assets, use the request-options overload:

NativeAd.load(
placementId = "YOUR_NATIVE_PLACEMENT_ID",
options = NativeAdRequestOptions(
shouldReturnUrlsForImageAssets = false
),
listener = object : NativeAdListener {
override fun onAdLoaded(ad: NativeAd) {
val assets = ad.getAssets() ?: return
val adChoicesView = ad.createAdChoicesView(this@MainActivity) ?: return
// Bind assets with your external View system; notify the SDK only after real events.
bindAssets(assets, adChoicesView)
}
}
)

NativeAdAssets fields:

Field Description
headline Required title
body Optional description; null when blank
callToAction CTA; falls back to legacy "OK" when missing
icon Required image asset
mainImage Optional main image; currently at most one
starRating Non-null only when finite and in (0, 5]; current 0 is treated as not provided
advertiser/store/price Not confirmed in the current protocol; returns null (no guessing)

Image assets include material uri, optional bitmap, width/height, and scale=1.0:

  • Default shouldReturnUrlsForImageAssets=false returns a prefetched Bitmap; callers must not mutate or recycle it.
  • When set to true, only URI is returned and Bitmap is null; the current path still reuses the legacy prefetch pipeline, and you must ensure CDN URL validity covers the ad lifetime.
  • shouldRequestMultipleImages and preferredMediaAspectRatio are not supported by the backend today; they are safely ignored (no image duplication or cropping).

After your external View system detects events, call on the main thread:

val impressionAccepted = nativeAd?.recordImpression() == true
val clickAccepted = nativeAd?.performClick(currentActivity) == true
  • Impression shares the same one-shot gate as legacy show(); the first valid call returns true.
  • Click reuses the SDK’s original tracking and landing flow; invalid, finishing, or destroyed Activity returns false, and the SDK does not retain the Activity.
  • createAdChoicesView(context) returns a new unmounted View each time; returns null when state is invalid, not on the main thread, or creation fails.
  • After destroy(), assets, impression, click, and disclosure View creation all become unavailable; previously obtained Bitmaps must not be used further.

Common show failure codes:

code Scenario
1200 Ad expired
1201 Ad instance already destroyed
1202 show called before a successful load
1203 Host Activity is finishing or destroyed
1210 Render failure (missing required View, missing native_layout, asset binding failure, etc.)

Common load failure codes:

code Scenario
1113 Native required fields, asset URI, or prefetched image invalid

After onAdLoadFailed, the SDK automatically calls destroy(). onAdDisplayFailed does not auto-destroy; release as needed or call NativeAd.load(...) again.

Manually call destroy() as a fallback in these cases:

  • You received an instance in onAdLoaded but the user leaves the page and no longer needs display
  • The host Activity.onDestroy runs while still holding a NativeAd reference—avoid memory leaks
override fun onDestroy() {
nativeAd?.destroy()
nativeAd = null
super.onDestroy()
}

Note: From NativeAd.load() until destroy(), the SDK holds the listener. Prefer a static inner class or a standalone class for NativeAdListener, and access the Activity via WeakReference, to avoid anonymous inner classes capturing the Activity and causing leaks.

  • Create a Rewarded Video ad slot in the console (placement_type=3) and obtain the placementId.
  • Complete AdSDK.init.
private var rewardAd: RewardAd? = null
RewardAd.load(
placementId = "YOUR_REWARDED_PLACEMENT_ID",
listener = object : RewardAdListener {
override fun onAdLoaded(ad: RewardAd) {
// Ad loaded; save the instance and call show(activity) at the right time
rewardAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// Load failed (including video asset prefetch failure)
}
override fun onAdDisplayed(ad: Ad) {
// Ad displayed and impression recorded
}
override fun onAdDisplayFailed(code: Int, message: String) {
// Display failed
}
override fun onAdClicked(ad: Ad) {
// Click
}
override fun onAdHidden(ad: Ad) {
// User closed the ad; finish Activity or preload the next ad here
}
override fun onRewardEarned(ad: Ad, reward: RewardItem) {
// Reward earned: reward.name / reward.amount
}
}
)

After receiving the instance in onAdLoaded, or when the user triggers display:

val ok = rewardAd?.show(this) ?: false
if (!ok) {
// Not ready, destroyed, expired, etc.; failure reason is also returned via onAdDisplayFailed
}

You can also check rewardAd?.isReady() before showing. After a successful show(activity), the current ad is consumed and isReady() returns false. To show another rewarded video, call RewardAd.load(...) again. Prefer passing the current page Activity. For SDK-managed full-screen rewarded ads, show returns false if the Activity is already destroyed. If the Activity is finishing but not yet destroyed, the SDK still attempts to launch the full-screen host; launch failures are reported via onAdDisplayFailed.

The SDK automatically calls destroy() after terminal callbacks such as onAdLoadFailed, onAdDisplayFailed, and onAdHidden. No manual destroy is needed after those states. onRewardEarned does not trigger auto-destroy; the ad is destroyed by the SDK after close via onAdHidden.

Manually call destroy() as a fallback in these cases:

  • You received an instance in onAdLoaded but the user leaves the page before show
  • The host Activity.onDestroy runs while still holding a RewardAd reference—handle carefully to avoid memory leaks
override fun onDestroy() {
rewardAd?.destroy()
rewardAd = null
super.onDestroy()
}

Reward and close-button notes:

  • The server may send reward.reward_time (seconds): after video playback starts, the SDK triggers the reward flow when that offset is reached.
  • When reward.verify is non-empty, the SDK sends an unsigned GET to that URL and treats code==0 in the unified response body as success before calling onRewardEarned. When verify is empty, verification is skipped and the callback fires directly. Verification failure / network errors do not callback.
  • The close button is delayed by render_control.close_offset_secs. Closing before reward_time shows an “abandon reward” confirmation; after the offset, close is immediate. The dialog only checks whether reward_time has elapsed—verify success/failure only affects whether onRewardEarned is called.
  • From RewardAd.load() until destroy(), the SDK holds the listener. Prefer a static inner class or WeakReference to the Activity to avoid anonymous inner classes capturing the Activity and causing leaks.

All public failure callbacks currently return the error constant name string as message. Prefer code for business logic and message for logging/troubleshooting.

code message Scenario
1000 UNKNOWN Unknown error
1001 APP_KEY_EMPTY AdConfig.appKey is empty
1002 INVALID_PARAMS Other AdConfig parameters are invalid
1003 ALREADY_INITIALIZED Duplicate AdSDK.init call
1010 NETWORK_UNAVAILABLE No network, DNS failure, or host unreachable
1011 NETWORK_TIMEOUT Network request timed out
1012 NETWORK_ERROR Other network I/O failure
1020 RESPONSE_PARSE_ERROR SSP response JSON or model parse failure
code message Scenario
1100 SDK_NOT_INITIALIZED Ad requested before initialization
1101 SDK_DISABLED Server init returned sdk_enabled=false
1102 PLACEMENT_ID_EMPTY Placement ID is empty
1103 API_CLIENT_UNAVAILABLE Internal ad request client unavailable
1110 MATERIAL_TIMEOUT Required asset prefetch timed out
1111 MATERIAL_DOWNLOAD_FAIL Required asset download failed
1112 MATERIAL_EXTRACT_FAIL Playable zip extract or entry HTML locate failed
1113 NATIVE_ASSET_INVALID Native required fields, asset URI, or prefetched image invalid
code message Scenario
1200 AD_EXPIRED Ad expired
1201 AD_DESTROYED Ad instance already destroy()ed
1202 AD_NOT_READY Display called before a successful load
1203 HOST_ACTIVITY_DESTROYED Host Activity cannot be used for this display; interstitial/rewarded only reject already destroyed; Native and other host-View entry points also reject finishing
1210 RENDER_FAIL Render failure or display host launch failure
code message Scenario
3000 SSP_ERROR Generic server error; unknown 3xxx business codes are normalized to this
3001 NO_AD_FILL No ad fill
3002 INVALID_REQUEST Request body parse error
3003 INVALID_APP_KEY App delisted or does not exist
3004 INVALID_PLACEMENT Placement closed or does not exist
3005 INVALID_SIGNATURE Signature failure
3006 FREQUENCY_LIMITED Frequency capped
3007 REQUEST_EXPIRED X-Timestamp differs too much from server time
3008 REPLAY_ATTACK_DETECTED Server detected duplicate X-Nonce
3009 RATE_LIMIT_EXCEEDED Server rate limited
3010 INVALID_PLACEMENT_TYPE Placement type not supported by this SDK version
5000 SERVER_INTERNAL_ERROR SSP ad server internal error

The current version does not provide public APIs for the following ad types. Do not call non-existent classes based on other SDK documentation:

Type Status
Splash Planned