Files
assistant-android/app/src/main/java/com/gh/ad/AdDelegateHelper.kt
叶子维 03bbcc2af3 Merge branch 'feat/GHZS-4955' into 'dev'
feat: 开屏广告神策埋点补充—客户端 https://jira.shanqu.cc/browse/GHZS-4955

See merge request halo/android/assistant-android!1587
2024-03-25 15:36:04 +08:00

683 lines
24 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.gh.ad
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Message
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import com.alibaba.android.arouter.launcher.ARouter
import com.facebook.drawee.view.SimpleDraweeView
import com.gh.common.exposure.ExposureManager
import com.gh.common.util.DirectUtils.directToLinkPage
import com.gh.common.util.LogUtils
import com.gh.common.util.NewFlatLogUtils.logOpenScreenAdSkip
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.BuildConfig
import com.gh.gamecenter.MainActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.common.exposure.ExposureSource
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.provider.IBeiziAdProvider
import com.gh.gamecenter.core.provider.ICsjAdProvider
import com.gh.gamecenter.core.utils.CurrentActivityHolder
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.core.utils.TimeUtils.getToday
import com.gh.gamecenter.entity.AdConfig
import com.gh.gamecenter.entity.StartupAdEntity
import com.gh.gamecenter.feature.exposure.ExposureEvent.Companion.createEvent
import com.gh.gamecenter.feature.exposure.ExposureType
import com.gh.gamecenter.retrofit.RetrofitManager
import com.halo.assistant.HaloApp
import io.reactivex.schedulers.Schedulers
/**
* 广告实现代理类
*
* 由它来分发功能实现到具体的实现
*
* 以最复杂的开屏广告为例有三种实现1. 自有的广告实现 2. 穿山甲的开屏广告实现 3. Beizi 的开屏广告实现)
*
* 由于两个广告 SDK 有可能在一次启动中都被使用,所以会根据获取到的广告配置 config 来决定是否需要出是很好两个 SDK
*/
object AdDelegateHelper {
private var mCsjAdImpl: ICsjAdProvider? = null
private var mBeiziAdImpl: IBeiziAdProvider? = null
private val mAdConfigList: ArrayList<AdConfig> by lazy { arrayListOf() }
private var mSplashAd: AdConfig? = null
private var mDownloadManagerAd: AdConfig? = null
private val mGameSearchAdList: ArrayList<AdConfig> by lazy { arrayListOf() }
private var mVGameLaunchAd: AdConfig? = null
val vGameLaunchAd: AdConfig?
get() = mVGameLaunchAd
private const val AD_SDK_CSJ = "穿山甲"
private const val AD_SDK_BEIZI = "倍孜"
const val AD_TYPE_SDK = "third_party_ads" // 第三方 SDK 广告
const val AD_TYPE_OWNER = "owner_ads" // 自有广告
private const val KEY_CACHE_CONFIG = "cache_config" // 放在 SP 里的广告缓存(避免接口加载问题)
private val mAdConfigSp: SharedPreferences by lazy {
HaloApp.getInstance().getSharedPreferences("AdConfig", Context.MODE_PRIVATE)
}
var isShowingSplashAd = false // 是否正在显示开屏广告
var gameSearchKeyword = ""
fun initAdSdk(context: Context) {
// 初始化 Beizi
if (mBeiziAdImpl == null) {
mBeiziAdImpl =
ARouter.getInstance().build(RouteConsts.provider.beiziAd).navigation() as? IBeiziAdProvider
mBeiziAdImpl?.initSDK(context)
}
// 初始化穿山甲
if (mCsjAdImpl == null) {
mCsjAdImpl =
ARouter.getInstance().build(RouteConsts.provider.csjAd).navigation() as? ICsjAdProvider
val csjAppId = if (EnvHelper.isDevEnv) BuildConfig.DEV_CSJ_APPID else BuildConfig.CSJ_APPID
mCsjAdImpl?.initSDK(context, csjAppId, HaloApp.getInstance().oaid)
// 监听亮色/暗色模式切换
DarkModeUtils.registerModeChangeListener {
val topActivity = CurrentActivityHolder.getCurrentActivity() ?: return@registerModeChangeListener
updateThemeStatus(context, DarkModeUtils.isDarkModeOn(topActivity))
}
}
}
/**
* 请求接口获取广告相关配置
*/
@SuppressLint("CheckResult")
fun requestAdConfig(isFromRetry: Boolean, keyword: String = "", callback: (() -> Unit)? = null) {
// mAdConfigList 不为空不需要重试
if (isFromRetry && mAdConfigList.isNotEmpty()) {
return
}
val paramsMap = if (keyword.isNotEmpty()) mapOf("keyword" to keyword) else mapOf()
RetrofitManager.getInstance()
.newApi
.getAdConfig(paramsMap)
.subscribeOn(Schedulers.io())
.subscribe(object : BiResponse<List<AdConfig>>() {
override fun onSuccess(data: List<AdConfig>) {
gameSearchKeyword = keyword
handleAdConfig(data)
callback?.invoke()
// 缓存数据到 SP 供接口请求失败用
SPUtils.setString(mAdConfigSp, KEY_CACHE_CONFIG, data.toJson())
}
override fun onFailure(exception: Exception) {
super.onFailure(exception)
// 若接口请求失败时,从 SP 里获取上次缓存的数据
val cachedConfig: List<AdConfig>? = SPUtils.getString(mAdConfigSp, KEY_CACHE_CONFIG).toObject()
if (cachedConfig != null && mAdConfigList.isEmpty()) {
handleAdConfig(cachedConfig)
}
callback?.invoke()
}
})
}
/**
* 获取搜索页的广告列表
*/
fun getGameSearchAdList(): ArrayList<AdConfig> {
return mGameSearchAdList
}
/**
* 获取下载管理页的广告
*/
fun getDownloadManagerAd(): AdConfig? {
return mDownloadManagerAd
}
/**
* 处理广告配置
*/
fun handleAdConfig(configList: List<AdConfig>) {
mAdConfigList.clear()
mGameSearchAdList.clear()
mSplashAd = null
mDownloadManagerAd = null
mVGameLaunchAd = null
for (config in configList) {
mAdConfigList.add(config)
// 处理返回的数据
when (config.location) {
"halo_launch" -> {
config.ownerAd?.startAd?.let { it.id = config.ownerAd.id }
mSplashAd = config
}
"download_manager" -> mDownloadManagerAd = config
"game_search" -> config.let { mGameSearchAdList.add(it) }
"helper_launch" -> mVGameLaunchAd = config
}
}
}
/**
* 是否需要显示开屏广告
* @param isHotLaunch 是否为热启动
*/
fun shouldShowStartUpAd(isHotLaunch: Boolean): Boolean {
return mSplashAd != null
&& !isShowingSplashAd
&& (!isHotLaunch || shouldShowStartUpAdWhenHotLaunch())
&& !isMatchAdFreeRule(mSplashAd)
&& isMatchStartUpAdDisplayRule()
}
/**
* 热启动是否需要显示开屏广告
*/
private fun shouldShowStartUpAdWhenHotLaunch() =
mSplashAd?.displayRule?.hotStartSplashAd?.type == AD_TYPE_SDK && mSplashAd?.hotStartThirdPartyAd != null
/**
* 是否需要显示下载管理广告
*/
fun shouldShowDownloadManagerAd(): Boolean {
return mDownloadManagerAd != null && !isMatchAdFreeRule(mDownloadManagerAd) && isMatchDownloadManagerAdDisplayRule()
}
/**
* 是否需要显示游戏搜索广告
*/
fun shouldShowGameSearchAd(adConfig: AdConfig): Boolean {
return !isMatchAdFreeRule(adConfig) && isMatchGameSearchAdDisplayRule(adConfig)
}
/**
* 是否在免广告时长内
*/
private fun isMatchAdFreeRule(adConfig: AdConfig?): Boolean {
adConfig?.displayRule?.run {
if (adFreeDuration > 0) {
val ghInstalledDurationInHours = (System.currentTimeMillis() - PackageUtils.getInstalledTime(
HaloApp.getInstance(),
BuildConfig.APPLICATION_ID
)).toFloat() / 1000 / 3600
return ghInstalledDurationInHours < adFreeDuration
} else {
return false
}
}
return false
}
/**
* 是否大于开屏广告展示间隔时长
*/
private fun isMatchStartUpAdDisplayRule(): Boolean {
mSplashAd?.displayRule?.run {
if (adDisplayInterval > 0) {
val lastShowTime = SPUtils.getLong(Constants.SP_LAST_SPLASH_AD_SHOW_TIME, 0L)
val durationInMinutes = (System.currentTimeMillis() - lastShowTime).toFloat() / 1000 / 60
return durationInMinutes > adDisplayInterval
} else {
return true
}
}
return true
}
/**
* 是否大于广告管理展示间隔时长
*/
private fun isMatchDownloadManagerAdDisplayRule(): Boolean {
mDownloadManagerAd?.displayRule?.run {
if (adDisplayInterval > 0) {
val lastShowTime = SPUtils.getLong(Constants.SP_LAST_DOWNLOAD_MANAGER_AD_SHOW_TIME, 0L)
val durationInMinutes = (System.currentTimeMillis() - lastShowTime).toFloat() / 1000 / 60
return durationInMinutes > adDisplayInterval
} else {
return true
}
}
return true
}
/**
* 是否大于游戏搜索展示间隔时长
*/
private fun isMatchGameSearchAdDisplayRule(adConfig: AdConfig?): Boolean {
adConfig?.displayRule?.run {
if (adDisplayInterval > 0) {
val lastShowTime = SPUtils.getLong(Constants.SP_LAST_GAME_SEARCH_AD_SHOW_TIME + adConfig.position, 0L)
val durationInMinutes = (System.currentTimeMillis() - lastShowTime).toFloat() / 1000 / 60
return durationInMinutes > adDisplayInterval
} else {
return true
}
}
return true
}
/**
* 更新主题样式
*/
private fun updateThemeStatus(context: Context, isDarkMode: Boolean) {
mCsjAdImpl?.updateThemeStatus(context, isDarkMode)
}
/**
* 请求开屏广告
*/
@JvmStatic
fun requestSplashAd(
activity: Activity,
adViewWidthInPx: Int,
adViewHeightInPx: Int,
adViewWidthInDp: Float,
adViewHeightInDp: Float,
startAdContainer: ViewGroup,
sdkStartAdContainer: ViewGroup,
adsViewGroup: FrameLayout,
handler: BaseActivity.BaseHandler,
isHotLaunch: Boolean,
hideAction: () -> Unit
) {
val hideCallback = {
isShowingSplashAd = false
hideAction.invoke()
}
if (mSplashAd != null) {
when (if (isHotLaunch) mSplashAd!!.displayRule.hotStartSplashAd?.type else mSplashAd!!.displayRule.adSource) {
AD_TYPE_SDK -> {
isShowingSplashAd = true
requestThirdPartySplashAd(
activity,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
startAdContainer,
sdkStartAdContainer,
adsViewGroup,
handler,
isHotLaunch,
hideCallback
)
}
AD_TYPE_OWNER -> {
isShowingSplashAd = true
requestStandardSplashAd(
activity,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
startAdContainer,
sdkStartAdContainer,
adsViewGroup,
handler,
isHotLaunch,
hideCallback
)
}
}
} else {
hideCallback.invoke()
}
}
/**
* 获取第三方开屏广告
*/
private fun requestThirdPartySplashAd(
activity: Activity,
adViewWidthInPx: Int,
adViewHeightInPx: Int,
adViewWidthInDp: Float,
adViewHeightInDp: Float,
startAdContainer: ViewGroup,
sdkStartAdContainer: ViewGroup,
adsViewGroup: FrameLayout,
handler: BaseActivity.BaseHandler,
isHotLaunch: Boolean,
hideCallback: () -> Unit
) {
// 第三方开屏广告回调,失败时根据接口配置选项决定是否显示自有开屏广告
val sdkSplashCallback: (isSuccess: Boolean) -> Unit = { isSuccess ->
if (isSuccess) {
hideCallback.invoke()
SPUtils.setLong(Constants.SP_LAST_SPLASH_AD_SHOW_TIME, System.currentTimeMillis())
} else {
if (mSplashAd?.displayRule?.adSource == AD_TYPE_SDK && mSplashAd?.displayRule?.onFailedAction == "show" && !isHotLaunch) {
sdkStartAdContainer.visibility = View.GONE
requestStandardSplashAd(
activity,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
startAdContainer,
sdkStartAdContainer,
adsViewGroup,
handler,
isHotLaunch,
hideCallback
)
} else {
hideCallback.invoke()
}
}
}
val thirdPartyAd = if (isHotLaunch) mSplashAd?.hotStartThirdPartyAd else mSplashAd?.thirdPartyAd
// 第三方广告的数据为空,按加载失败处理
if (mSplashAd == null || thirdPartyAd == null) {
sdkSplashCallback.invoke(false)
return
}
val timeout = if (isHotLaunch) {
((mSplashAd?.displayRule?.hotStartSplashAd?.timeout ?: 3.5F) * 1000).toInt()
} else {
((mSplashAd?.displayRule?.timeout ?: 3.5F) * 1000).toInt()
}
if (thirdPartyAd.sourceName == AD_SDK_BEIZI) {
sdkStartAdContainer.visibility = View.VISIBLE
requestBeiziSplashAd(sdkStartAdContainer, adsViewGroup, adViewWidthInPx, adViewHeightInPx, timeout.toLong(), sdkSplashCallback)
} else if (thirdPartyAd.sourceName == AD_SDK_CSJ) {
sdkStartAdContainer.visibility = View.VISIBLE
requestCsjSplashAd(
activity,
thirdPartyAd.slotId,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
sdkStartAdContainer,
timeout,
sdkSplashCallback
)
}
}
/**
* 获取穿山甲的开屏广告
*/
private fun requestCsjSplashAd(
activity: Activity,
slotId: String,
adViewWidthInPx: Int,
adViewHeightInPx: Int,
adViewWidthInDp: Float,
adViewHeightInDp: Float,
startAdContainer: ViewGroup,
timeout: Int,
callback: (isSuccess: Boolean) -> Unit,
) {
if (mCsjAdImpl == null) {
callback.invoke(false)
} else {
mCsjAdImpl?.requestSplashAd(
activity,
slotId,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
startAdContainer,
timeout,
callback,
)
}
}
/**
* 获取 Beizi 的开屏广告
*/
private fun requestBeiziSplashAd(
startAdContainer: View,
adsFl: FrameLayout,
adViewWidthInPx: Int,
adViewHeightInPx: Int,
timeout: Long,
callback: (isSuccess: Boolean) -> Unit,
) {
if (mBeiziAdImpl == null) {
callback.invoke(false)
} else {
mBeiziAdImpl?.requestSplashAd(startAdContainer, adsFl, adViewWidthInPx, adViewHeightInPx, timeout, callback)
}
}
/**
* 显示自有的开屏广告
*/
private fun requestStandardSplashAd(
activity: Activity,
adViewWidthInPx: Int,
adViewHeightInPx: Int,
adViewWidthInDp: Float,
adViewHeightInDp: Float,
startAdContainer: ViewGroup,
sdkStartAdContainer: ViewGroup,
adsViewGroup: FrameLayout,
handler: BaseActivity.BaseHandler,
isHotLaunch: Boolean,
hideCallback: () -> Unit
) {
val splashAd = mSplashAd?.ownerAd?.startAd
val onEmptyCallback = {
if (mSplashAd?.displayRule?.adSource == AD_TYPE_OWNER && mSplashAd?.displayRule?.onFailedAction == "show" && mSplashAd?.thirdPartyAd != null) {
// 自有广告为空时,显示第三方广告
requestThirdPartySplashAd(
activity,
adViewWidthInPx,
adViewHeightInPx,
adViewWidthInDp,
adViewHeightInDp,
startAdContainer,
sdkStartAdContainer,
adsViewGroup,
handler,
isHotLaunch,
hideCallback
)
} else {
hideCallback.invoke()
}
}
if (splashAd == null) {
onEmptyCallback.invoke()
return
}
if (!TextUtils.isEmpty(splashAd.img)) {
val showedTodayTimestamp = SPUtils.getString(Constants.SP_STARTUP_AD_TIMESTAMP, "") ?: ""
when (splashAd.rule) {
"each" -> showStandardSplashAd(splashAd, startAdContainer, handler, hideCallback)
"once" -> if (TextUtils.isEmpty(showedTodayTimestamp)
|| !showedTodayTimestamp.contains(splashAd.id)
) {
showStandardSplashAd(splashAd, startAdContainer, handler, hideCallback)
} else {
onEmptyCallback.invoke()
}
"everyday" -> {
val today = getToday()
if (TextUtils.isEmpty(showedTodayTimestamp)
|| !showedTodayTimestamp.contains(today)
|| !showedTodayTimestamp.contains(splashAd.id)
) {
showStandardSplashAd(splashAd, startAdContainer, handler, hideCallback)
} else {
onEmptyCallback.invoke()
}
}
else -> hideCallback.invoke()
}
SPUtils.setString(Constants.SP_STARTUP_AD_TIMESTAMP, splashAd.id + getToday())
} else {
hideCallback.invoke()
}
}
private fun showStandardSplashAd(
ad: StartupAdEntity,
startAdContainer: ViewGroup,
handler: BaseActivity.BaseHandler,
hideCallback: () -> Unit
) {
val jumpBtn: View = startAdContainer.findViewById(R.id.jumpBtn)
val jumpDetailBtn: TextView = startAdContainer.findViewById(R.id.jumpDetailBtn)
val adImage: SimpleDraweeView = startAdContainer.findViewById(R.id.adImage)
val icpContainer: View? = startAdContainer.findViewById(R.id.startAdIcpContainer)
startAdContainer.visibility = View.VISIBLE
icpContainer?.visibility = View.VISIBLE
jumpDetailBtn.text = ad.desc
jumpDetailBtn.setDrawableEnd(
AppCompatResources.getDrawable(
startAdContainer.context,
R.drawable.ic_startup_ad_arrow
), null, null
)
ImageUtils.display(adImage, ad.img)
startAdContainer.setOnClickListener {
// 拦截点击事件传递
}
jumpBtn.setOnClickListener {
handler.removeMessages(MainActivity.COUNTDOWN_AD)
hideCallback.invoke()
val linkEntity = ad.jump
logOpenScreenAdSkip(
ad.id,
(if (linkEntity.text != null) linkEntity.text else "")!!,
(if (linkEntity.type != null) linkEntity.type else "")!!,
(if (linkEntity.link != null) linkEntity.link else "")!!
)
SensorsBridge.trackEvent(
"SplashAdOwnSkip",
"splash_ad_id",
ad.id,
"link_type",
linkEntity.type ?: "",
"link_id",
linkEntity.link ?: "",
"link_text",
linkEntity.text ?: ""
)
}
val sources: MutableList<ExposureSource> = ArrayList()
sources.add(ExposureSource("开屏广告", ad.id))
val event = createEvent(null, sources, null, ExposureType.EXPOSURE)
ExposureManager.log(event)
if (ad.button) {
jumpDetailBtn.setOnClickListener { v: View ->
val linkEntity = ad.jump
directToLinkPage(v.context, linkEntity, "(启动广告)", "", event)
SensorsBridge.trackEvent(
"SplashAdOwnClick",
"splash_ad_id",
ad.id,
"link_type",
linkEntity.type ?: "",
"link_id",
linkEntity.link ?: "",
"link_text",
linkEntity.text ?: ""
)
v.postDelayed({
handler.removeMessages(MainActivity.COUNTDOWN_AD)
hideCallback.invoke()
}, 1000)
}
jumpDetailBtn.visibility = View.VISIBLE
LogUtils.logStartAd("watch_start_ads", ad)
} else {
LogUtils.logStartAd("start_ads", ad)
}
SPUtils.setLong(Constants.SP_LAST_SPLASH_AD_SHOW_TIME, System.currentTimeMillis())
val msg = Message.obtain()
msg.what = MainActivity.COUNTDOWN_AD
msg.obj = ad
handler.sendMessageDelayed(msg, 1000)
}
/**
* 获取第三方信息流广告
*/
fun requestThirdPartyFlowAd(
fragment: Fragment,
slotId: String,
adContainerView: ViewGroup,
expressViewWidth: Float,
callback: (isSuccess: Boolean) -> Unit,
) {
mCsjAdImpl?.requestFlowAd(fragment, adContainerView, slotId, expressViewWidth, callback)
}
/**
* 获取第三方 Banner 广告
*/
fun requestThirdPartyBannerAd(
fragment: Fragment,
containerView: ViewGroup,
ad: AdConfig.ThirdPartyAd,
expressViewWidthInDp: Float,
callback: (isSuccess: Boolean) -> Unit
) {
val slotId = ad.slotId
val displayRatio: Float = if (ad.displaySize.isEmpty()) {
2F
} else {
val array = ad.displaySize.split("*")
if (array.size == 2) {
array[0].toFloat() / array[1].toFloat()
} else {
2F
}
}
val expressViewHeightInDp = expressViewWidthInDp / displayRatio
mCsjAdImpl?.requestBannerAd(
fragment,
containerView,
slotId,
expressViewWidthInDp,
expressViewHeightInDp,
callback
)
}
/**
* 取消开屏广告
*/
fun cancelSplashAd(context: Context) {
mBeiziAdImpl?.cancelSplashAd(context)
mCsjAdImpl?.cancelSplashAd(context)
}
}