前几篇写过穿山甲sdk激励视频按钮丢失回调的问题,今天说说广点通sdk激励视频的问题。
由于穿山甲sdk蹦出的关闭按钮问题,那么我很自然的想到了广点通sdk的激励视频是否也存在相同的问题。还按照老思路,打了一堆日志,来看看日志是否有确失。先用 adb 命令来看看激励视频 Activity 的名字,发现是 PortraitADActivity。 接着就可以在通过实现 Application.ActivityLifecycleCallbacks 接口的方法,在 onActivityResumed(Activity activity) 中来实现视频播放4秒就关闭 Activity 的操作了。搜索 PortraitADActivity 这个类,发现了它是个空类,继承了 ADActivity 这个类,
public class PortraitADActivity extends ADActivity {
public PortraitADActivity() {
}
}
ADActivity 中,看着不禁笑了,这是个壳子,看到这种写法,明显是初代插件化的写法,ACTD 是个接口,真正的逻辑是放在 ACTD 的实现类中,通过 ADActivity 的生命周期回调方法,调用 ACTD 的方法。
public class ADActivity extends Activity {
private ACTD a;
public ADActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
ADActivity var2 = this;
Bundle var3;
if ((var3 = this.getIntent().getExtras()) != null) {
String var4 = var3.getString("gdt_activity_delegate_name");
String var6 = var3.getString("appid");
if (!StringUtil.isEmpty(var4) && !StringUtil.isEmpty(var6)) {
try {
if (GDTADManager.getInstance().initWith(var2.getApplicationContext(), var6)) {
var2.a = GDTADManager.getInstance().getPM().getPOFactory().getActivityDelegate(var4, var2);
if (var2.a == null) {
GDTLogger.e("Init ADActivity Delegate return null,delegateName" + var4);
}
} else {
GDTLogger.e("Init GDTADManager fail in AdActivity");
}
} catch (Throwable var5) {
GDTLogger.e("Init ADActivity Delegate Faile,DelegateName:" + var4, var5);
}
}
}
if (this.a != null) {
this.a.onBeforeCreate(savedInstanceState);
} else {
this.finish();
}
super.onCreate(savedInstanceState);
if (this.a != null) {
this.a.onAfterCreate(savedInstanceState);
}
}
}
看到这,我们也不知道 ACTD 的实现类是什么,没办法了,只能想办法打印下 a 的值,它是个私有的属性,怎么办呢?反射这时候闪亮登场了,方法如下
@Override
public void onActivityResumed(Activity activity) {
String name = activity.getClass().getSimpleName();
if("PortraitADActivity".equals(name)){
try {
Field field = getDeclaredField(activity, "a");
field.setAccessible(true);
Object obj = field.get(activity);
Log.e("PortraitADActivity", "obj: " + obj.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
public Field getDeclaredField(Object object, String fieldName){
Field field = null ;
Class<?> clazz = object.getClass() ;
for(; clazz != Object.class ; clazz = clazz.getSuperclass()) {
try {
field = clazz.getDeclaredField(fieldName) ;
return field ;
} catch (Exception e) {
}
}
return null;
}
打印的值为 PortraitADActivity: obj: com.qq.e.comm.plugin.rewardvideo.f@1c668d2 ,我们知道了 ACTD 的实现类是 com.qq.e.comm.plugin.rewardvideo.f,找找这个类,居然没有?原来项目里引用的是 arr 包,那么把它解压,发现有个 assets 的文件夹,继续进去,发现 gdtadv2.jar 这个jar,使用反编译工具,看看它里面的代码。果然,在里面找到了f 这个类,里面都是混淆过后的代码,它实现了 OnClickListener 等接口。
public class f implements OnClickListener, ACTD, com.qq.e.comm.plugin.ac.b.a, com.qq.e.comm.plugin.b.d.a {
public void onClick(View v) {
if (v == this.f) {
this.w.b(System.currentTimeMillis());
a(1, null);
} else if (v == this.n.a()) {
...
} else if (v == this.m.b()) {
this.v.f();
this.a.finish();
}
...
}
}
我们知道,关闭按钮的点击事件不出意外就在这个里面实现。只看代码的话,关闭按钮点击事件大体可以定位到 v == this.m.b() 这个判断里。下一步就是想办法获取到关闭按钮,还是使用 Android studio 自带的工具 Layout Inspector 来看看激励视频的布局,我看了视频播放中和播放结束时的布局,显示如下
从这两张图上可以看出,关闭按钮一直都在,视频播放时处于隐藏状态,视频播放结束时显示出来,并且该 ImageView 没有设置 id,我们想通过 findViewById() 的方法就行不通了;细看布局,发现它外面的布局是 c,那么办法来了,简单点的,我们可以找到 R.id.content 的容器,然后通过获取它的子view,继续,知道获取到 ImageView;如果想给自己找点麻烦,顺便想看看 c 这个容器到底是何方神圣,那么我们就获取子view的时候,到 c 就停止。我们继续看 com.qq.e.comm.plugin.rewardvideo.f 这个类,onAfterCreate(Bundle savedInstanceState) 中,有几点注意
public void onAfterCreate(Bundle savedInstanceState) {
...
this.v = d.b(this.a.getIntent().getIntExtra(aa.a, 0));
if (this.v == null) {
GDTLogger.e("RewardVideo activity fail to create ! ad instance pass failed");
this.a.finish();
m.a(20152, 0, this.s);
} else if (this.c.W()) {
...
e();
g();
} else {
this.v.a(5001);
this.a.finish();
m.a(20052, 2, this.s);
}
}
我们会发现,它里面会做校验,如果不符合的话会直接调用 this.a.finish() 方法,会直接把 Activity 关闭,这种情况下,通过代码分析,不会有回调;看看 e() 这个方法,
private void e() {
this.g = new RelativeLayout(this.a);
this.m = new c(this.a);
...
this.m.setVisibility(View.INVISIBLE);
this.m.b().setOnClickListener(this);
this.m.a(this.g);
...
this.a.setContentView(this.g, new LayoutParams(-1, -1));
...
}
上述是简化代码,可以看出 g 就是上图中 content 的子view, c 被创建后,隐藏了起来,看看 c 的代码
public class c extends RelativeLayout {
private final ImageView a;
private final a b;
public c(Context context) {
super(context);
this.b = new f(context, null).a();
this.b.f();
addView(this.b.b(), new LayoutParams(-1, -1));
this.a = new ImageView(context);
this.a.setScaleType(ScaleType.FIT_CENTER);
this.a.setImageBitmap(an.a("iVBOR...mCC\n"));
ViewGroup.LayoutParams layoutParams = new LayoutParams(aj.a(context, 30), aj.a(context, 30));
layoutParams.addRule(9, -1);
layoutParams.addRule(10, -1);
layoutParams.topMargin = aj.a(context, 15);
layoutParams.leftMargin = aj.a(context, 20);
addView(this.a, layoutParams);
if (GDTADManager.getInstance().getSM().getInteger("rewardVideoEndcardSoft", 0) == 1) {
setLayerType(1, null);
} else {
this.b.b().setBackgroundColor(0);
}
}
public a a() {
return this.b;
}
public void a(ViewGroup viewGroup) {
if (getParent() == null) {
viewGroup.addView(this, new ViewGroup.LayoutParams(-1, -1));
}
}
public View b() {
return this.a;
}
}
可以看出,c 是个相对布局,它的构造方法中创建了 ImageView,并添加到自己里面,a(ViewGroup viewGroup) 方法是把自己添加到了 g 里面, b() 对外暴露的就是关闭按钮 ImageView,这是重新看上面 e() 方法中的代码,就明白了,通过调用 b() 获取关闭按钮,然后设置点击事件,可以再对照下 onClick(View v) 方法,它里面的 v.f() 是发送消息,调用回调方法的。
知道对完提供的方法了,那么老样子,继续使用反射,开始
private void performCloseView(Activity activity){
FrameLayout frameLayout = (FrameLayout) activity.findViewById(Window.ID_ANDROID_CONTENT);
RelativeLayout relativeLayout = (RelativeLayout) frameLayout.getChildAt(0);
if(relativeLayout != null){
try {
View viewC = relativeLayout.getChildAt(0);
Class clazz = viewC.getClass();
Method method = clazz.getMethod("b");
View view = (View) method.invoke(viewC);
view.performClick();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样,就找到关闭按钮,并且调用了它的点击事件。我在大量打log的情况,在 ActivityLifecycleCallbacks 的 onActivityResumed(Activity activity) 方法中,使用 Handler 延迟4秒执行上面的方法,这样,视频播放4秒就关闭了,不用傻傻的几十秒才能进行下一次请求。
如何检测关闭按钮的思路和流程都在上一篇中,本篇就是对前面的一个补充。广点通sdk是把广告放到两个部分里,实现了动态加载,如果在 Android Studio 中直接看的话,有一部分会隐藏。这样会更安全些、灵活些?如果我们也封装sdk的话,这是个思路。请知道的大佬给予指正。