联运安卓接入文档

阅读前提

  • 本文档主要是为游戏接入字节联运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资源

自测用例:

直播联运cp自测用例 

今日游戏联运渠道SDK自测用例 

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表示初始化失败,失败的可能原因:

  1. 已经初始化
  2. 获取AppId失败,请确认应用是否包含广告能力
  3. 插件未加载,调用本接口前请确保已经调用 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进行显示的,因此在 ExpressInteractionListeneronRenderSuccess() 方法参数中,可以获取到一个 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编码,获得签名字符串。

注意:

  1. 计算签名的时候不需要对参数进行urlencode处理("application/x-www-form-urlencoded"编码),但是发送请求的时候需要进行urlencode处理,这是很多开发者最容易犯错的地方
  2. 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)接口说明

  1. 发货通知接口,必须保证幂等即多次重试不影响结果正确性通知接口中status=2表示成功,CP需要判断该状态进行处理。
  2. 如果支付成功,但通知cp失败,我方会多次重试,重试间隔(即每次重试后等待时间)为:

[3秒, 5秒, 15秒, 1分, 5分, 30分, 1小时, 12小时,24小时](后续可能调整), 当cp明确返回成功(body为字符串"SUCCESS")后,停止重试。

  1. 我方采用异步发货,步骤7.1完成时间与步骤7完成时间,无法保证先后顺序。

因此如果cp客户端在接到sdk支付完成通知时,立即请求服务端更新支付、道具相关数据,即在步骤7.1后立即执行步骤9,服务端可能尚未更新订单状态,可以通过重试/延迟请求来解决。

  1. 渠道会有优惠劵、今币等活动, 建议通过订单状态判断是否支付成功,需要校验金额的话,以订单金额(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字段:

无法复制加载中的内容

返回码

无法复制加载中的内容

诚邀您对本文档易读易用性进行评价
好用
不好用