跳转到内容

Android 集成Android

环境 最低要求
Android Studio Android Studio Giraffe (2023.1)及以上版本
Gradle / AGP Gradle 9.4.1 / Android Gradle Plugin 9.2.x
JDK 17(AGP 9.2.x 的最低要求;SDK 源码兼容级别为 Java 11)
compileSdkVersion 36(需安装 Android API 36 / 36.1 平台)
Android SDK Build Tools 36.0.0
minSdkVersion 24(Android 7.0)
targetSdkVersion 建议 36;构建不强制,按接入 App 的发布策略设置
语言 Kotlin 或 Java(示例为 Kotlin)

前置准备(应用 AppKey 与广告位 ID)

Section titled “前置准备(应用 AppKey 与广告位 ID)”

在进行SDK集成之前,您需要先有一个HeheBid Platform 的开发者账号,然后注册您的应用、创建广告位ID (Placement ID)等操作,得到集成所必要的信息:App Key 和 Placement ID。

App Key: App Key 是给开发者应用分配的唯一标识,在开发者前台创建应用后即可获取App Key。

Placement ID: 是广告位ID (Placement ID)的唯一标识。开发者可以创建多个广告位ID (Placement ID),进入菜单【流量管理】 > 【广告位管理】可以查看到所有的广告位ID (Placement ID)信息。


对外分发通道为 GitHub 公开仓 hehesdk/android-release + JitPack(预构建 AAR)。
将下方示例中的 VERSION 替换为实际 git tag(当前公开最新:0.2.6)。

方式一:远端 Maven 依赖(推荐)

Section titled “方式一:远端 Maven 依赖(推荐)”

若接入工程使用 Gradle dependencyResolutionManagement,可在 settings.gradle.kts 中配置:

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

在 App 模块 build.gradle.kts 中添加远端依赖:

dependencies {
// 注意:artifact 为 android-release 仓下的模块名 hehesdk,中间只有一个冒号分隔版本
implementation("com.github.hehesdk.android-release:hehesdk:0.2.6")
// 远端 POM 未声明 Media3;如需让 SDK 优先使用 Media3 播放视频广告,由宿主显式添加。
// 不添加时 SDK 会自动降级为系统 MediaPlayer 播放路径。
implementation("androidx.media3:media3-exoplayer:1.10.1")
}

将发版产物 hehesdk-release.aar 复制到接入工程的 app/libs/(可由 Hehe 提供,或自行从 SDK 工程 :hehesdk:assembleRelease 产出)。

在 App 模块 build.gradle.kts 中:

dependencies {
implementation(files("libs/hehesdk-release.aar"))
// flat AAR 不会传递依赖,需由宿主显式声明
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")
// 可选:宿主需要 Media3 播放路径时添加
implementation("androidx.media3:media3-exoplayer:1.10.1")
}

SDK 作为 Android Library,会在合并后的 Manifest 中自动声明以下权限,接入方一般无需重复添加(若被工具剥离,请确保保留):

权限 用途
android.permission.INTERNET 网络请求
android.permission.ACCESS_NETWORK_STATE 网络状态
android.permission.READ_PHONE_STATE 设备标识相关(按协议采集)
android.permission.READ_BASIC_PHONE_STATE 设备标识相关(API 33+)
com.google.android.gms.permission.AD_ID 获取 Google Advertising ID(GAID)

若通过 中介 Adapter(AdMob / MAX / TopOn / TradPlus)接入,还需在 Manifest 配置 App Key(由 Adapter 自动初始化,宿主不要再调 AdSDK.init):

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

直连本页 API(BannerAd / InterstitialAd 等)时,App Key 通过下方 AdConfig 传入即可。


完成 SDK 集成后,在加载任何广告之前必须调用 AdSDK.init。建议在 Application.onCreate 或首个使用广告的 Activity.onCreate 中执行。

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() {
// 服务端 init 接口请求成功;endpoint、sdk_enabled 等由 SDK 内部应用
// 建议此时或 getServerInitState() == SUCCESS 后再 loadAd
}
override fun onFailure(code: Int, message: String) {
// 初始化失败:code 见「错误码说明」;message 为错误码常量名(如 NETWORK_TIMEOUT)
}
})
val info = AdSDK.getVersionInfo()
val name = info.sdkName // 如 "hehessp"
val version = info.sdkVersion // 与发版 tag 对齐,如 "0.2.6"
  • 已在后台创建 Banner 类型广告位,并取得 placementId。
  • 已完成 AdSDK.init。
  • 布局中预留广告容器(如 FrameLayout)。

Step 1:创建 BannerAd 对象并传入回调

Section titled “Step 1:创建 BannerAd 对象并传入回调”
private var bannerAd: BannerAd? = null
// 在 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() {
// 广告加载完成且已自动挂载到容器
}
override fun onAdLoadFailed(code: Int, message: String) {
// 加载失败
}
override fun onAdDisplayed(ad: Ad) {
// 广告已展示并产生曝光
}
override fun onAdDisplayFailed(code: Int, message: String) {
// 展示失败
}
override fun onAdClicked(ad: Ad) {
// 点击
}
override fun onAdHidden(ad: Ad) {
// 关闭(若创意支持关闭或 destroy 触发会话释放)
}
}
)
bannerAd?.loadAd()

Banner 尺寸规则:

  • SDK 优先使用 init placements 中与广告响应 placementId 匹配的 w/h,单位为 dp。
  • SDK 将 w/h 转为 px 后设置广告 View 的固定宽高;容器为 FrameLayout 时广告 View 水平居中,不按宿主容器宽度缩放。
  • init 未下发有效尺寸时,回退 BannerAdConfig.bannerSize;默认 SMALL=320x50,还支持 MEDIUM=320x100、LARGE=300x200。
  • bannerSize 同时用于广告请求的 container_width/container_height,主要表示请求选材尺寸;接入方通常无需显式传入。

布局示例(推荐 banner_root 使用 FrameLayout,宽度占满可用区域、高度随 SDK 添加的固定尺寸广告 View 撑开):

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

接入方应确保页面可容纳后台为该 Banner 广告位配置的 dp 宽高;当前 SDK 不会把超出容器的广告 View 自动缩小。

在 Activity.onDestroy 中释放,避免内存泄漏:

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

广告单元:插屏广告(Interstitial)

Section titled “广告单元:插屏广告(Interstitial)”
  • 已在后台创建 插屏 类型广告位,并取得 placementId。
  • 已完成 AdSDK.init。
private var interstitialAd: InterstitialAd? = null
InterstitialAd.load(
placementId = "YOUR_INTERSTITIAL_PLACEMENT_ID",
listener = object : InterstitialAdListener {
override fun onAdLoaded(ad: InterstitialAd) {
// 广告加载完成,保存实例并在合适时机 show(activity)
interstitialAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// 加载失败(含视频/试玩素材预下载失败)
}
override fun onAdDisplayed(ad: Ad) {
// 广告已展示并产生曝光
}
override fun onAdDisplayFailed(code: Int, message: String) {
// 展示失败
}
override fun onAdClicked(ad: Ad) {
// 点击
}
override fun onAdHidden(ad: Ad) {
// 用户关闭插屏,可在此 finish Activity 或预加载下一条
}
}
)

Java 接入示例:

InterstitialAd.load("YOUR_INTERSTITIAL_PLACEMENT_ID", new InterstitialAdListener() {
@Override
public void onAdLoaded(@NonNull InterstitialAd ad) {
interstitialAd = ad;
}
// 其它回调按需重写
});

在 onAdLoaded 中已拿到实例,或在用户触发时机对保存的实例调用:

val ok = interstitialAd?.show(this) ?: false
if (!ok) {
// 未就绪、已 destroy、已过期等;失败原因也会通过 onAdDisplayFailed 返回
}

展示前也可使用 interstitialAd?.isReady() 判断。

show(activity) 成功后当前广告会被消费,isReady() 会回到 false;如需再次展示插屏,请重新调用 InterstitialAd.load(...) 加载下一条广告。请优先传入当前页面 Activity;对于 SDK 自管全屏插屏,Activity 已 destroyed 时 show 会返回 false,Activity 正在 finishing 但尚未 destroyed 时 SDK 会继续尝试调起全屏载体,若启动失败会通过 onAdDisplayFailed 返回。

SDK 会在 onAdLoadFailed、onAdDisplayFailed、onAdHidden 等终态回调后自动调用 destroy(),接入方在终态后无需再手动销毁。

以下场景建议主动 destroy() 作兜底:

  • 已在 onAdLoaded 拿到实例,但用户离开页面且尚未 show
  • 宿主 Activity.onDestroy 时仍持有 InterstitialAd 引用,注意处理避免内存泄漏
override fun onDestroy() {
interstitialAd?.destroy()
interstitialAd = null
super.onDestroy()
}

说明: destroy() 不会强行关闭已打开的全屏插屏 Activity;一般在 onAdHidden 后结束当前页面,并重新 InterstitialAd.load(...) 加载下一条。


  • 已在后台创建 原生 类型广告位,并取得 placementId。
  • 已完成 AdSDK.init。
  • 布局中已使用 HeHeNativeContainer 作为原生广告根容器,并按 SDK 固定 id 声明必需控件。
private var nativeAd: NativeAd? = null
NativeAd.load(
placementId = "YOUR_NATIVE_PLACEMENT_ID",
listener = object : NativeAdListener {
override fun onAdLoaded(ad: NativeAd) {
// 广告加载完成,保存实例并在合适时机 show(activity, container)
nativeAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// 加载失败(含 icon/image 素材预下载失败)
}
override fun onAdDisplayed(ad: Ad) {
// 容器可见且产生曝光
}
override fun onAdDisplayFailed(code: Int, message: String) {
// 展示失败(如必需 View 缺失、native_layout 不完整等)
}
override fun onAdClicked(ad: Ad) {
// 整卡点击
}
}
)

原生广告采用固定 XML 模板方案:接入方自定义 HeHeNativeContainer 内部布局与样式,SDK 负责请求、素材预下载、View 绑定、曝光、点击与落地页跳转。

布局示例(根节点必须为 HeHeNativeContainer,子 View 使用 SDK 固定 id):

<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 契约(id 定义于 SDK 的 res/values/ids.xml,接入方布局中须使用相同名称):

View id 类型 是否必需
hehe_native_title TextView 必需
hehe_native_icon ImageView 必需
hehe_native_cta TextView / Button 必需
hehe_native_desc TextView 可选
hehe_native_image ImageView 可选
hehe_native_rating TextView / RatingBar 可选
  • 必需 View 缺失或类型错误时,show() 返回 false 并回调 onAdDisplayFailed(code=1210, message="RENDER_FAIL")。
  • 可选 View 缺失不报错;对应素材为空时 SDK 会隐藏该 View。

在 onAdLoaded 中或用户触发时机,对保存的实例调用:

val container = findViewById<HeHeNativeContainer>(R.id.native_root)
val ok = nativeAd?.show(this, container) ?: false
if (!ok) {
// 未就绪、已 destroy、已过期、Activity 无效等;失败原因也会通过 onAdDisplayFailed 返回
}

展示前也可使用 nativeAd?.isReady() 判断。

展示说明:

  • show(activity, container) 必须传入当前有效、未销毁的 Activity,用于落地页跳转;请勿使用 ApplicationContext 或 FLAG_ACTIVITY_NEW_TASK 方式跳转。
  • 横竖屏由 SDK 初始化时的 appContext 自动解析,接入方无需传入。
  • SDK 保留历史 native 字段优先,并兼容服务端 native_layout;title、icon.url、cta_text 为必需素材。为兼容旧版本,cta_text 缺失时仍显示 "OK"。image.url 若下发,加载阶段必须可用,否则回调 onAdLoadFailed。
  • 曝光在容器 attach、可见且 getGlobalVisibleRect() 有可见区域后触发一次 onAdDisplayed。
  • 整卡 HeHeNativeContainer 可点击,点击后触发 onAdClicked 与落地页跳转。
  • SDK 会在容器左下角自动挂载广告披露角标。
  • 与插屏不同,同一 NativeAd 实例可多次调用 show()(show 不消费素材);曝光仅上报一次。

旧 NativeAd.load(placementId, listener) 行为不变。需要独立资产时可使用请求选项重载:

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
// 资产由外部 View 系统绑定;确认真实事件后再通知 SDK。
bindAssets(assets, adChoicesView)
}
}
)

NativeAdAssets 字段如下:

字段 说明
headline 必需标题
body 可选描述,空白时为 null
callToAction CTA;缺失时保留旧 "OK" 兜底
icon 必需图片资产
mainImage 可选主图,当前最多一张
starRating 仅有限且在 (0, 5] 时非空;当前 0 按未提供处理
advertiser/store/price 当前协议未确认,返回 null,不猜值

图片资产包含素材 uri、可选 bitmap、宽高和 scale=1.0:

  • 默认 shouldReturnUrlsForImageAssets=false,返回预下载 Bitmap;调用方不得修改或 recycle。
  • 设为 true 时只返回 URI,Bitmap 为 null;当前仍会复用旧预下载链路,且需先确认 CDN URL 有效期覆盖广告有效期。
  • shouldRequestMultipleImages 与 preferredMediaAspectRatio 当前后端不支持,会安全忽略,不复制图片或裁切素材。

外部 View 系统检测到事件后,在主线程调用:

val impressionAccepted = nativeAd?.recordImpression() == true
val clickAccepted = nativeAd?.performClick(currentActivity) == true
  • 曝光与旧 show() 共用同一个一次性门,首次有效调用返回 true。
  • 点击复用 SDK 原 tracking 与 landing;无效、finishing 或 destroyed Activity 返回 false,SDK 不保存 Activity。
  • createAdChoicesView(context) 每次返回新的未挂载 View;状态无效、非主线程或创建失败时返回 null。
  • destroy() 后资产、曝光、点击和披露 View 创建入口均失效;已取得的 Bitmap 不得继续使用。

常见 show 失败码:

code 场景
1200 广告已过期
1201 广告实例已 destroy
1202 未 load 成功就 show
1203 宿主 Activity 已 finishing 或 destroyed
1210 渲染失败(缺必需 View、缺 native_layout、素材绑定失败等)

常见 load 失败码:

code 场景
1113 原生广告必需字段、素材 URI 或预下载图片无效

onAdLoadFailed 后 SDK 会自动调用 destroy()。onAdDisplayFailed 不会自动 destroy,接入方按需释放或重新 NativeAd.load(...)。

以下场景建议主动 destroy() 作兜底:

  • 已在 onAdLoaded 拿到实例,但用户离开页面且不再需要展示
  • 宿主 Activity.onDestroy 时仍持有 NativeAd 引用,注意避免内存泄漏
override fun onDestroy() {
nativeAd?.destroy()
nativeAd = null
super.onDestroy()
}

说明: NativeAd.load() 至 destroy() 期间 SDK 会持有 listener。建议优先使用 static 内部类 或独立类实现 NativeAdListener,通过 WeakReference 访问 Activity,避免匿名内部类捕获 Activity 导致泄漏。

广告单元:激励视频广告(Rewarded)

Section titled “广告单元:激励视频广告(Rewarded)”
  • 已在后台创建 激励视频 类型广告位(placement_type=3),并取得 placementId。
  • 已完成 AdSDK.init。
private var rewardAd: RewardAd? = null
RewardAd.load(
placementId = "YOUR_REWARDED_PLACEMENT_ID",
listener = object : RewardAdListener {
override fun onAdLoaded(ad: RewardAd) {
// 广告加载完成,保存实例并在合适时机 show(activity)
rewardAd = ad
}
override fun onAdLoadFailed(code: Int, message: String) {
// 加载失败(含视频素材预下载失败)
}
override fun onAdDisplayed(ad: Ad) {
// 广告已展示并产生曝光
}
override fun onAdDisplayFailed(code: Int, message: String) {
// 展示失败
}
override fun onAdClicked(ad: Ad) {
// 点击
}
override fun onAdHidden(ad: Ad) {
// 用户关闭广告,可在此 finish Activity 或预加载下一条
}
override fun onRewardEarned(ad: Ad, reward: RewardItem) {
// 激励发奖达成:reward.name / reward.amount
}
}
)

在 onAdLoaded 中已拿到实例,或在用户触发时机对保存的实例调用:

val ok = rewardAd?.show(this) ?: false
if (!ok) {
// 未就绪、已 destroy、已过期等;失败原因也会通过 onAdDisplayFailed 返回
}

展示前也可使用 rewardAd?.isReady() 判断。show(activity) 成功后当前广告会被消费,isReady() 会回到 false;如需再次展示激励视频,请重新调用 RewardAd.load(...) 加载下一条广告。请优先传入当前页面 Activity;对于 SDK 自管全屏激励广告,Activity 已 destroyed 时 show 会返回 false,Activity 正在 finishing 但尚未 destroyed 时 SDK 会继续尝试调起全屏载体,若启动失败会通过 onAdDisplayFailed 返回。

SDK 会在 onAdLoadFailed、onAdDisplayFailed、onAdHidden 等终态回调后自动调用 destroy(),接入方在终态后无需再手动销毁。onRewardEarned 不触发自动销毁,广告仍由 onAdHidden 关闭后由 SDK 自动销毁。

以下场景建议主动 destroy() 作兜底:

  • 已在 onAdLoaded 拿到实例,但用户离开页面且尚未 show
  • 宿主 Activity.onDestroy 时仍持有 RewardAd 引用,注意处理避免内存泄漏
override fun onDestroy() {
rewardAd?.destroy()
rewardAd = null
super.onDestroy()
}

发奖与关闭按钮说明:

  • 服务端下发 reward.reward_time(秒):视频开播后到点 SDK 触发发奖流程。
  • reward.verify 非空时,SDK 对该 URL 发 GET 校验(不签名),按统一响应体 code==0 判定通过、通过才回调 onRewardEarned;verify 为空时跳过校验直接回调;校验失败/网络异常不回调。
  • 关闭按钮按 render_control.close_offset_secs 延迟显示;reward_time 到点前点关闭会弹“放弃奖励”确认框,到点后直接关闭。弹窗只看 reward_time 是否到点,与 verify 成败无关;verify 成败只影响是否回调 onRewardEarned。
  • RewardAd.load() 至 destroy() 期间 SDK 会持有 listener,建议使用 static 内部类 或 WeakReference 访问 Activity,避免匿名内部类捕获 Activity 导致泄漏。

所有公开失败回调的 message 当前返回错误码常量名字符串,接入方应优先使用 code 做逻辑判断,使用 message 做日志排查。

code message 场景
1000 UNKNOWN 未知错误
1001 APP_KEY_EMPTY AdConfig.appKey 为空
1002 INVALID_PARAMS AdConfig 其它参数非法
1003 ALREADY_INITIALIZED 重复调用 AdSDK.init
1010 NETWORK_UNAVAILABLE 无网络、DNS 失败或主机不可达
1011 NETWORK_TIMEOUT 网络请求超时
1012 NETWORK_ERROR 其它网络 IO 失败
1020 RESPONSE_PARSE_ERROR SSP 响应 JSON 或模型解析失败
code message 场景
1100 SDK_NOT_INITIALIZED 未初始化就请求广告
1101 SDK_DISABLED 服务端 init 下发 sdk_enabled=false
1102 PLACEMENT_ID_EMPTY 广告位 ID 为空
1103 API_CLIENT_UNAVAILABLE SDK 内部广告请求客户端不可用
1110 MATERIAL_TIMEOUT 必要素材预下载超时
1111 MATERIAL_DOWNLOAD_FAIL 必要素材下载失败
1112 MATERIAL_EXTRACT_FAIL 试玩 zip 解压或入口 HTML 定位失败
1113 NATIVE_ASSET_INVALID 原生广告必需字段、素材 URI 或预下载图片无效
code message 场景
1200 AD_EXPIRED 广告已过期
1201 AD_DESTROYED 广告实例已 destroy()
1202 AD_NOT_READY 未加载成功就调用展示
1203 HOST_ACTIVITY_DESTROYED 宿主 Activity 不可用于本次展示;插屏/激励只拒绝已 destroyed,Native 等依赖宿主 View 的入口也会拒绝 finishing
1210 RENDER_FAIL 渲染失败或展示载体启动失败
code message 场景
3000 SSP_ERROR 服务端通用错误;未知 3xxx 业务码也会归一为该错误
3001 NO_AD_FILL 无广告填充
3002 INVALID_REQUEST 请求体解析错误
3003 INVALID_APP_KEY App 下架或不存在
3004 INVALID_PLACEMENT 广告位关闭或不存在
3005 INVALID_SIGNATURE 签名失败
3006 FREQUENCY_LIMITED 频次限制
3007 REQUEST_EXPIRED X-Timestamp 与服务器时间相差过大
3008 REPLAY_ATTACK_DETECTED 服务端检测到重复 X-Nonce
3009 RATE_LIMIT_EXCEEDED 服务器限流
3010 INVALID_PLACEMENT_TYPE 广告位类型本 SDK 版本不支持
5000 SERVER_INTERNAL_ERROR SSP 广告服务器内部异常

当前版本尚未提供以下广告类型的公开 API,请勿按其他 SDK 文档自行调用不存在的类:

类型 状态
开屏(Splash) 规划中