前几篇写过穿山甲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 来看看激励视频的布局,我看了视频播放中和播放结束时的布局,显示如下

Android 接入 广点通 广点通手机端_ADA

Android 接入 广点通 广点通手机端_广点通sdk_02

从这两张图上可以看出,关闭按钮一直都在,视频播放时处于隐藏状态,视频播放结束时显示出来,并且该 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的话,这是个思路。请知道的大佬给予指正。