版权申明:未经允许请勿转载。转载前请先联系作者(hello@yeshen.org)

最近接了一下QQ钱包的客户端。官方文档在这里

PS:官方文档其实已经很详细了。

如何接入

step 1 从服务端获取一包支付数据,构造一个PayApi的对象

step 2 做一下安装检查、版本检查、参数检查,检查完发起支付

api.checkParams() // 参数检查
IOpenApi openApi = OpenApiFactory.getInstance(activity, APP_ID);
openApi.isMobileQQInstalled() // 安装检查
openApi.isMobileQQSupportApi(OpenConstants.API_NAME_PAY) // 版本检查
openApi.execApi(api);

step 3 增加一个Activity,增加处理 callbackScheme的回调

接入细节代码

<activityandroid:name=".payment.QQ$QQActivity"android:exported="true"android:launchMode="singleTop"><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.BROWSABLE" /><category android:name="android.intent.category.DEFAULT" /><data android:scheme="qwallet10000000" /></intent-filter>
</activity>
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;import com.tencent.mobileqq.openpay.api.IOpenApi;
import com.tencent.mobileqq.openpay.api.IOpenApiListener;
import com.tencent.mobileqq.openpay.api.OpenApiFactory;
import com.tencent.mobileqq.openpay.constants.OpenConstants;
import com.tencent.mobileqq.openpay.data.base.BaseResponse;
import com.tencent.mobileqq.openpay.data.pay.PayApi;
import com.tencent.mobileqq.openpay.data.pay.PayResponse;import org.json.JSONException;
import org.json.JSONObject;public final class QQ {private static final String APP_ID = "10000000";private static OnPayment sPaymentCallback = null;public static enum PayCode {SUCCESS(0, "支付成功"),ERROR_JSON(1, "json数据解析失败"),ERROR_PARAMETER(2, "参数错误"),FAIL_CANCEL(3, "取消支付"),ERROR_FAIL(4, "支付失败"),UNKNOWN(5, "结果未知"),ERROR_QQ_NOT_INSTALL(6, "QQ未安装"),ERROR_QQ_NOT_SUPPORT_PAY(7, "QQ版本不支持");private final int code;private final String desc;PayCode(int code, String desc) {this.code = code;this.code = code;}public int getCode() {return this.code;}public String getDesc() {return this.desc;}}@UiThreadvoid qqPay(final @NonNull Activity activity, final @NonNull String payData, final @NonNull OnPayment callback) {sPaymentCallback = null;JSONObject object;PayApi api = new PayApi();try {object = new JSONObject(payData);api.appId = object.optString("a", "");api.serialNumber = object.optString("b", String.valueOf(System.currentTimeMillis()));api.callbackScheme = object.optString("c", "qwallet10000000");api.tokenId = object.optString("d", "");api.pubAcc = object.optString("e", "");api.pubAccHint = object.optString("f", "");api.nonce = object.optString("g", "");api.timeStamp = object.optLong("h", System.currentTimeMillis() / 1000);api.bargainorId = object.optString("i", "");api.sig = object.optString("j", "");api.sigType = object.optString("k", "");} catch (JSONException e) {callback.onPayment(PayCode.ERROR_JSON.getCode(), PayCode.ERROR_JSON.getDesc());return;}if (!api.checkParams()) {callback.onPayment(PayCode.ERROR_PARAMETER.getCode(), PayCode.ERROR_PARAMETER.getDesc());return;}IOpenApi openApi = OpenApiFactory.getInstance(activity, APP_ID);if (!openApi.isMobileQQInstalled()) {callback.onPayment(PayCode.ERROR_QQ_NOT_INSTALL.getCode(), PayCode.ERROR_QQ_NOT_INSTALL.getDesc());} else if (!openApi.isMobileQQSupportApi(OpenConstants.API_NAME_PAY)) {callback.onPayment(PayCode.ERROR_QQ_NOT_SUPPORT_PAY.getCode(), PayCode.ERROR_QQ_NOT_SUPPORT_PAY.getDesc());} else /*likely*/ {sPaymentCallback = callback;openApi.execApi(api);}}public static final class QQActivity extends Activity implements IOpenApiListener {@Nullableprivate IOpenApi openApi;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);openApi = OpenApiFactory.getInstance(this, APP_ID);handleIntent(getIntent());}@Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);setIntent(intent);handleIntent(intent);}@UiThreadprivate void handleIntent(Intent intent) {if (openApi == null) openApi = OpenApiFactory.getInstance(this, APP_ID);if (openApi.handleIntent(intent, this)) {// onOpenResponse} else {if (sPaymentCallback != null)sPaymentCallback.onPayment(PayCode.UNKNOWN.getCode(), PayCode.UNKNOWN.getDesc());}sPaymentCallback = null;finish();}@UiThread@Overridepublic void onOpenResponse(BaseResponse response) {if (sPaymentCallback == null) return;if (response instanceof PayResponse) {PayResponse payResponse = (PayResponse) response;if (payResponse.isSuccess()) {sPaymentCallback.onPayment(PayCode.SUCCESS.getCode(), PayCode.SUCCESS.getDesc());} else {switch (payResponse.retCode) {case -1: // 用户主动放弃支付sPaymentCallback.onPayment(PayCode.FAIL_CANCEL.getCode(), PayCode.FAIL_CANCEL.getDesc());return;case -2:// 登录态超时case -3://重复提交订单case -4:// 快速注册用户手机号不一致case -5:// 账户被冻结case -6: //支付密码输入错误次数超过上限case -100://网络异常错误sPaymentCallback.onPayment(PayCode.ERROR_FAIL.getCode(), PayCode.ERROR_FAIL.getDesc());return;case -101:sPaymentCallback.onPayment(PayCode.ERROR_PARAMETER.getCode(), PayCode.ERROR_PARAMETER.getDesc());return;default:sPaymentCallback.onPayment(PayCode.UNKNOWN.getCode(), PayCode.UNKNOWN.getDesc());}}} else {sPaymentCallback.onPayment(PayCode.UNKNOWN.getCode(), PayCode.UNKNOWN.getDesc());}}}}

–以上便完成了QQ钱包的接入。–

PS:QQ钱包几乎没什么产品接入。主流的支付方式是支付宝和微信。如果产品经理没有很好的理由,大概率这就是没必要的工作了:)。

支付的设计

解压看到核心库很小,并且已经是16年的输出了。总的来说,QQ钱包SDK的实现很朴素,也会很稳定。

7.7K May 31  2016 mqqopenpay.jar

QQ钱包SDK是面向接口编程,它实际上实现了这几个接口。

public interface IOpenApi {boolean isMobileQQInstalled();boolean isMobileQQSupportApi(String var1);boolean execApi(BaseApi var1);boolean handleIntent(Intent var1, IOpenApiListener var2);
}

如何判断QQ已经安装

private String getQQVersionName() {try {PackageInfo packageInfo;return (packageInfo = this.mContext.getPackageManager().getPackageInfo("com.tencent.mobileqq", 0)) != null && !TextUtils.isEmpty(packageInfo.versionName) ? packageInfo.versionName : null;} catch (NameNotFoundException var2) {var2.printStackTrace();return null;} catch (Exception var3) {var3.printStackTrace();return null;}
}

小结: getQQVersionName()!=null 即QQ已经安装。

如何判断用户手机上的QQ版本是否支持支付

public final boolean isMobileQQSupportApi(String api) {if (TextUtils.isEmpty(api)) {return false;} else if (api.compareTo("pay") == 0) {if ((api = this.a()) == null) {return false;} else {return compare(api, "4.7.2") >= 0;}}
}

大概逻辑就是看上一步获取的 VersionName 是否大于 4.7.2,小于这个版本的话,就提示不支持支付。
从代码中观察,QQ钱包在 5.3.0 之后有一次支付协议上的改动,支付SDK层面做了这个的兼容。

这里看到SDK使用了 compareTo ,而不是 equal 。在重重限定判断之下,这里实际上没啥区别了。
但是在普通场景下,我觉得用 equal 更好一点。一是 equal 更安全一点,二是代码会简洁清晰一点。

小结:VersionName 是否大于 4.7.2即支持QQ支付

如何唤起QQ,进行支付

private boolean target5_3_0(BaseApi baseApi) {Bundle bundle = new Bundle();baseApi.toBundle(bundle);try {String packageName;if (TextUtils.isEmpty(packageName = this.mContext.getPackageName())) {return false;} else {bundle.putString("_mqqpay_payapi_packageName", packageName);Intent intent;(intent = new Intent()).setAction("android.intent.action.VIEW");Uri uri = Uri.parse("mqqwallet://open_pay/");intent.setData(uri);intent.setPackage("com.tencent.mobileqq");intent.putExtras(bundle);intent.addFlags(0x10000000).addFlags(0x8000000);this.mContext.startActivity(intent);return true;}} catch (Exception var4) {var4.printStackTrace();return false;}
}private boolean target4_7_2(BaseApi baseApi) {try {String packageName;if (TextUtils.isEmpty(packageName = this.mContext.getPackageName())) {return false;} else {Bundle bundle = new Bundle();baseApi.toBundle(bundle);bundle.putString("_mqqpay_baseapi_pkgname", packageName);Intent intent;(intent = new Intent()).setAction("android.intent.action.VIEW");Uri uri = Uri.parse("mqqwallet://open_pay/");intent.setData(uri);intent.setPackage("com.tencent.mobileqq");intent.putExtras(bundle);intent.addFlags(0x10000000).addFlags(0x8000000);this.mContext.startActivity(intent);return true;}} catch (Exception var4) {var4.printStackTrace();return false;}
}

梳理下来就是5.3.0包名字段的名字被改了。从 _mqqpay_baseapi_pkgname 改成了 _mqqpay_payapi_packageName

OK,要如何唤起QQ钱包?

step 1 把支付的参数都带上

step 2 发起 mqqwallet 的广播,限定广播调用者必须是QQ这个应用 com.tencent.mobileqq

step 3 intent带上了两个flag,

FLAG_RECEIVER_FOREGROUND:设定广播为前台广播,提升广播的优先级。
FLAG_ACTIVITY_MULTIPLE_TASK :如果在创建新文档时设置 FLAG_ACTIVITY_MULTIPLE_TASK 标志,则系统始终会以目标 Activity 作为根创建新任务。

PS:个人认为加多一个 FLAG_ACTIVITY_NEW_TASK 或许效果更好。但是去掉这个标志位,更有APP内付费的意味。

step 4 其他逻辑都是走系统的Intent逻辑,那么系统的逻辑是怎么走的呢?参考下一小节如何自定义scheme?

如何自定义scheme?

package A

step 1 在AndroidMainfest中

<activityandroid:name="org.yeshen.browser.CallActivity"android:exported="true"android:launchMode="singleTop"><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:host="yeshen.org"android:scheme="ys" /></intent-filter>
</activity>

step 2 设置接收的Activity。

package org.yeshen.browser;import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebView;public class CallActivity extends Activity {private WebView mWebView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mWebView = findViewById(R.id.webview);onNewIntent(getIntent());}protected void onNewIntent(Intent intent) {if (intent == null) return;if (Intent.ACTION_VIEW.equals(intent.getAction())) {if (intent.getData() != null) {Uri uri = intent.getData();Log.e("CallActivity", uri.toString() + "," + uri.getQueryParameter("current"));}String url = intent.getStringExtra("open");if (!TextUtils.isEmpty(url)) {mWebView.loadUrl(url);}}}
}

package B

唤起应用并传递数据。

try{String url ="ys://yeshen.org?current="+System.currentTimeMillis();Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));intent.putExtra("open","http://yeshen.org");intent.setPackage("org.yeshen.browser");intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);
}catch (Exception e){e.printStackTrace();
}

回调的处理也是类似的,就是把这个过程颠倒过来而已。

PS: 这个SDK有没有优化的空间?

1 使用AAR,XML也一起包起来会更好。(aar可以兼容eclipse,有需要再做一点转化)

2 支付数据直接服务端下发就好,我期望是只有一个参数。一个接口。

比如我自己包装的qqPay就能达到这种要求。

void qqPay(final @NonNull Activity activity, final @NonNull String payData, final @NonNull OnPayment callback);

3 H5支付的连接在原生支付这里也要可以直接用。

1、2、3点都能明显减低开发者的接入成本。

如何鉴定是来自可信的客户端请求

支付安全本质上是服务端的同学来保证的。但是客户端也能在一定程度上做识别。比如包名识别,比如签名校验。

看到上面QQ-SDK支付是把应用的packageName传了给QQ主应用。所以问题有几个:

如何根据包名查其他应用的签名

系统有现成的API。

PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);

PS:其实这里有一个非技术层面的问题,签名错了要不要给支付?给支付是出于什么原因,不给支付又是出于什么原因。处理这种业务逻辑的时候,要多想一点,技术上要有冗余。

Intent的接受者能知道/获得CallerActivity的信息吗?

Activity.getReferrer()

Android 5.1+支持 大概这样使用:

//activity.getReferrer()
try {Class activityClass = Activity.class;@SuppressWarnings("JavaReflectionMemberAccess")Field refererField = activityClass.getDeclaredField("mReferrer");refererField.setAccessible(true);return (String) refererField.get(activity);
} catch (Exception e) {return "";
}

还有更通用的方法?Activity.getCallingPackage()

Android5.1之下需要调用者配合,调用者可以是这样调用:

String url = "ys://yeshen.org?current=" + System.currentTimeMillis();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.putExtra("open", "http://yeshen.org");
intent.setPackage("org.yeshen.browser");
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivityForResult(intent,1);

关键在于 startActivityForResult + 去掉 FLAG_ACTIVITY_NEW_TASK

回过头来看QQ支付中的 _mqqpay_payapi_packageName 是可以去掉的,但是为何没有去掉,也许是兼容性问题?也许是设计者没考虑周全?

其他

测试过程中,测试工程师发起调用之后,唤起了QQ,但是没唤起QQ支付界面(必现)。卸载我们的应用重装之后又无法复现了。我看到信息的时候已经晚了。
靠猜测,也许是刚安装之后,QQ本身缓存了一些应用信息,在缓存的信息没更新导致查不到应用信息?

QQ钱包的接入及其设计分析相关推荐

  1. android qq钱包接入,北京接入QQ钱包让公交充值实现“秒付”

    原标题:北京接入QQ钱包让公交充值实现"秒付" 近日,腾讯旗下QQ钱包基于NFC功能推出的城市一卡通充值功能在北京正式上线,这意味着,北京市民今后只需要在手机上即可快速进行公交卡充 ...

  2. QQ小程序支付 QQ钱包支付 微信支付

    前言 由于公司业务需要,最近这段时间对接了QQ小程序支付[包括QQ钱包支付 和 QQ小程序内微信支付],由于网络上相关的资料很少,遂留此文,以备后用.[顺便吐槽一下,官方文档不可全信] 由于业务关系, ...

  3. H5开发在QQ钱包的应用实践

    内容来源:2017年6月24日,腾讯前端高级工程师周明礼在"腾讯Web前端大会 TFC 2017 "进行<QQ钱包h5应用开发实践>演讲分享.IT 大咖说作为独家视频合 ...

  4. php qq钱包扫码接口,php最新版qq钱包扫码支付源码

    代码效果图 代码说明 php最新版qq钱包扫码支付源码,这套qq钱包扫码支付,经过云码素材修改,已经完全能正常使用,云码素材本站也在使用,如果你想测试一下,要以登录到会员中心,充值中使用qq钱包扫码支 ...

  5. php开发微信商户平台支付宝支付,不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...

    我们知道,开发微信支付需要申请服务号并且需要一系列的资料才可以开通.怪麻烦的,现在我们可以用第三方开放的免签约微信支付接口,支付宝接口和QQ钱包接口,实现实时到帐的微信支付开发. 我们只需要在第三方的 ...

  6. 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...

    我们知道,开发微信支付需要申请服务号并且需要一系列的资料才可以开通.怪麻烦的,现在我们可以用第三方开放的免签约微信支付接口,支付宝接口和QQ钱包接口,实现实时到帐的微信支付开发. 我们只需要在第三方的 ...

  7. qq钱包php开发文档,QQ轻应用

    QQ轻应用开发者文档 一.背景 QQ轻应用是运行在手Q中的轻应用,采用兼容微信的方式来运作,除了少数几个组件和API跟微信稍微不同外,其他API和组件都兼容微信小程序.这样好处是极大减少了第三方开发轻 ...

  8. abc云支付php,凉秋易支付,免签约支付平台,彩虹易支付,abc云支付云钱包,云支付,云结算,支付接口,支付营销,易支付,微信支付,支付宝,QQ钱包,个人支付接口,免签支付接口...

    支付接口介绍 使用此接口可以实现支付宝.QQ钱包.微信支付与财付通的即时到账,免签约,无需企业认证.接口API地址是:http://pay.lqan.cn/ 本文阅读对象:商户系统(在线购物平台.人工 ...

  9. Lambda钱包API接入教程

    钱包API接入文档说明 参考钱包版本: Wallet0.4.40 钱包账户信息文档文档: 钱包账户信息.签名.配置文件说明 主网的钱包服务地址: 39.107.247.86:13659 测试网的钱包服 ...

  10. 聚合支付:将支付宝、微信、qq钱包三码收款码合而为一

    很急很关键,代码先睹为快,请点我 项目搭建基于ssm框架,本博客略过搭建过程,记得引入文件上传.zxing.mybatis.mysql依赖包 <!-- https://mvnrepository ...

最新文章

  1. Oracle 10g(10.2.0.4)升级到10.2.0.5.19
  2. python一次性读取整个文件-python-文件中的行是否读取整个文件
  3. Web安全1沙箱隔离
  4. error2---BeginPath和EndPath之间的TextOut无法显示
  5. python单步调试的方法_python断点调试方法
  6. BZOJ 1725: [Usaco2006 Nov]Corn Fields牧场的安排
  7. 逻辑斯谛(Logistic)回归、参数估计教程
  8. 海思Hi3516--移动侦测原理与理解
  9. 用户生命周期价值及产品运营策略
  10. [SARscape] 多时相SAR影像的应用 - 监督分类、提取水稻种植区 - 以Sentinel-1A数据为例
  11. java date clone_Java Date clone()方法与示例
  12. 死磕源码系列【springboot项目打印is not eligible for getting processed by all BeanPostProcessors (for example: n
  13. FLASH多文件上传组件
  14. 移远EC20在linux下驱动移植
  15. 云呐|固定资产管理系统的主要功能有哪些
  16. 1分钟让别人喜欢你(一)
  17. 详细记录Word文档(包含doc文件和docx文件的上传图片会回显)转Html实现前端预览
  18. unity3d 模拟电脑实现_基于.Unity3D的三维虚拟电脑组装实验系统开发毕业设计(含源文件).doc...
  19. 打开计算机服务的五种方法
  20. ssm毕设项目美容院管理系统jn8j9(java+VUE+Mybatis+Maven+Mysql+sprnig)

热门文章

  1. 西安火车站迎来“大手术”
  2. 抛开时代背景,任何技术讨论都是瞎耽误功夫
  3. 回到未来2——货币战争
  4. 能上QQ但是打不开网页
  5. vb c语言入门教程,c语言和vb哪个简单
  6. js替换和全部替换字符串标点
  7. python包 —rdkit 安装
  8. 用c语言解三角函数公式大全初中,初中三角函数公式大全
  9. 一次函数的斜率公式_一次函数斜率公式是什么?
  10. 昨天晚上全新打造N无线AP