联运安卓接入文档
阅读前提
-
本文档主要是为游戏接入字节联运SDK提供指引,如果有任何接入上的困难,可以随时和技术支持同学沟通;
-
本文档包含客户端接入流程和服务端接入流程,前6节部分为客户端接入文档,服务端接入流程请跳至第7节查阅;
-
对于接入过旧版本普通联运SDK(版本号1.X.X)的cp,请注意config.json中的union_mode必须设置为0。由于接口调用变动,升级2.X.X版本,可以另外需添加下面依赖(注:不是从1.X版本升级到新版本的不需要添加)
开发环境配置
2.1 建议的开发环境
- 开发工具:要求使用Android Studio 3.0版本及以上,Gradle版本建议5.4.1版本及以上,Gradle插件(Android gradle plugin)版本建议3.2.2版本及以上;
- SDK支持版本:最低支持Android 4.4以上版本(minSdkVersion>=17,targetSdkVersion建议>=26);
- Android abi限制:请不要限制
abiFilters
仅为'armeabi'
,否则可能出现功能异常,请至少保留'armeabi-v7a'
; - 特别注意:游戏包体的VersionCode以及VersionName必须设置,不要留空。
2.2 下载SDK Demo工程以及aar资源
自测用例:
2.3 在游戏中引入SDK依赖
(1)使用GBSDK-helper接入(推荐)
GBSDK-helper是字节游戏SDK针对游戏接入SDK处理冲突难的问题,专门开发的接入辅助工具,如果游戏的接入使用了以下接入方式:
- Gradle参与进行的渠道apk构建/游戏APK构建;
- 渠道融合工具出包;
- Unity导出Android Studio工程接入;
针对以上接入方式,我们都推荐使用GBSDK-helper进行接入,否则请使用传统的aar依赖接入方式。
添加Maven源依赖
首先在渠道包工程或者游戏工程的根目录下build.gradle文件中添加 Maven源以及依赖,如下所示:
buildscript {
repositories {
maven {
url 'https://artifact.bytedance.com/repository/ttgamesdk/'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2' //打包建议最小的gradle plugin版本
classpath 'com.bytedance.ttgame:gbsdk_helper:1.0.1'
}
}
allprojects {
repositories {
maven {
url 'https://artifact.bytedance.com/repository/ttgamesdk/'
}
}
}
依赖SDK
在 Android Application 或 Library 模块的 build.gradle 末尾添加以下配置,请通过接入工具获取配置。
接入工具:
Windows版
不支持在 Docs 外粘贴 block
MacOS版
不支持在 Docs 外粘贴 block
Linux版
不支持在 Docs 外粘贴 block
配置代码如下所示:
apply plugin: 'gbsdk.helper'
gbsdk {
appId 'xxx'
version 'xxx'
plugin true
localDepend true
autoDeConflict true
optionals (['applog', 'aweme', 'union'] as String[])
}
(2)使用AAR包接入
推荐优先使用方式(1)接入,如果上述方式接入遇到较大问题,或者不支持gradle,可以和技术支持同学获取AAR包接入。直播联运-Unity接入aar包方式说明
(3)使用远程依赖接入
注意,使用远程依赖方式接入,需要自行处理好资源id错误的问题,否则可能引起启动崩溃。
以下$version字段可替换为最新版本号:2.0.3.1
api "com.bytedance.ttgame:gbsdk_common_host:$version"
api "com.bytedance.ttgame:gbsdk_common_plugin:$version"
api "com.bytedance.ttgame:gbsdk_optional_aweme:$version"
api "com.bytedance.ttgame:gbsdk_optional_union_plugin:$version"
api "com.bytedance.ttgame:gbsdk_optional_applog:$version"
Tips:上述版本使用了android support依赖库,如果游戏使用了androidx相关依赖库,且希望以插件形式接入,则需要改用下面的依赖:
api "com.bytedance.ttgame:gbsdk_common_host:$version"
api "com.bytedance.ttgame:gbsdk_common_plugin_androidx:$version"
api "com.bytedance.ttgame:gbsdk_optional_aweme:$version"
api "com.bytedance.ttgame:gbsdk_optional_union_plugin_androidx:$version"
api "com.bytedance.ttgame:gbsdk_optional_applog:$version"
如果使用了androidx相关依赖库,需要在gradle.properties
添加下面的语句:(注:android不需要添加)
android.useAndroidX=true
android.enableJetifier=true
2.4 配置build.gradle
CP需要在build.gradle文件中的defaultConfig模块下添加下述字段
multiDexEnabled true
//埋点数据实时验证功能,如果不需要可以填其他值,比如应用包名,详见以下文档:https://datarangers.com.cn/help/doc?lid=2230&did=45237
manifestPlaceholders.put("APPLOG_SCHEME", "union_game")
2.5 配置SDK参数文件config.json
当完成上面几步之后,需要在src/main/assets目录下添加config.json文件(可参考demo工程),config.json里需要配置特定的参数:
{
"app_id": "游戏的appid",
"screen_orientation": "sensorPortrait",
"union_mode": 1 //直播联运为1,普通联运为0,若不清楚可联系技术同学获取自身union_mdoe
}
请注意,该文件请确保json格式的正确,不要添加无效值,也不要随意删减内容。
参数含义说明:
app_id:请填写创建游戏后下发的appid参数,请注意appid在创建应用成功的通知邮件中
screen_orientation:屏幕方向,sensorPortrait为竖屏,sensorLandscape为横屏
union_mode:以直播联运的方式接入,填1;以普通联运接入,填0
is_necessary_permission:(可选字段)设置为false,则在无权限的情况下也可正常初始化,进入游戏,反之则必须在有权限的情况下才能初始化成功。不填写默认为true。
SDK初始化
请注意,在调用SDK的任何其他功能前,请确保SDK初始化成功,否则可能会有不可预期的错误。
游戏需要修改application的实现,以初始化渠道SDK,可以使用直接继承或者间接调用的方式进行处理
3.1 继承Application
3.1.1 直接继承Application(二选一)
游戏的Application直接继承com.bytedance.ttgame.tob.common.host.api.GBApplication,如下:
import com.bytedance.ttgame.tob.common.host.api.GBApplication;
public class GameApplication extends GBApplication {
//...
}
3.1.2 间接调用Application(二选一)
如果由于特殊原因,游戏的application无法继承GBApplication,可以在游戏的application实现以下代码:
import android.app.Application;
public class GameApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//请对齐游戏的application的attachBaseContext时机,不要延后调用
GBCommonSDK.attachBaseContext(base);
}
@Override
public void onCreate() {
super.onCreate();
//请对齐游戏的application的onCreate时机,不要延后调用
GBCommonSDK.onCreate(this);
}
}
3.2 初始化
在游戏Activity的主线程调用GBCommonSDK的init方法进行初始化:
GBCommonSDK.init(activity, new InitCallback() {
@Override
public void onSuccess() {
Toast.makeText(activity, "初始化成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed(int errorCode, String errorMsg) {
Toast.makeText(activity, "初始化失败: " + errorCode + ", " + errorMsg, Toast.LENGTH_SHORT).show();
}
});
初始化错误码
错误码 | 错误含义 | 建议处理方式 |
-1 | 未知错误 | 一般是接入错误,和技术同学沟通 |
-2 | preInit失败 | |
-3 | init失败 |
在游戏主Activity(游戏画面的Activity)中尽早调用以下方法,设置完在登录成功后会自动展示悬浮球(注:直播联运不需要添加)
GBCommonSDK.setGameActivity(this);
3.3 权限申请处理(可选)
如果游戏侧希望接管游戏的权限申请,可以通过以下方法注入一个权限申请拦截器:
PermissionInterceptor.setPermissionRequestHandler(new IPermissionRequestHandler() {
/**
* SDK请求权限
* @param permissions 需要申请的权限
* @param callback 当权限申请有结论时,需要手动回调给该callback
* @return 是否进行处理,如果进行处理,则返回true,否则返回false,返回false时SDK内部会自动进行权限的处理。
*/
@Override
public boolean requestPermission(@NonNull String[] permissions, @Nullable IPermissionReqListener callback) {
Toast.makeText(MainActivity.this, "申请权限中", Toast.LENGTH_SHORT).show();
//当前SDK需要申请permissions这个数组的权限,游戏可以代为处理
//这里进行处理,利用context启动权限申请之类的操作...
//处理结果需要回调给游戏
if (callback != null) {
/**
* 多条权限申请回调
* @param isAllGranted 是否全部已经授权
* @param permissions 申请的权限详情,The requested permissions. Never null.
* @param grantResults 和第二个参数一一对应的申请结果,The grant results for the corresponding permissions
* which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}
* or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.
*/
//这里是回调了所有权限申请失败的结果。如果代为申请成功了,那么需要具体再调整
callback.onPermissionRequest(false, permissions, new int[permissions.length]);
}
//如果不希望SDK再进行权限申请的处理,返回true,否则返回false
return false;
}
});
登录能力
4.1 功能介绍
集成抖音授权登录,为游戏联运提供账号体系
4.2 方法声明
账号相关接口声明:
/**
* 是否已登录
*/
boolean isLogin();
/**
* 唤起登录弹窗
*/
void login(Activity context, IAccountCallback<UserInfoResult> callback);
/**
* 切换帐号
*/
void switchLogin(Activity context, ISwitchCallback<UserInfoResult> callback);
/**
* 退出登录
*/
void logout(Activity context, IAccountCallback<UserInfoResult> callback);
返回结果声明:
//登录结果类声明
public class UserInfoResult {
//错误码
public int code;
//错误信息
public String message;
//用户信息
public UserInfo data;
}
//用户信息类声明
public class UserInfo {
private String accessToken;//token
private ExtraData extraData;//扩展字段
}
//扩展字段声明
public class ExtraData {
private boolean isGuest; //是否游客
private int userType; //用户类型,绑定后为绑定的用户类型
private long userId; //uid
private int identityType;//云控返回的用户实名认证的等级 1=low,2=mid,3=high
//其中userType用户类型如下:
public static final int AWEME = 4; //抖音
}
4.3 调用示例
发起登录
在SDK初始化完成的前提下,调用以下方法,可以弹出一个登录弹窗:
GBCommonSDK.getService(IUnionService.class).login(this, new IAccountCallback<UserInfoResult>() {
@Override
public void onSuccess(@Nullable UserInfoResult userInfoResult) {
//获取用户token、extraData(isGuest是否游客、userType用户类型)
//接入方根据token,交给游戏服务端并与聚合sdk服务器进行交互获取sdk_open_id,
//verifyGameUser(userInfoResult);
}
@Override
public void onFailed(@Nullable UserInfoResult userInfoResult) {
//获取token失败
Toast.makeText(AccountActivity.this, "登录失败," + gson.toJson(userInfoResult), Toast.LENGTH_SHORT).show();
}
});
判断登录状态
通过以下方法判断游戏是否已经登录:
GBCommonSDK.getService(IUnionService.class).isLogin()
切换登录账号
如果游戏内有切换登录账号的按钮,可以在**已经登录的情况下,**调用以下接口打开一个切换登录界面:
GBCommonSDK.getService(IUnionService.class).switchLogin(this, new ISwitchCallback<UserInfoResult>() {
@Override
public void onSuccess(@Nullable UserInfoResult userInfoResult) {
//获取用户token、extraData(isGuest是否游客、userType用户类型)
//接入方根据token,交给游戏服务端并与聚合sdk服务器进行交互获取sdk_open_id,
//verifyGameUser(userInfoResult);
}
@Override
public void onFailed(@Nullable UserInfoResult userInfoResult) {
Toast.makeText(AccountActivity.this, "切换登录失败, " + gson.toJson(userInfoResult), Toast.LENGTH_SHORT).show();
}
@Override
public void onLogout(@Nullable UserInfoResult userInfoResult) {
Toast.makeText(AccountActivity.this, "账号登出", Toast.LENGTH_SHORT).show();
}
});
退出登录
如果游戏内有退出登录账号的按钮,可以在**已经登录的情况下,**调用以下接口退出登录状态:
GBCommonSDK.getService(IUnionService.class).logout(this, new IAccountCallback<UserInfoResult>() {
@Override
public void onSuccess(@Nullable UserInfoResult result) {
Toast.makeText(AccountActivity.this, result != null ? result.message : "Result is null", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed(@Nullable UserInfoResult result) {
Toast.makeText(AccountActivity.this, result != null ? result.message : "Result is null", Toast.LENGTH_SHORT).show();
}
});
4.4 错误码
错误码 | 描述 | 推荐处理 |
201101 | 未实名用户禁止登录 | |
201102 | 游客禁止登录 | |
101101 | 亲爱的玩家,根据未成年人保护规则,您只能于节假日、周五、六、日的20点-21点登录游戏。您今日无法继续体验游戏,请合理安排时间 |
实名认证与防沉迷功能
SDK内部已内嵌游戏防沉迷逻辑,游戏内可以去除同样功能的防沉迷功能。
5.1 实名认证
(1)接口声明
进行实名认证的接口定义如下所示(IUnionService类):
//IUnionService.java
/**
* 触发实名认证接口
*
* @param context 游戏的activity
* @param realNameType 实名认证类型(走网络配置,实名认证可关闭,实名认证不可关闭)
* @param callback 回调
*/
void realNameVerify(Activity context, @RealNameType int realNameType, IAccountCallback<UserInfoResult> callback);
/**
* 判断用户是否实名
*/
void isVerify();
RealNameType枚举如下所示:
public class RealNameType {
//走网络配置,由云控处理
public static final int NETWORK_TYPE = 0;
//弹出可关闭
public static final int OPTION_TYPE = 2;
//必须实名认证
public static final int FORCE_TYPE = 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NETWORK_TYPE, OPTION_TYPE, FORCE_TYPE})
public @interface realName {
}
}
IAccountCallback的定义如下所示:
public interface IAccountCallback<UserInfoResult> {
//
@MainThread
void onSuccess(@Nullable UserInfoResult result);
@MainThread
void onFailed(@Nullable UserInfoResult exception);
}
返回结果UserInfoResult定义如下:
public class UserInfoResult {
public int code;
public String message;
}
(2)调用示例
判断用户是否实名
GBCommonSDK.getService(IUnionService.class).isVerify()
触发实名认证
如果用户未实名,在合适的场景,可以调用以下接口,触发一次实名弹窗:
GBCommonSDK.getService(IUnionService.class).realNameVerify(this, RealNameType.NETWORK_TYPE, new IAccountCallback<UserInfoResult>() {
@Override
public void onSuccess(@Nullable UserInfoResult result) {
Toast.makeText(AccountActivity.this, result != null ? result.message : "Result is null", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed(@Nullable UserInfoResult result) {
Toast.makeText(AccountActivity.this, result != null ? result.message : "Result is null", Toast.LENGTH_SHORT).show();
}
});
错误码
错误码 | 描述 | 推荐处理 |
0 | 成功 | |
-1 | 用户取消认证 | |
-1202 | 该帐号已经实名认证过 | 提示已实名 |
5.2 查询用户年龄
(1) 接口声明
查询年龄枚举值的接口定义如下所示(IUnionService类):
/**
* 年龄枚举 -1:未实名 8:0-8岁 16:8-16岁 18:16-18岁 100:大于18岁 左闭右开
*/
int getAgeType();
(2)调用示例
GBCommonSDK.getService(IUnionService.class).getAgeType()
5.3 防沉迷相关
(1)时间限制规则
无法复制加载中的内容
(2)SDK提示方式
无法复制加载中的内容
支付能力
接入方调用该接口,传递支付请求参数和回调接口,进行支付。回调通知地址,在构建支付信息时上送即可。
6.1 发起支付
(1)接口声明
进行下单支付的接口定义如下所示:
/**
* 下单支付
*
* @param activity 游戏支付页面
* @param payInfo 支付参数
* @param callback 回调
*/
void pay(Activity activity, PayInfo payInfo, IPayCallback<PayResult> callback);
PayInfo的定义如下:
public String cpOrderId;// 订单id,长度限制为80字节;
public String sdkOpenId;// 登录成功之后的sdkOpenId,可以在登录验证接口获取
public int amountInCent; // 金额,单位分
public String productId; // 商品id,长度限制为80字节
public String productName; // 商品名称,长度限制为100字节,注:需体现所购买商品名称和数量
public String productDesc; // 商品描述,长度限制为20字节
public String callbackUrl; // 回调地址 CP上送即可,不需要另外单独配置
public String extraInfo; // 游戏自定义信息,长度限制为255字节
IPayCallback的定义如下:
public interface IPayCallback<PayResult> {
@MainThread
void onSuccess(@Nullable PayResult result);
@MainThread
void onFailed(@Nullable PayResult result);
}
(2)调用示例
private void realPay(String cpOrderId, int amount, String productId, String productName, String productDesc,
String callbackUrl, String extraInfo, IPayCallback<PayResult> callback) {
PayInfo payInfo = new PayInfo();
payInfo.setAmountInCent(amount);
payInfo.setCallbackUrl(callbackUrl);
payInfo.setCpOrderId(cpOrderId);
payInfo.setExtraInfo(extraInfo);
payInfo.setProductDesc(productDesc);
payInfo.setProductId(productId);
payInfo.setProductName(productName);
payInfo.setSdkOpenId(AccountActivity.sSdkOpenId);
GBCommonSDK.getService(IUnionService.class).pay(this, payInfo, callback);
}
(3)支付限制规则
无法复制加载中的内容
(4)支付错误码
无法复制加载中的内容
6.2 支付成功全局监听
SDK悬浮球内支持对部分历史订单进行支付,对这部分订单可以设置全局的监听,收到监听结果后及时给用户发货,更新游戏界面,优化用户体验。
监听接口:
public interface PaySuccessListener {
/**
* 直接支付的订单
*/
int TYPE_DIRECT_PAY = 1;
/**
* 历史订单
*/
int TYPE_HISTORY = 2;
/**
* @param type {@link PaySuccessListener#TYPE_DIRECT_PAY} {@link PaySuccessListener#TYPE_HISTORY}
*/
void onPaySuccess(int type, String cpOrderId, String productId);
}
调用示例:
GBCommonSDK.getService(IUnionService.class).setPaySuccessListener(new PaySuccessListener() {
@Override
public void onPaySuccess(int type, String cpOrderId, String productId) {
// TODO
}
});
广告能力(可选)
注:仅支持普通联运项目,直播联运暂不支持广告
7.1 公共类
AdConfig
用于在加载广告时配置广告的参数,通过 AdConfig.Builder 进行构建,其参数如下:
private boolean isAutoPlay; // 是否自动播放视频
private String codeId; // 穿山甲平台上的广告位ID
private int imageAcceptedWidth; // 广告图片的宽度,单位px
private int imageAcceptedHeight; // 广告图片的高度,单位px
private float expressViewAcceptedWidth; // 指定广告View的宽度,单位dp
private float expressViewAcceptedHeight; // 指定广告View的高度,单位dp
private boolean isSupportDeepLink; // 是否支持Deep Link
private int adCount; // 加载广告的数量
private String rewardName; // 激励视频广告奖励的名字
private int rewardAmount; // 激励视频广告奖励的数量
private String mediaExtra; // 附加参数
private String userId; // 激励视频广告需指定用户ID
private int orientation; // 视频的方向
private int nativeAdType; //
private int adloadSeq; //
private String primeRit; //
private int[] externalABVid; //
ShowableAd
ShowableAd是一个接口,表示不依赖容器显示的广告,用于插屏广告、激励视频广告和全屏视频广告中。接口如下:
boolean hasShown(); // 是否已经显示过
void show(Activity activity); // 可以在合适时机调用以显示广告
7.2 初始化
7.2.1 接口声明
调用广告相关的接口前,必须先调用以下的接口进行初始化:
boolean initAdSdk(Activity, InitConfig)
返回true表示初始化成功,返回false表示初始化失败,失败的可能原因:
- 已经初始化
- 获取AppId失败,请确认应用是否包含广告能力
- 插件未加载,调用本接口前请确保已经调用 GBCommonSDK.init() 接口
7.2.2 相关类说明
InitConfig包含穿山甲SDK的初始化参数,通过InitConfig.Builder进行构建,其参数如下:
private String appName; // 必填,应用名
private int titleBarTheme; // 标题栏主题
private boolean allowShowNotify; // 是否允许sdk展示通知栏提示
private boolean allowShowPageWhenScreenLock; // 是否在锁屏场景支持展示广告落地页
private int[] directDownloadNetworkType; // 允许直接下载的网络状态集合
private boolean debug; // 调试开关,上线后需要改为release
7.2.3 调用示例
InitConfig config = new InitConfig.Builder()
.appName(APP_NAME)
.titleBarTheme(InitConfig.TITLE_BAR_THEME_DARK)
.directDownloadNetworkType(InitConfig.NETWORK_STATE_WIFI)
.allowShowNotify(true)
.allowShowPageWhenScreenLock(true)
.build();
if (GBCommonSDK.getService(IAdService.class).init(this, config)) {
Toast.makeText(this, "初始化成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "初始化失败", Toast.LENGTH_SHORT).show();
}
7.3 添加/移除应用下载监听
7.3.1 接口声明
添加或移除全局的广告应用下载的监听:
void addAdAppDownloadListener(AdAppDownloadListener) // 注册App下载监听
void removeAdAppDownloadListener(AdAppDownloadListener) // 移除App下载监听
7.3.2 相关类说明
AppDownloadListener:用于监听广告对应的应用程序下载的状态,接口如下:
void onIdle(); // 空闲
void onDownloadActive(long totalBytes, long currBytes, String fileName, String appName); // 下载中
void onDownloadPaused(long totalBytes, long currBytes, String fileName, String appName); // 暂停下载
void onDownloadFailed(long totalBytes, long currBytes, String fileName, String appName); // 下载失败
void onDownloadFinished(long totalBytes, String fileName, String appName); // 下载完成
void onInstalled(String fileName, String appName); // 安装完成
7.3.3调用示例
private AppDownloadListener mDownloadListener = new AppDownloadListener.Stub() {
@Override
public void onIdle() {
Log.d(TAG, "onIdle");
}
@Override
public void onDownloadActive(long totalBytes, long currBytes, String fileName, String appName) {
Log.d(TAG, "onDownloadActive");
}
@Override
public void onDownloadPaused(long totalBytes, long currBytes, String fileName, String appName) {
Log.d(TAG, "onDownloadPaused");
}
@Override
public void onDownloadFailed(long totalBytes, long currBytes, String fileName, String appName) {
Log.d(TAG, "onDownloadFailed");
}
@Override
public void onDownloadFinished(long totalBytes, String fileName, String appName) {
Log.d(TAG, "onDownloadFinished");
}
@Override
public void onInstalled(String fileName, String appName) {
Log.d(TAG, "onInstalled");
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GBCommonSDK.getService(IAdService.class).addAppDownloadListener(mDownloadListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
GBCommonSDK.getService(IAdService.class).removeAppDownloadListener(mDownloadListener);
}
7.4 开屏广告
7.4.1 接口声明
void loadSplashAd(AdConfig, SplashLoadListener, SplashInteractionListener)
第一个参数为广告配置;第二个参数为广告加载的监听;第三个参数为广告状态的监听。
7.4.2 相关类说明
SplashLoadListener接口如下所示:
void onLoad(View view); // 加载成功,获取到广告View
void onTimeout(); // 加载超时
void onError(int code, String msg); // 加载出错,code为状态码,可对照穿山甲平台的错误码
加载成功后,获取到广告的View,需要将其放到对应的容器中进行显示。
SplashInteractionListener接口如下所示:
void onAdClicked(View view, int type); // 广告被点击,会自动跳转到落地页
void onAdShow(View view, int type); // 广告显示
void onAdSkip(); // 广告点击跳过
void onAdTimeOver(); // 广告时间到
需要实现 onAdSkip()
和 onAdTimeOver()
方法执行操作,例如关闭开屏广告页并跳转到其他页面。
7.4.3 调用示例
AdConfig config = new AdConfig.Builder()
.codeId("xxx")
.supportDeepLink(true)
.imageAcceptedSize(1080, 1920)
.build();
GBCommonSDK.getService(IAdService.class).loadSplashAd(config, new SplashLoadListener() {
@Override
public void onLoad(View view) {
mRoot.removeAllViews();
mRoot.addView(view);
}
@Override
public void onTimeout() {
finish();
}
@Override
public void onError(int code, String msg) {
finish();
}
}, new SplashInteractionListener.Stub() {
@Override
public void onAdSkip() {
finish();
}
@Override
public void onAdTimeOver() {
finish();
}
});
7.5 Banner广告
7.5.1 接口声明
void loadBannerAd(AdConfig, ExpressLoadListener, ExpressInteractionListener, @Nullable DislikeCallback)
第一个参数为广告配置;第二个参数为广告加载的监听;第三个参数为广告状态的监听,第四个参数为dislike的响应,可以为空,如果为空,dislike点击没有反应。
7.5.2 相关类说明
ExpressLoadListener接口如下所示:
void onAdLoad(int size); // 获取到广告数据,size为数量
void onError(int code, String msg); // 加载出错,code为状态码,可对照穿山甲平台的错误码
onAdLoad()调用时,已经获取到广告数据,但渲染后的View需要通过ExpressInteractionListener接口获取。
ExpressInteractionListener接口如下所示:
void onAdClicked(View view, int type); // 广告被点击
void onAdShow(View view, int type); // 广告显示
void onAdDismiss(); // 广告消失
void onRenderSuccess(View view, float width, float height, @Nullable ShowableAd ad); // 渲染成功
void onRenderFail(View view, String msg, int code); // 渲染失败
在 onRenderSuccess()
接口可以获得渲染后的view,对于Banner广告,把view添加到容器中显示即可,ShowableAd的参数对Banner广告而言一定为空,使用Banner广告不需要关注这个参数。
DislikeCallback需要实现的接口如下所示:
public abstract void onSelected(int position, String value);
public abstract void onCancel();
public abstract void onShow();
当Dislike具体的选项被点击后,onSelected()
会被调用,可以在方法里做出具体的响应,例如隐藏广告或是加载一条新的Banner广告。创建 DislikeCallback 需要传递一个 Activity 对象。
7.5.3 调用示例
AdConfig config = new AdConfig.Builder()
.codeId("xxx")
.supportDeepLink(true)
.expressViewAcceptedSize(dm.widthPixels / dm.density, 50) // // 必须指定,否则View大小为0
.adCount(1)
.build();
GBCommonSDK.getService(IAdService.class).loadBannerAd(config, new ExpressLoadListener() {
@Override
public void onAdLoad(int size) {
}
@Override
public void onError(int code, String msg) {
}
}, new ExpressInteractionListener.Stub() {
@Override
public void onRenderSuccess(View view, float width, float height, @Nullable ShowableAd ad) {
mBannerContainer.removeAllViews();
mBannerContainer.addView(view);
}
}, new DislikeCallback(this) {
@Override
public void onSelected(int i, String s) {
mBannerContainer.removeAllViews();
}
@Override
public void onCancel() {
}
@Override
public void onRefuse() {
}
});
7.6 插屏广告
7.6.1 接口声明
void loadInteractionAd(AdConfig, ExpressLoadListener,ExpressInteractionListener)
第一个参数为广告配置;第二个参数为广告加载的监听;第三个参数为广告状态的监听。
插屏广告的监听接口和Banner广告接口的是一样的,接口声明可以参考Banner广告。不同的是,插屏广告是通过单独的Activity进行显示的,因此在 ExpressInteractionListener
的 onRenderSuccess()
方法参数中,可以获取到一个 ShowableAd
对象,如果不需要特殊处理,直接调用其 show()
方法进行显示即可。
7.6.2 调用示例
AdConfig config = new AdConfig.Builder()
.codeId("xxx")
.expressViewAcceptedSize(350, 350) // 必须指定,否则View大小为0
.supportDeepLink(true)
.adCount(1)
.build();
GBCommonSDK.getService(IAdService.class).loadInteractionAd(config, new ExpressLoadListener() {
@Override
public void onAdLoad(int size) {
}
@Override
public void onError(int code, String msg) {
}
}, new ExpressInteractionListener.Stub() {
@Override
public void onRenderSuccess(View view, float width, float height, @Nullable ShowableAd ad) {
if (ad != null && !ad.hasShown()) {
ad.show(AdTestActivity.this);
}
}
});
7.7 激励视频广告
7.7.1 接口声明
void loadRewardVideoAd(AdConfig, VideoLoadListener, VideoInteractionListener)
第一个参数为广告配置;第二个参数为广告加载的监听;第三个参数为广告状态的监听。
7.7.2 相关类说明
VideoLoadListener接口如下所示:
void onVideoAdLoad(ShowableAd ad); // 视频开始加载
void onVideoCached(ShowableAd ad); // 视频缓存完成
void onError(int code, String msg); // 加载出错
在 onVideoAdLoad()
方法中可以获得到一个 ShowableAd
对象,但调用 show()
方法显示视频广告,可能会卡顿,因此推荐在 onVideoCached()
方法中调用 ShowableAd.show()
显示广告,视频播放更流畅。
VideoInteractionListener接口如下所示:
void onAdShow(); // 广告显示
void onAdVideoBarClick(); // 跳转按钮被点击
void onAdClose(); // 广告关闭
void onVideoComplete(); // 视频播放完成
void onVideoError(); // 视频播放出错
void onSkippedVideo(); // 点击跳过
void onRewardVerify(boolean verify, int amount, String name, int code, String message); // 激励视频的奖励校验结果,只用于激励视频广告
7.7.3 调用示例
AdConfig config = new AdConfig.Builder()
.codeId("xxx")
.supportDeepLink(true)
.rewardName("金币")
.rewardAmount(1000)
.userId("1234")
.orientation(AdConfig.VERTICAL)
.build();
GBCommonSDK.getService(IAdService.class).loadRewardVideoAd(config, new VideoLoadListener() {
private ShowableAd mAd;
@Override
public void onVideoAdLoad(ShowableAd ad) {
mAd = ad;
}
@Override
public void onVideoCached() {
if (mAd != null && !mAd.hasShown()) {
mAd.show(AdTestActivity.this);
}
}
@Override
public void onError(int code, String msg) {
}
}, new VideoInteractionListener.Stub(){
@Override
public void onRewardVerify(boolean verify, int amount, String name) {
Toast.makeText(AdTestActivity.this, "verify = " + verify + ", amount = " + amount + ", name = " + name, Toast.LENGTH_SHORT).show();
}
});
7.8 全屏视频广告
7.8.1 接口声明
void loadFullScreenVideoAd(AdConfig, VideoLoadListener,VideoInteractionListener)
第一个参数为广告配置;第二个参数为广告加载的监听;第三个参数为广告状态的监听。全屏视频广告的监听接口和激励视频广告的接口的是一样的,接口声明可以参考激励视频广告。
7.8.2 调用示例
AdConfig config = new AdConfig.Builder()
.codeId("xxx")
.supportDeepLink(true)
.orientation(AdConfig.VERTICAL)
.build();
GBCommonSDK.getService(IAdService.class).loadFullScreenVideoAd(config, new VideoLoadListener() {
private ShowableAd mAd;
@Override
public void onVideoAdLoad(ShowableAd ad) {
mAd = ad;
}
@Override
public void onVideoCached() {
if (mAd != null && !mAd.hasShown()) {
mAd.show(AdTestActivity.this);
}
}
@Override
public void onError(int code, String msg) {
}
}, new VideoInteractionListener.Stub(){
@Override
public void onAdVideoBarClick() {
Toast.makeText(AdTestActivity.this, "click", Toast.LENGTH_SHORT).show();
}
});
服务端接入
8.1 名词解释
- app_id 每个游戏在联运平台录入信息后,会拥有一个唯一id标示,int32类型
- app_secret 与app_id唯一对应的密钥,用于双方通信鉴权
- sdk_open_id 用户唯一标识,每个游戏独立且全联运平台唯一
8.2 通用签名机制
cp与我方的所有接口均需要通过签名机制鉴权
(1) 签名算法
假设某个中台的API需要3个参数,分别是“k1”、“k2”、“k3”,它们的值分别是“v1”、“v2”、“v3”,调用中台分配给游戏这边的私钥Secret Key为“secretKey”,计算方法如下所示:
- 将请求参数格式化,以key=value格式,按照key的字母顺序从小到大排序,并用&符号拼接。如“k1=v1&k2=v2&k3=v3”。
- 将上面拼接的字符串,使用Hmac-sha1方式使用游戏方在中台申请到的secretKey加密(secretKey由发行在开发平台上申请后给到游戏服务端),然后将HMAC计算返回原始二进制数据后进行Base64编码,获得签名字符串。
注意:
- 计算签名的时候不需要对参数进行urlencode处理("application/x-www-form-urlencoded"编码),但是发送请求的时候需要进行urlencode处理,这是很多开发者最容易犯错的地方
- sign参数不参与签名
(2) 示例
比如现在要请求中台的登录验证接口,请求的参数如下:
{
"app_id": 1234,
"access_token": "q3fafa33sHFU+V9h32h0v8weVEH/04hgsrHFHOHNNQOBC9fnwejasubw==",
"ts": 1555912969,
}
1、经过第一步格式化后的字符串如下:
access_token=q3fafa33sHFU+V9h32h0v8weVEH/04hgsrHFHOHNNQOBC9fnwejasubw==&app_id=1234&ts=1555912969
2、经过第二步使用密钥(假设此时的secretKey是1ACCgaRXbazbaVzkd1NzwVc2G7wg1d6G)签名后的签名字符串如下:
sTFH83DV+Vamgr6SRsC/NNjw0+Q=
以下是通过http post请求登录验证接口的报文示例(域名根据游戏国内外和内外网类型选择对应的域名):
注意:发送请求的时候需要进行urlencode处理。
POST http://www.xxx.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
access_token=q3fafa33sHFU%2BV9h32h0v8weVEH%2F04hgsrHFHOHNNQOBC9fnwejasubw%3D%3D&app_id=1234&ts=1555912969&sign=sTFH83DV%2BVamgr6SRsC%2FNNjw0%2BQ%3D
(3) 参考代码
Golang代码实现
import (
"strings"
"sort"
"fmt"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
)
func GetSign(params map[string]interface{}, secretKey string) string {
//将key排序
keys := []string{}
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
//格式化,拼接元素
ss := []string{}
for i, k := range keys {
if i > 0 {
ss = append(ss, "&")
}
ss = append(ss, fmt.Sprintf("%v=%v", k, params[k]))
}
content := strings.Join(ss, "")
//使用密钥进行Hmac-sha1加密
mac := hmac.New(sha1.New, []byte(secretKey))
mac.Write([]byte(content))
//base64编码获得最终的sign
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func TestSign() {
params := map[string]interface{}{
"app_id": 1234,
"access_token": "q3fafa33sHFU+V9h32h0v8weVEH/04hgsrHFHOHNNQOBC9fnwejasubw==",
"ts": 1555912969,
}
fmt.Println(GetSign(params, "1ACCgaRXbazbaVzkd1NzwVc2G7wg1d6G"))
}
Java代码实现
public static String getSign(Map<String, Object> params, String secretKey){
//给参数进行排序,游戏方自己实现排序算法,通过各种方式都可以,只要实现key按字母从小到大排序即可
Map<String, Object> sortMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
sortMap.putAll(params);
//拼接成字符串
StringBuilder sb = new StringBuilder();
Iterator<String> iterator = sortMap.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String value = String.valueOf(sortMap.get(key));
sb.append(key).append("=").append(value);
if(iterator.hasNext()){
sb.append("&");
}
}
//使用密钥进行Hmac-sha1加密
Mac mac;
try {
mac = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
SecretKeySpec spec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
try {
mac.init(spec);
} catch (InvalidKeyException e) {
e.printStackTrace();
}
mac.update(sb.toString().getBytes());
//base64编码获得最终的sign
return Base64.encodeBase64String(mac.doFinal());
}
C++代码实现
//g++ -g xx.cpp -lcrypto -std=c++11
#include <iostream>
#include <string>
#include <sstream>
#include <map>
#include <string.h>
#include <stdlib.h>
#include <openssl/buffer.h>
#include <openssl/bio.h>
#include <openssl/hmac.h>
void Base64Encode(const std::string& buffer, std::string& b64text)
{
BIO *bio, *b64;
BUF_MEM *bufferPtr;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); //Ignore newlines - write everything in one line
BIO_write(bio, buffer.data(), buffer.size());
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bufferPtr);
b64text = std::string(bufferPtr->data, bufferPtr->length);
BIO_free_all(bio);
}
void GetSign(const std::string& secretKey, const std::map<std::string, std::string>& params, std::string& sign) {
std::stringstream ss;
bool first = true;
for(auto pair: params) {
if (!first) { ss << "&"; }
ss << pair.first << "=" << pair.second;
first = false;
}
auto data = ss.str();
// Be careful of the length of string with the choosen hash engine. SHA1 needed 20 characters.
// Change the length accordingly with your choosen hash engine.
const static int HMAC_LENGTH = 20;
unsigned char hmac[HMAC_LENGTH];
HMAC_CTX *ctx = HMAC_CTX_new();
HMAC_CTX_reset(ctx);
// Using sha1 hash engine here.
// You may use other hash engines. e.g EVP_md5(), EVP_sha224, EVP_sha512, etc
HMAC_Init_ex(ctx, secretKey.data(), secretKey.size(), EVP_sha1(), NULL);
HMAC_Update(ctx, (const unsigned char*)data.data(), data.size());
HMAC_Final(ctx, hmac, NULL);
std::string sha1Data((const char*)hmac, HMAC_LENGTH);
Base64Encode(sha1Data, sign);
}
Python代码实现
import hmac
import hashlib
import base64
def GetSign(params={}, secretKey=""):
keys = []
for k in params:
keys.append(k)
keys.sort()
ss = []
n = len(keys)
for i in range(n):
k = keys[i]
if i > 0:
ss.append("&")
ss.append("{}={}".format(k, params[k]))
content = "".join(ss)
mac = hmac.new(bytes(secretKey, 'utf-8'), bytes(content, 'utf-8'), hashlib.sha1).digest()
return base64.b64encode(mac).decode()
def TestSign():
params = {
"app_id": 1234,
"access_token": "q3fafa33sHFU+V9h32h0v8weVEH/04hgsrHFHOHNNQOBC9fnwejasubw==",
"ts": 1555912969
}
sign = GetSign(params, "1ACCgaRXbazbaVzkd1NzwVc2G7wg1d6G")
print(sign)
8.3 cp登陆验证
(1) 流程示意

(2) 请求数据
请求方式:
Url: /gsdk/usdk/account/verify_user
Host: https://gsdk.snssdk.com
Method: POST
Content-Type: application/x-www-form-urlencoded
接口参数(需以form格式提交):
无法复制加载中的内容
(3) 返回数据
返回值 (格式为text/json):
无法复制加载中的内容
data字段:
无法复制加载中的内容
age_type字段:
无法复制加载中的内容
返回码
无法复制加载中的内容
8.4 cp支付回调
(1)流程示意图

(2)接口说明
- 发货通知接口,必须保证幂等,即多次重试不影响结果正确性。通知接口中status=2表示成功,CP需要判断该状态进行处理。
- 如果支付成功,但通知cp失败,我方会多次重试,重试间隔(即每次重试后等待时间)为:
[3秒, 5秒, 15秒, 1分, 5分, 30分, 1小时, 12小时,24小时](后续可能调整), 当cp明确返回成功(body为字符串"SUCCESS")后,停止重试。
- 我方采用异步发货,步骤7.1完成时间与步骤7完成时间,无法保证先后顺序。
因此如果cp客户端在接到sdk支付完成通知时,立即请求服务端更新支付、道具相关数据,即在步骤7.1后立即执行步骤9,服务端可能尚未更新订单状态,可以通过重试/延迟请求来解决。
- 渠道会有优惠劵、今币等活动, 建议通过订单状态判断是否支付成功,需要校验金额的话,以订单金额(amount)为参考。
(3)请求数据
请求字段(以form格式提交):
无法复制加载中的内容
(4)返回数据
cp需要返回body为"SUCCESS"(大写),我方以此判断为成功。
8.5 cp支付订单状态查询
(1)接口说明
cp可以根据内部订单号,主动向我方查询订单状态
(2)请求数据
请求方式:
Url: /gsdk/usdk/payment/order/query
Host: https://gsdk.snssdk.com
Method: GET
接口参数:
无法复制加载中的内容
(3)返回数据
返回值(格式为text/json):
无法复制加载中的内容
data字段:
无法复制加载中的内容
返回码
无法复制加载中的内容