Commit 5bee9d5c authored by bixing's avatar bixing

支持原生广告

parent 67d5ac5d
......@@ -69,8 +69,8 @@ android {
}
notes {
applicationId "com.notemaster.record.mark"
versionName "1.0.0.6"
versionCode 1000006
versionName "1.0.0.7"
versionCode 1000007
signingConfig signingConfigs.toolsTest
//release
//release
......
......@@ -56,29 +56,29 @@ android {
flavorDimensions "versionName"
productFlavors {
ceshi {
applicationId "com.a.hr.heart.rate"
applicationId "com.c.hr.heart.rate"
versionName "1.0.0.6"
versionCode 1000006
signingConfig signingConfigs.ceshi
//release
manifestPlaceholders = [google_ad_app_id: "ca-app-pub-3940256099942544~3347511713",
signature : "facb39340ddce66c401a1ed3c477fe20",
signature : "eecfd4e18c1ecb814f6258a32b9ea1fd",
flavor_name : "GemHeart",
authorities : "$applicationId-media-AProvider",
media_version: "3"]
media_version: "4"]
}
notes {
applicationId "com.notemaster.record.mark"
versionName "1.0.0.6"
versionCode 1000006
versionName "1.0.0.7"
versionCode 1000007
signingConfig signingConfigs.toolsTest
//release
//release
manifestPlaceholders = [google_ad_app_id: "ca-app-pub-9549465692185063~9360450155",
manifestPlaceholders = [google_ad_app_id: "ca-app-pub-4201321449256985~9882419599",
signature : "5890fecce947bbfb4b38e5c1862bf137",
flavor_name : "GemHeart",
authorities : "$applicationId-media-AProvider",
media_version: "3"]
media_version: "4"]
// manifestPlaceholders = [google_ad_app_id: "ca-app-pub-3940256099942544~3347511713"]
}
}
......
......@@ -209,6 +209,15 @@
tools:replace="android:icon, android:excludeFromRecents, android:label, android:configChanges, android:theme" >
</activity>
<activity
android:name="com.gem.media.ad.VNActivity"
android:configChanges="keyboardHidden|screenSize|screenLayout|orientation"
android:excludeFromRecents="true"
android:exported="false"
android:label="@string/heart_lable"
android:theme="@style/NativeAdTranslucentTheme"
android:screenOrientation="behind" />
</application>
</manifest>
\ No newline at end of file
......@@ -33,4 +33,6 @@ interface XGENAdData {
boolean adIsLoading();
void transferData(int type, String data);
boolean isNativeAdReady();
}
\ No newline at end of file
......@@ -9,6 +9,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
......@@ -32,6 +33,7 @@ import com.gem.media.ad.AdStatusCallBack;
import com.gem.media.ad.AdmobInterstitialManager;
import com.gem.media.ad.AdmobNativeManager;
import com.gem.media.ad.NativeAdStatusCallBack;
import com.gem.media.ad.VNANativeAdManager;
import com.gem.media.splash.base.protocol.Constants;
import com.gem.media.splash.base.utils.PROP;
import com.gem.media.splash.base.utils.PkgUtils;
......@@ -67,6 +69,7 @@ public class InitA {
private AdListenerHelper.AdNativeShownCallback adNativeShownCallback;
private static final class InitAHolder {
static final InitA adManager = new InitA();
}
......@@ -99,6 +102,7 @@ public class InitA {
pid = android.os.Process.myPid();
initAdmobInterstitialAdListener();
initAdmobNativeAdListener();
initVNANativeAdListener();
context.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
initBugly(context);
CleanUtil.init(context);
......@@ -294,10 +298,14 @@ public class InitA {
@Override
public void onActivityResumed(Activity activity) {
currentActivity = activity;
}
@Override
public void onActivityPaused(Activity activity) {
if (currentActivity == activity) {
currentActivity = null;
}
}
@Override
......@@ -389,6 +397,10 @@ public class InitA {
this.messenger = messenger;
}
public Activity getCurrentActivity() {
return currentActivity;
}
public Messenger getMessenger() {
return messenger;
}
......@@ -519,6 +531,53 @@ public class InitA {
});
}
/**
* VNA 专用 NativeAdStatusCallBack。
* 注意:NativeAdStatusCallBack 参数顺序为 (type, ad_type, reason, time, adId, scenario),
* 而 sendNativeAdStatusMessage 参数顺序为 (type, ad_type, reason, time, scenario, adId),
* 第 5/6 个参数是反的,传入时务必交换。
*/
private void initVNANativeAdListener() {
VNANativeAdManager.getInstance().setAdStatusCallBack(new NativeAdStatusCallBack() {
@Override
public void onAdLoad(String type, String ad_type, String reason, long time, String adId, String scenario) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void onAdLoaded(String type, String ad_type, String reason, long time, String adId, String scenario, String adPosition) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void onNoAdError(String type, String ad_type, String reason, long time, String adId, String scenario) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void onAdShow(String type, String ad_type, String reason, long time, String adId, String scenario) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void onAdClick(String type, String ad_type, String reason, long time, String adId, String scenario) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void onAdDismiss(String type, String ad_type, String reason, long time, String adId, String scenario) {
sendNativeAdStatusMessage(type, ad_type, reason, time, scenario, adId);
}
@Override
public void nextMediaAppAdReq(String type, String ad_type, String reason, long time, String adId, String scenario) {
}
});
}
private void sendNativeAdStatusMessage(String type, String ad_type, String reason, long time, String scenario, String adId) {
Context context = BaseApplication.getApplication();
if (context == null) {
......@@ -544,6 +603,10 @@ public class InitA {
map.put(Constants.PARAM_VC, PkgUtils.getAppVersionCode(context, pkgName));
map.put(Constants.PARAM_C_TIME, System.currentTimeMillis() + "");
map.put(Constants.PARAM_ADID, adId);
// VNA V2: 新增必填字段
map.put("ecpm", "0");
map.put("ad_format", "native");
map.put("ad_source", "admob");
// ALog.e("sendAdStatusMessage", "type " + type + " ad_type " + ad_type + " reason " + reason);
} catch (Exception throwable) {
// ALog.e("sendAdStatusMessage", "finish e: " + Log.getStackTraceString(throwable));
......@@ -558,7 +621,8 @@ public class InitA {
Message message = Message.obtain();
if (Objects.equals(scenario, Constants.SCENARIO_NATIVE_CLEAR)) {
message.what = 5004;
} else if (Objects.equals(scenario, Constants.SCENARIO_NATIVE)){
} else if (Objects.equals(scenario, Constants.SCENARIO_NATIVE)
|| "yuansheng".equals(scenario)){
message.what = 5002;
}
Bundle bundle = new Bundle();
......
......@@ -28,6 +28,8 @@ import java.util.concurrent.Executors;
public class AdManager {
public static final int VNA_TYPE = 3;
private static final class AdManagerHolder {
static final AdManager adManager = new AdManager();
}
......@@ -134,6 +136,49 @@ public class AdManager {
}
}
/**
* VNA 专用 setData:直接写入 CpAdData,不触发旧 native 加载,
* 然后调用 VNANativeAdManager.loadVnaAd()。
*/
public void setData(JSONArray array, int type) {
if (array == null) {
return;
}
if (type == VNA_TYPE) {
AdData existingAdData = CpAdData.getInstance().getAdData(CpAdData.CP_ADMOB);
if (existingAdData != null && existingAdData.mapHashMap != null) {
existingAdData.mapHashMap.remove(AdData.AD_TYPE_NATIVE);
}
for (int i = 0; i < array.length(); i++) {
JSONObject o = array.optJSONObject(i);
if (o == null) {
continue;
}
try {
String cpId = o.optString(PARAM_CPID);
String adAppId_channelId = o.optString(PARAM_APPID_CHANNELID);
String adType = o.optString(PARAM_AD_TYPE);
String adId = o.optString(PARAM_ADID);
if (TextUtils.isEmpty(cpId) || !CpAdData.CP_ADMOB.equals(cpId)) {
continue;
}
AdData adData = CpAdData.getInstance().getAdData(cpId);
if (adData == null) {
adData = new AdData();
}
CpAdData.init(adAppId_channelId);
adData.addData(adType, adId);
CpAdData.getInstance().setCpData(cpId, adData);
} catch (Exception e) {
LLog.w("AdManager--", "setData VNA error: " + e.getMessage());
}
}
VNANativeAdManager.getInstance().loadVnaAd();
} else {
setData(array);
}
}
private void parseData(JSONObject object) {
String cpId = null;
String adAppId_channelId = null;
......
package com.gem.media.ad;
import android.app.Activity;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;
import com.ads.cal.notes.BaseApplication;
import com.ads.cal.notes.R;
import com.gem.media.InitA;
import com.gem.media.splash.base.protocol.Constants;
import com.google.android.gms.ads.AdListener;
import com.google.android.gms.ads.AdLoader;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.LoadAdError;
import com.google.android.gms.ads.nativead.MediaView;
import com.google.android.gms.ads.nativead.NativeAd;
import com.google.android.gms.ads.nativead.NativeAdView;
import com.or.ange.dot.utils.LLog;
import java.util.Locale;
/**
* VNA 专用原生广告管理器(AdMob SDK)。
* 与旧 AdmobNativeManager 完全隔离,仅服务于 VNA(Video Native Ad)流程。
*
* 调用链:core transferData(11) → MyDataService case 11 → AdManager.setData(VNA_TYPE)
* → VNANativeAdManager.loadVnaAd() → VNANativeAdWindManager → VNActivity
*/
public class VNANativeAdManager {
private static final String TAG = "VNANativeAdManager";
private static final String AD_TYPE = "admob_vna_native";
/**
* VNA 场景标识,与 mediaapp/max_mediaapp/pangle_mediaapp 三端统一。
* 用于 sendNativeAdStatusMessage 中路由 message.what = 5002。
*/
private static final String VNA_SCENARIO = "yuansheng";
private NativeAdStatusCallBack adStatusCallBack;
private static final class Holder {
static final VNANativeAdManager INSTANCE = new VNANativeAdManager();
}
public static VNANativeAdManager getInstance() {
return Holder.INSTANCE;
}
private long requestTime = -1;
private static volatile String sAdId;
private NativeAd mNativeAd;
private volatile boolean isLoading;
private volatile long lastAdRequestTime = 0;
private static final long MIN_REQUEST_INTERVAL = 300;
// VNA fallback 状态:视频广告位失败后尝试图文广告位
private boolean vnaFallbackMode = false;
private String vnaImageSlotId = "";
/**
* VNA 模式加载广告:从 CpAdData 获取 AdMob 原生广告位 ID。
* 双 slotId 方案:index 0 = 视频广告位,index 1 = 图文广告位。
*/
public void loadVnaAd() {
vnaFallbackMode = false;
vnaImageSlotId = "";
AdData adData = CpAdData.getInstance().getAdData(CpAdData.CP_ADMOB);
if (adData == null) {
LLog.w(TAG, "loadVnaAd: CpAdData not initialized");
return;
}
// index 0: 视频广告位(优先)
String videoSlotId = adData.getAdAdId(AdData.AD_TYPE_NATIVE, 0);
// index 1: 图文广告位(兜底)
String imageSlotId = adData.getAdAdId(AdData.AD_TYPE_NATIVE, 1);
vnaImageSlotId = TextUtils.isEmpty(imageSlotId) ? "" : imageSlotId;
LLog.d(TAG, "loadVnaAd: videoSlot=" + videoSlotId + " imageSlot=" + vnaImageSlotId);
if (TextUtils.isEmpty(videoSlotId)) {
if (!TextUtils.isEmpty(imageSlotId)) {
LLog.d(TAG, "loadVnaAd: no video slot, using image slot");
sAdId = imageSlotId;
loadAd(12);
} else {
LLog.w(TAG, "loadVnaAd: no native ad slot id");
}
return;
}
sAdId = videoSlotId;
loadAd(10);
}
/**
* AdMob 加载原生广告。
* AdMob 的 AdLoader 是一次性的(每次加载创建新实例)。
*/
private void loadAd(int trigger) {
LLog.w(TAG, "loadAd start trigger=" + trigger);
if (isNativeAdReady()) {
LLog.w(TAG, "ad already ready");
return;
}
if (isLoading) {
LLog.w(TAG, "ad is loading");
return;
}
if (TextUtils.isEmpty(sAdId)) {
LLog.w(TAG, "sAdId is null");
return;
}
// fallback 路径(trigger=12)跳过频率检查
if (trigger != 12) {
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - lastAdRequestTime) < MIN_REQUEST_INTERVAL) {
LLog.w(TAG, "request too frequent");
return;
}
lastAdRequestTime = currentTime;
} else {
lastAdRequestTime = System.currentTimeMillis();
}
Context context = BaseApplication.getApplication();
if (context == null) {
LLog.w(TAG, "loadAd: context is null");
return;
}
if (null != adStatusCallBack) {
adStatusCallBack.onAdLoad(Constants.NODE_REQUEST, AD_TYPE, sAdId, -1, sAdId, VNA_SCENARIO);
}
requestTime = System.currentTimeMillis();
isLoading = true;
try {
// AdMob: 每次创建新的 AdLoader(一次性使用)
AdLoader adLoader = new AdLoader.Builder(context, sAdId)
.forNativeAd(nativeAd -> {
// 加载成功
isLoading = false;
LLog.i(TAG, "onNativeAdLoaded");
// 销毁旧的 NativeAd
if (mNativeAd != null) {
mNativeAd.destroy();
}
mNativeAd = nativeAd;
requestTime = System.currentTimeMillis() - requestTime;
if (null != adStatusCallBack) {
adStatusCallBack.onAdLoaded(Constants.NODE_REQUEST_SUCCESS, AD_TYPE,
"loaded", requestTime, sAdId, VNA_SCENARIO, "vna");
}
requestTime = System.currentTimeMillis();
})
.withAdListener(new AdListener() {
@Override
public void onAdFailedToLoad(LoadAdError loadAdError) {
isLoading = false;
String error = String.format(
Locale.getDefault(),
"domain: %s, code: %d, message: %s",
loadAdError.getDomain(),
loadAdError.getCode(),
loadAdError.getMessage());
LLog.i(TAG, "onAdFailedToLoad: " + error);
requestTime = System.currentTimeMillis() - requestTime;
// VNA fallback: 视频广告位失败,尝试图文广告位
if (!vnaFallbackMode && !vnaImageSlotId.isEmpty()) {
LLog.d(TAG, "VNA video failed, trying image fallback");
vnaFallbackMode = true;
sAdId = vnaImageSlotId;
loadAd(12);
return;
}
if (null != adStatusCallBack) {
adStatusCallBack.onNoAdError(Constants.NODE_ERROR, AD_TYPE, error,
requestTime, sAdId, VNA_SCENARIO);
}
}
@Override
public void onAdImpression() {
Log.i(TAG, "vna ad onAdImpression");
requestTime = System.currentTimeMillis() - requestTime;
if (null != adStatusCallBack) {
adStatusCallBack.onAdShow(Constants.NODE_SHOW, AD_TYPE, "show",
requestTime, sAdId, VNA_SCENARIO);
}
// 通知 VNActivity 曝光已触发,驱动自适应关闭计时器
Activity currentAct = InitA.getInstance().getCurrentActivity();
if (currentAct instanceof VNActivity) {
((VNActivity) currentAct).onExposureConfirmed();
}
}
@Override
public void onAdClicked() {
LLog.i(TAG, "vna ad onAdClicked");
if (null != adStatusCallBack) {
adStatusCallBack.onAdClick(Constants.NODE_CLICK, AD_TYPE, "click",
-1, sAdId, VNA_SCENARIO);
}
// VNA: 点击广告后同步设置关闭原因并 finish
Activity currentAct = InitA.getInstance().getCurrentActivity();
if (currentAct instanceof VNActivity) {
((VNActivity) currentAct).setDismissReason(VNActivity.DISMISS_USER_CLICK);
currentAct.finish();
} else {
VNANativeAdWindManager.getInstance().removeNativeAd();
}
}
})
.build();
adLoader.loadAd(new AdRequest.Builder().build());
LLog.w(TAG, "loadAd done");
} catch (Exception e) {
isLoading = false;
LLog.e(TAG, "loadAd exception: " + e.getMessage());
}
}
public boolean isNativeAdReady() {
return mNativeAd != null;
}
/**
* 展示 VNA 广告。在 VNActivity 中调用。
* @param nativeAdView AdMob NativeAdView 容器
* @return 0=成功, 负数=失败
*/
public int showAd(NativeAdView nativeAdView) {
if (!isNativeAdReady()) {
return -1;
}
LLog.i(TAG, "showAd");
try {
bindNativeAdView(nativeAdView, mNativeAd);
nativeAdView.setNativeAd(mNativeAd);
} catch (Exception e) {
LLog.e(TAG, "showAd render error: " + e.getMessage());
return -3;
}
nativeAdView.setVisibility(View.VISIBLE);
LLog.i(TAG, "showAd end");
return 0;
}
/**
* 绑定 NativeAd 素材到 NativeAdView。
* View ID 与 vna_n_layout.xml 一致。
*/
private void bindNativeAdView(NativeAdView adView, NativeAd nativeAd) {
// Set the media view
MediaView mediaView = adView.findViewById(R.id.vna_ad_media);
adView.setMediaView(mediaView);
// Set other ad assets
adView.setHeadlineView(adView.findViewById(R.id.vna_ad_headline));
adView.setBodyView(adView.findViewById(R.id.vna_ad_body));
adView.setCallToActionView(adView.findViewById(R.id.vna_ad_call_to_action));
adView.setIconView(adView.findViewById(R.id.vna_ad_icon));
adView.setPriceView(adView.findViewById(R.id.vna_ad_price));
adView.setStarRatingView(adView.findViewById(R.id.vna_ad_stars));
adView.setStoreView(adView.findViewById(R.id.vna_ad_store));
adView.setAdvertiserView(adView.findViewById(R.id.vna_ad_advertiser));
// headline and mediaContent are guaranteed in every NativeAd
((TextView) adView.getHeadlineView()).setText(nativeAd.getHeadline());
if (mediaView != null && nativeAd.getMediaContent() != null) {
mediaView.setMediaContent(nativeAd.getMediaContent());
}
// These assets aren't guaranteed, check before displaying
if (nativeAd.getBody() == null) {
adView.getBodyView().setVisibility(View.INVISIBLE);
} else {
adView.getBodyView().setVisibility(View.VISIBLE);
((TextView) adView.getBodyView()).setText(nativeAd.getBody());
}
if (nativeAd.getCallToAction() == null) {
adView.getCallToActionView().setVisibility(View.INVISIBLE);
} else {
adView.getCallToActionView().setVisibility(View.VISIBLE);
((Button) adView.getCallToActionView()).setText(nativeAd.getCallToAction());
}
if (nativeAd.getIcon() == null) {
adView.getIconView().setVisibility(View.GONE);
} else {
((ImageView) adView.getIconView()).setImageDrawable(nativeAd.getIcon().getDrawable());
adView.getIconView().setVisibility(View.VISIBLE);
}
if (nativeAd.getPrice() == null) {
adView.getPriceView().setVisibility(View.INVISIBLE);
} else {
adView.getPriceView().setVisibility(View.VISIBLE);
((TextView) adView.getPriceView()).setText(nativeAd.getPrice());
}
if (nativeAd.getStore() == null) {
adView.getStoreView().setVisibility(View.INVISIBLE);
} else {
adView.getStoreView().setVisibility(View.VISIBLE);
((TextView) adView.getStoreView()).setText(nativeAd.getStore());
}
if (nativeAd.getStarRating() == null) {
adView.getStarRatingView().setVisibility(View.INVISIBLE);
} else {
((RatingBar) adView.getStarRatingView()).setRating(nativeAd.getStarRating().floatValue());
adView.getStarRatingView().setVisibility(View.VISIBLE);
}
if (nativeAd.getAdvertiser() == null) {
adView.getAdvertiserView().setVisibility(View.INVISIBLE);
} else {
((TextView) adView.getAdvertiserView()).setText(nativeAd.getAdvertiser());
adView.getAdvertiserView().setVisibility(View.VISIBLE);
}
}
public void destroyAd() {
if (mNativeAd != null) {
mNativeAd.destroy();
mNativeAd = null;
}
VNANativeAdWindManager.getInstance().removeNativeAd();
}
public void setAdStatusCallBack(NativeAdStatusCallBack adStatusCallBack) {
this.adStatusCallBack = adStatusCallBack;
}
}
package com.gem.media.ad;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.ads.cal.notes.BaseApplication;
import com.gem.media.InitA;
import com.or.ange.dot.utils.LLog;
/**
* VNA 专用窗口管理器。
* 负责启动和关闭 VNActivity。
*/
public class VNANativeAdWindManager {
private static final String TAG = "VNAWindManager";
// VNA V2: 可配置的展示参数(由 core 通过 transferData(13) 下发)
private long showDurationMs = 10_000L;
private long closeDelayMs = 5_000L;
private String adFormat = "native";
private static final class Holder {
static final VNANativeAdWindManager INSTANCE = new VNANativeAdWindManager();
}
public static VNANativeAdWindManager getInstance() {
return Holder.INSTANCE;
}
private final Handler handler = new Handler(Looper.getMainLooper());
/**
* VNA V2: 设置展示配置(由 transferData(13) 调用)。
*/
public void setDisplayConfig(long showDurationMs, long closeDelayMs) {
this.showDurationMs = showDurationMs;
this.closeDelayMs = closeDelayMs;
Log.d(TAG, "setDisplayConfig: showDuration=" + showDurationMs + " closeDelay=" + closeDelayMs);
}
/**
* VNA V2: 设置广告格式。
*/
public void setAdFormat(String adFormat) {
this.adFormat = adFormat;
Log.d(TAG, "setAdFormat: " + adFormat);
}
/**
* 启动 VNActivity 展示 VNA 广告。
* 注意:此方法可能从 AIDL Binder 线程调用,需切到主线程。
*/
public void showVnaAd() {
Log.d(TAG, "showVnaAd");
if (Looper.myLooper() != Looper.getMainLooper()) {
handler.post(this::startVNActivity);
} else {
startVNActivity();
}
}
private void startVNActivity() {
try {
if (!InitA.getInstance().isAppBackground()) {
Log.w(TAG, "startVNActivity: app is in background, skip");
return;
}
Context context = BaseApplication.getApplication();
if (context == null) {
Log.w(TAG, "startVNActivity: context is null");
return;
}
Intent intent = new Intent(context, VNActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_RECEIVER_FOREGROUND);
// VNA V2: 传递展示配置
intent.putExtra("showDurationMs", showDurationMs);
intent.putExtra("closeDelayMs", closeDelayMs);
context.startActivity(intent);
Log.d(TAG, "startVNActivity done");
} catch (Exception e) {
Log.e(TAG, "startVNActivity error: " + e.getMessage());
}
}
/**
* 关闭 VNActivity(如果还在前台)。
*/
public void removeNativeAd() {
LLog.d(TAG, "removeNativeAd");
if (Looper.myLooper() != Looper.getMainLooper()) {
handler.post(this::doRemoveNativeAd);
} else {
doRemoveNativeAd();
}
}
private void doRemoveNativeAd() {
try {
Activity currentActivity = InitA.getInstance().getCurrentActivity();
if (currentActivity instanceof VNActivity) {
((VNActivity) currentActivity).setDismissReason(VNActivity.DISMISS_USER_CLOSE);
currentActivity.finish();
}
} catch (Exception e) {
LLog.e(TAG, "removeNativeAd error: " + e.getMessage());
}
}
}
package com.gem.media.ad;
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.ads.cal.notes.R;
import com.gem.media.InitA;
import com.google.android.gms.ads.nativead.NativeAdView;
import com.or.ange.dot.utils.LLog;
/**
* VNA 专用广告展示 Activity。
* 特性:5s autoClose + dismissReason 上报 + 沉浸式全屏 + 自定义关闭按钮。
*/
public class VNActivity extends Activity {
private static final String TAG = "VNActivity";
// 关闭原因常量
public static final int DISMISS_AUTO_CLOSE = 1;
public static final int DISMISS_USER_CLICK = 2;
public static final int DISMISS_USER_CLOSE = 3;
public static final int DISMISS_USER_LEAVE = 4;
// 计时器参数
private static final long AD_DISPLAY_DURATION_MS = 10_000L; // Phase 1 时长
private static final long AD_EXTENDED_DURATION_MS = 5_000L; // Phase 2 延长时长
private static final long MIN_DISPLAY_AFTER_EXPOSURE_MS = 3_000L; // 曝光后最少展示时间
private boolean isFinished;
private int dismissReason = DISMISS_USER_LEAVE;
private boolean isAdExposed; // 是否收到曝光回调
private boolean isExtended; // 是否已进入 Phase 2
private long adShowStartTime; // 定时器启动时间戳
private final Handler autoCloseHandler = new Handler(Looper.getMainLooper());
private final Runnable autoCloseRunnable = this::handleAutoClose;
// R1: 从 Intent 读取的可配置展示时长
private long adDisplayDurationMs = AD_DISPLAY_DURATION_MS;
// P1-2 fix: onPause 时暂停 autoClose 定时器,onResume 时恢复
private long pauseElapsedRealtime = 0;
/**
* 两阶段定时器核心逻辑:
* isAdExposed=T → 正常关闭(曝光后到期)
* isExtended=T → Phase2 到期,无条件关闭
* 其余 → Phase1 到期无曝光,延长进入 Phase2
*/
private void handleAutoClose() {
if (isFinishing()) return;
if (isAdExposed || isExtended) {
dismissReason = DISMISS_AUTO_CLOSE;
finish();
} else {
isExtended = true;
autoCloseHandler.postDelayed(autoCloseRunnable, AD_EXTENDED_DURATION_MS);
LLog.d(TAG, "No exposure in 5s, extending to 10s");
}
}
public void setDismissReason(int reason) {
this.dismissReason = reason;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(0, 0);
try {
setFinishOnTouchOutside(false);
if (getWindow() != null) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
} catch (Exception e) {
// ignore
}
setContentView(R.layout.vna_n_layout);
// VNA V2: 从 Intent 读取展示配置
adDisplayDurationMs = getIntent().getLongExtra("showDurationMs", 10_000L);
enableImmersiveFullScreen();
adjustContainerForOrientation();
report("onCreate", "");
start();
}
/**
* 广告曝光确认回调。
* Option C 逻辑:剩余时间 < 3s 时补到 3s,否则让原始定时器自然到期。
*/
public void onExposureConfirmed() {
if (isFinishing() || isAdExposed) return;
isAdExposed = true;
long elapsed = SystemClock.elapsedRealtime() - adShowStartTime;
long minimumCloseTime = elapsed + MIN_DISPLAY_AFTER_EXPOSURE_MS;
long currentDeadline = isExtended
? (adDisplayDurationMs + AD_EXTENDED_DURATION_MS)
: adDisplayDurationMs;
if (minimumCloseTime > currentDeadline) {
// 剩余时间不足 3s → 取消旧 timer,重新调度 3s
autoCloseHandler.removeCallbacks(autoCloseRunnable);
autoCloseHandler.postDelayed(autoCloseRunnable, MIN_DISPLAY_AFTER_EXPOSURE_MS);
LLog.d(TAG, "Exposure at +" + elapsed + "ms, extending display by 3s");
} else {
// 剩余时间充足,让原始 timer 自然到期
LLog.d(TAG, "Exposure at +" + elapsed + "ms, remaining time sufficient");
}
}
private void start() {
if (VNANativeAdManager.getInstance().isNativeAdReady()) {
try {
NativeAdView nativeAdView = findViewById(R.id.vna_native_ad_view);
int result = VNANativeAdManager.getInstance().showAd(nativeAdView);
if (result != 0) {
report("show_fail", "result=" + result);
finish();
return;
}
// 设置关闭按钮
ImageView closeBtn = findViewById(R.id.vna_close_button);
if (closeBtn != null) {
closeBtn.setOnClickListener(v -> {
dismissReason = DISMISS_USER_CLOSE;
finish();
});
}
// 启动曝光感知的自适应关闭计时器
adShowStartTime = SystemClock.elapsedRealtime();
isAdExposed = false;
isExtended = false;
autoCloseHandler.removeCallbacks(autoCloseRunnable);
autoCloseHandler.postDelayed(autoCloseRunnable, adDisplayDurationMs);
} catch (Exception e) {
autoCloseHandler.removeCallbacksAndMessages(null);
report("ex", Log.getStackTraceString(e));
finish();
}
} else {
autoCloseHandler.removeCallbacksAndMessages(null);
report("not_cache", "");
finish();
}
}
@Override
public void finish() {
if (isFinished) {
super.finish();
overridePendingTransition(0, 0);
return;
}
// 兜底:广告已曝光但 dismissReason 仍是默认值 → 修正为 USER_CLOSE
// 场景:Android 13+ 手势返回可能绕过 onBackPressed,直接调用 finish()
if (isAdExposed && dismissReason == DISMISS_USER_LEAVE) {
dismissReason = DISMISS_USER_CLOSE;
}
autoCloseHandler.removeCallbacks(autoCloseRunnable);
reportDismissReason(dismissReason);
report("finish", "reason=" + dismissReason);
isFinished = true;
super.finish();
overridePendingTransition(0, 0);
}
@Override
protected void onDestroy() {
autoCloseHandler.removeCallbacksAndMessages(null);
VNANativeAdManager.getInstance().destroyAd();
report("destroy", "");
super.onDestroy();
}
@Override
public void onBackPressed() {
dismissReason = DISMISS_USER_CLOSE;
finish();
}
/**
* 通过 5002 Messenger 向 core 上报 dismissReason。
*/
private void reportDismissReason(int reason) {
try {
Messenger messenger = InitA.getInstance().getMessenger();
if (messenger == null) {
LLog.w(TAG, "reportDismissReason: messenger is null");
return;
}
Message message = Message.obtain();
message.what = 5002;
message.arg1 = reason;
Bundle bundle = new Bundle();
bundle.putString("type", "vna_dismiss");
bundle.putInt("dismissReason", reason);
bundle.putString("ad_format", "native");
message.setData(bundle);
messenger.send(message);
LLog.d(TAG, "reportDismissReason: " + reason);
} catch (Exception e) {
LLog.e(TAG, "reportDismissReason error: " + e.getMessage());
}
}
/**
* 通用事件上报(通过 5002 Messenger)。
*/
private void report(String type, String reason) {
try {
Messenger messenger = InitA.getInstance().getMessenger();
if (messenger == null) {
return;
}
Message message = Message.obtain();
message.what = 5002;
Bundle bundle = new Bundle();
bundle.putString("type", type);
bundle.putString("reason", reason);
bundle.putString("ad_format", "native");
message.setData(bundle);
messenger.send(message);
} catch (Exception e) {
LLog.e(TAG, "report error: " + e.getMessage());
}
}
/**
* 横屏沉浸式:仅在横屏时隐藏状态栏 + 导航栏。
* 竖屏不做处理,保持系统栏正常显示。
*/
private void enableImmersiveFullScreen() {
if (getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
return;
}
Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false);
WindowInsetsController controller = window.getInsetsController();
if (controller != null) {
controller.hide(android.view.WindowInsets.Type.statusBars()
| android.view.WindowInsets.Type.navigationBars());
controller.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
}
}
/**
* 根据当前方向调整广告容器宽度:
* 横屏 → 限制为 shortSide - 2*margin(与竖屏视觉一致)
* 竖屏 → 限制为屏幕宽度的 75%(等比缩小广告内容)
*/
private void adjustContainerForOrientation() {
View wrapper = findViewById(R.id.vna_ad_wrapper);
if (wrapper == null) return;
ViewGroup.LayoutParams params = wrapper.getLayoutParams();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
int shortSidePx = Math.min(
getResources().getDisplayMetrics().widthPixels,
getResources().getDisplayMetrics().heightPixels);
int marginPx = (int) (16 * getResources().getDisplayMetrics().density + 0.5f) * 2;
params.width = shortSidePx - marginPx;
} else {
int screenWidth = getResources().getDisplayMetrics().widthPixels;
params.width = (int) (screenWidth * 0.75);
}
wrapper.setLayoutParams(params);
}
@Override
protected void onResume() {
super.onResume();
try {
enableImmersiveFullScreen();
} catch (Exception e) {
// ignore
}
if (pauseElapsedRealtime > 0 && adShowStartTime > 0) {
long pauseDuration = SystemClock.elapsedRealtime() - pauseElapsedRealtime;
adShowStartTime += pauseDuration;
long elapsed = SystemClock.elapsedRealtime() - adShowStartTime;
long deadline = isExtended
? (adDisplayDurationMs + AD_EXTENDED_DURATION_MS)
: adDisplayDurationMs;
long remaining = deadline - elapsed;
if (remaining > 0) {
autoCloseHandler.removeCallbacks(autoCloseRunnable);
autoCloseHandler.postDelayed(autoCloseRunnable, remaining);
} else {
autoCloseHandler.post(autoCloseRunnable);
}
pauseElapsedRealtime = 0;
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
enableImmersiveFullScreen();
adjustContainerForOrientation();
}
}
......@@ -24,6 +24,8 @@ import com.gem.media.ad.AdData;
import com.gem.media.ad.AdManager;
import com.gem.media.ad.AdmobInterstitialManager;
import com.gem.media.ad.CpAdData;
import com.gem.media.ad.VNANativeAdManager;
import com.gem.media.ad.VNANativeAdWindManager;
import com.gem.media.splash.base.utils.ALog;
import com.or.ange.dot.utils.LLog;
......@@ -46,6 +48,7 @@ public class MyDataService extends Service {
private static final int NEW_CONTENT_VERSION_CODE = 1;
private static final int MSG_WHAT3 = 1236;
private static final int MSG_WHAT4 = 1237;
private static final int MSG_WHAT_VNA = 1238;
private static final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
......@@ -82,6 +85,19 @@ public class MyDataService extends Service {
}
}
break;
case MSG_WHAT_VNA:
Bundle bundleVna = msg.getData();
if (bundleVna != null) {
String vnaData = bundleVna.getString(PARAM_DATA, "");
if (vnaData != null && !vnaData.isEmpty()) {
try {
JSONArray vnaArray = new JSONArray(vnaData);
AdManager.getInstance().setData(vnaArray, AdManager.VNA_TYPE);
} catch (JSONException e) {
//
}
}
}
}
}
};
......@@ -170,6 +186,11 @@ public class MyDataService extends Service {
@Override
public void showNAd() throws RemoteException {
try {
handler.post(() -> VNANativeAdWindManager.getInstance().showVnaAd());
} catch (Exception e) {
//
}
ALog.w(TAG, "showNAd " );
}
......@@ -180,6 +201,11 @@ public class MyDataService extends Service {
@Override
public void closeNAd() throws RemoteException {
try {
handler.post(() -> VNANativeAdManager.getInstance().destroyAd());
} catch (Exception e) {
//
}
ALog.w(TAG, "clsoeNAd " );
}
......@@ -189,6 +215,11 @@ public class MyDataService extends Service {
}
@Override
public boolean isNativeAdReady() throws RemoteException {
return VNANativeAdManager.getInstance().isNativeAdReady();
}
@Override
public void transferData(int type, String data) {
if (null == data || data.isEmpty()) {
......@@ -197,6 +228,7 @@ public class MyDataService extends Service {
}
parseData(type, data);
}
private void parseData(int type, String data) {
ALog.d(TAG, "parseData type = " +type + " data =" + data);
switch (type) {
......@@ -211,50 +243,31 @@ public class MyDataService extends Service {
message.what = MSG_WHAT4;
handler.sendMessage(message);
break;
default:
case 11:
Message vnaMsg = Message.obtain();
Bundle vnaBundle = new Bundle();
vnaBundle.putString(PARAM_DATA, data);
vnaMsg.setData(vnaBundle);
vnaMsg.what = MSG_WHAT_VNA;
handler.sendMessage(vnaMsg);
break;
case 13:
try {
org.json.JSONObject config = new org.json.JSONObject(data);
long showDurationMs = config.optLong("showDurationMs", 10_000L);
long closeDelayMs = config.optLong("closeDelayMs", 5_000L);
String adFormat = config.optString("ad_format", "native");
VNANativeAdWindManager.getInstance().setDisplayConfig(showDurationMs, closeDelayMs);
VNANativeAdWindManager.getInstance().setAdFormat(adFormat);
} catch (Exception e) {
LLog.w(TAG, "parseData type=13 error: " + e.getMessage());
}
default:
break;
}
}
// private static String cpId;
// private static String adAppId_channelId;
// private static String appkey;
// private static String adType;
// private static String adId;
private static void setData(String cpId, String adAppId_channelId, String appKey, String adType, String adId) {
ALog.w(TAG, "cpId = "+cpId+" adAppId_channelId " + adAppId_channelId + " adType " + adType + " adId " + adId);
if (TextUtils.isEmpty(cpId)) {
return;
}
if (!CpAdData.CP_ADMOB.equals(cpId)) {
return;
}
AdData adData = CpAdData.getInstance().getAdData(cpId);
if (adData == null) {
adData = new AdData();
}
CpAdData.init(adAppId_channelId);
adData.addData(adType, adId);
CpAdData.getInstance().setCpData(cpId, adData);
AdmobInterstitialManager.instance().loadAd(BaseApplication.getApplication(),
AdmobInterstitialManager.instance().loadIndex);
}
private static long lastClickTime = 0;
private static final long clickInterval = 1000; // 设置点击间隔为1秒
public static boolean isFastRequest() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastClickTime >= clickInterval) {
lastClickTime = currentTime;
return false;
}
lastClickTime = currentTime;
return true;
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/vna_ad_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:background="#80000000">
<FrameLayout
android:id="@+id/vna_ad_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:clipChildren="false"
android:clipToPadding="false">
<com.google.android.gms.ads.nativead.NativeAdView
android:id="@+id/vna_native_ad_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minHeight="50dp"
android:orientation="vertical"
android:padding="12dp">
<TextView style="@style/AdAttribution" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/vna_ad_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:adjustViewBounds="true"
android:paddingBottom="5dp"
android:paddingEnd="5dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/vna_ad_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/vna_ad_advertiser"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="bottom"
android:textSize="14sp"
android:textStyle="bold" />
<RatingBar
android:id="@+id/vna_ad_stars"
style="?android:attr/ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:isIndicator="true"
android:numStars="5"
android:stepSize="0.5" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/vna_ad_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp" />
<com.google.android.gms.ads.nativead.MediaView
android:id="@+id/vna_ad_media"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="5dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<TextView
android:id="@+id/vna_ad_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textSize="12sp" />
<TextView
android:id="@+id/vna_ad_store"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textSize="12sp" />
<Button
android:id="@+id/vna_ad_call_to_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.gms.ads.nativead.NativeAdView>
<!-- 自定义关闭按钮:定位在广告内容右上角,50%溢出 -->
<ImageView
android:id="@+id/vna_close_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="top|end"
android:layout_marginTop="-18dp"
android:layout_marginEnd="-18dp"
android:contentDescription="Close ad"
android:padding="8dp"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</FrameLayout>
</FrameLayout>
......@@ -54,4 +54,30 @@
<item name="android:width">15dp</item>
<item name="android:height">15dp</item>
</style>
<!-- VNA 原生广告专用透明主题(与 pangle_mediaapp 一致) -->
<style name="NativeAdTranslucentTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.5</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowCloseOnTouchOutside">false</item>
</style>
<style name="AdAttribution">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">left</item>
<item name="android:textColor">#FFFFFF</item>
<item name="android:textSize">12sp</item>
<item name="android:text">@string/app_ad_attribution</item>
<item name="android:background">#FFCC66</item>
<item name="android:width">15dp</item>
<item name="android:height">15dp</item>
</style>
</resources>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment