0%

Xposed开发之微信赞赏

个人开发者开发出来的 App,经常会见到微信赞赏(或者捐赠)入口。这个是如何实现的呢?

一般有以下方案:

  1. 一般的个人 App 是不会接入微信/支付宝 SDK 的,因为 SDK 嵌入成本高,也会增加不小的 APK 体积,应用市场审核一般也是通过不了的。
  2. 而微信对通过直接使用相应的 URI 传参跳转至微信相应捐赠页面是不可行的。
  3. 众多独立 App 开发者就采取迂回策略,通过 App 内嵌收款二维码 或者 赞赏码图片,把图片放到相册中,并跳转至微信扫一扫界面,通过引导用户从相册中选取相应的二维码图片,达到曲线救国的目的。

以上方案中,只有方案 3 可行,看起来挺麻烦的,但不失为一种有效的方案。

那么,如果让强大的 Xposed 来做,会不会有其他的方式呢?因为的解决方案中有很致命的点在于,需要引导用户自己去选取二维码图片从而实现迂回跳转到捐赠/转账页面,这一系列操作中交互很多,其实是很不方便的。 Xposed 能越过这些麻烦的操作的同时也能达到最终目的吗?

原理分析

从上面可行的 方案3 中,以微信赞赏为例,赞赏时的界面如下:

微信赞赏界面

其 Activity 为 QrRewardSelectMoneyUI,因为该 Activity 没有暴露给其他第三方 App 进行调用,所以上面的 方案2 是不可行的,而是需要通过微信内部其他 Activity 实现跳转。

实质上就是微信内部 Activity 带着相关的参数跳转到赞赏界面,其本质就是通过 Intent 携带相关参数,启动目标赞赏 Activity。

那么 Xposed 实现微信捐赠的思路就来了:

  1. 用户点击 App 中的微信赞赏时,跳转至微信的主界面 com.tencent.mm.ui.LauncherUI
  2. Hook 主界面的 onCreate 方法,在 Intent 中添加相关参数,跳转至捐赠界面 com.tencent.mm.plugin.collect.reward.ui.QrRewardSelectMoneyUI,并销毁主界面 Activity。
  3. 为了以防万一,Hook QrRewardSelectMoneyUIonCreate 方法,已确保相关参数正确传递。完工。

那么,启动 QrRewardSelectMoneyUI 时到底需要传递哪些参数呢? 这时就可以通过反编译查看源码,或者通过 Xposed Hook 的方式,进行相关参数的获取。

技术实现

常量类 Const

1
2
3
4
5
6
7
8
9
10
public interface Const {

String WECHAT_PACKAGE_NAME = "com.tencent.mm";
String WECHAT_LAUNCHER_UI = WECHAT_PACKAGE_NAME + ".ui.LauncherUI";
String WECHAT_QR_REWARD_SELECT_MONEY_UI = WECHAT_PACKAGE_NAME + ".plugin.collect.reward.ui.QrRewardSelectMoneyUI";
String WECHAT_KEY_EXTRA_DONATE = "TianmaDonate";
// 核心参数,可以通过 Xposed Hook 的方式获取参数的值。
String WECHAT_QRCODE_URL = "m01pPa@:hEyGJ5P*a1@$xPI";

}

核心Hook类 DonateWechatHook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class DonateWechatHook implements IXposedHookLoadPackage {

private static final String KEY_SCENE = "key_scene";
private static final String KEY_QRCODE_URL = "key_qrcode_url";
private static final String KEY_CHANNEL = "key_channel";

@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (Const.WECHAT_PACKAGE_NAME.equals(lpparam.packageName)) {
try {
hookLauncherUIOnCreate(lpparam);
hookQrRewardSelectMoneyUI(lpparam);
} catch (Throwable e) {
e.printStackTrace();
}
}
}

/**
* Hook com.tencent.mm.ui.LauncherUI#onCreate();
*/
private void hookLauncherUIOnCreate(XC_LoadPackage.LoadPackageParam lpparam) {
XposedHelpers.findAndHookMethod(Const.WECHAT_LAUNCHER_UI,
lpparam.classLoader,
"onCreate",
Bundle.class,
new LauncherUIOnCreateHook());
}

private class LauncherUIOnCreateHook extends XC_MethodHook {

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Activity activity = (Activity) param.thisObject;
if (activity != null) {
Intent intent = activity.getIntent();
if (intent != null) {
ComponentName cn = intent.getComponent();
String className = cn == null ? null : cn.getClassName();
// 自定义的参数,用于区分正常启动微信主界面
boolean hasDonateExtra = intent.hasExtra(Const.WECHAT_KEY_EXTRA_DONATE);
if (Const.WECHAT_LAUNCHER_UI.equals(className) && hasDonateExtra) {
// 参数校验成功,则跳转至微信赞赏界面,并销毁主Activity
intent.removeExtra(Const.WECHAT_KEY_EXTRA_DONATE);

Intent donateIntent = new Intent();
donateIntent.setClassName(activity, Const.WECHAT_QR_REWARD_SELECT_MONEY_UI);
donateIntent.putExtra(KEY_SCENE, 2);
donateIntent.putExtra(KEY_QRCODE_URL, Const.WECHAT_QRCODE_URL);
donateIntent.putExtra(KEY_CHANNEL, 13);
donateIntent.putExtra(Const.WECHAT_KEY_EXTRA_DONATE, true);
activity.startActivity(donateIntent);
activity.finish();
}

}
}
}
}

/**
* Hook com.tencent.mm.plugin.collect.reward.ui.QrRewardSelectMoneyUI#onCreate();
*/
private void hookQrRewardSelectMoneyUI(XC_LoadPackage.LoadPackageParam lpparam) {
XposedHelpers.findAndHookMethod(Const.WECHAT_QR_REWARD_SELECT_MONEY_UI,
lpparam.classLoader,
"onCreate",
Bundle.class,
new QrRewardSelectMoneyUIOnCreateHook());
}

private class QrRewardSelectMoneyUIOnCreateHook extends XC_MethodHook {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Activity activity = (Activity) param.thisObject;
if (activity != null) {
Intent intent = activity.getIntent();
if (intent != null) {
// 确保参数合法
boolean hasDonateExtra = intent.hasExtra(Const.WECHAT_KEY_EXTRA_DONATE);
if (hasDonateExtra) {
String qrCodeUrl = intent.getStringExtra(KEY_QRCODE_URL);
if (TextUtils.isEmpty(qrCodeUrl)) {
intent.putExtra(KEY_QRCODE_URL, Const.WECHAT_QRCODE_URL);
}
intent.removeExtra(Const.WECHAT_KEY_EXTRA_DONATE);
}
}
}
}
}
}

调用则是正常启动微信主界面,并传递特定参数即可:

1
2
3
4
5
6
public void donateByWechat() {
Intent intent = new Intent();
intent.setClassName(Const.WECHAT_PACKAGE_NAME, Const.WECHAT_LAUNCHER_UI);
intent.putExtra(Const.WECHAT_KEY_EXTRA_DONATE, true);
startActivity(intent);
}

源码: DonateWechatHook

参考

WechatLuckeyMoney