# java-客户端接入指南 --- 本文档面向 Android 开发者,描述的接入方式适合运营在 4399 平台的独代游戏。 ## SDK 下载 SDK和相关示例的下载地址可在游戏创建后,进入 **「游戏管理-SDK服务」** 页面获取。 image ## SDK 说明 ### 用户信息与隐私策略

注意

如果您是开发者,在为用户提供服务前请阅读 《4399通行证用户服务协议》《隐私政策》,了解SDK对个人信息收集范围、处理目的以及权限使用情况。请您向用户提供服务时,告知相关信息并取得用户同意。

如果您是用户,请在使用我们的服务前阅读 《4399通行证用户服务协议》《隐私政策》,了解SDK对个人信息收集范围、处理目的以及权限使用情况。充分理解后再开始使用我们的服务。

### 功能描述 为4399独家代理的游戏提供的SDK,提供游戏激活、礼包、更新等服务,和游戏官方消息官方活动、论坛等丰富而优质的游戏内容 若游戏有被静态修改后二次打包的情况,SDK 对此类破解提供了一些解决方案,详见 [服务端文档](https://sdkftp.4399doc.com/external/operate/3.17/server_api_2.html) ### SDK 组成 #### 客户端 SDK SDK 优先提供在线`aar`依赖方式,Demo 结构遵循 Android Studio(as) 规范,但仍然保留了 `jar+res` 的依赖方式。 SDK 支持的编译配置 **android:minSdkVersion >= 16**。 #### 服务端 API 服务端支持额外的功能,参考 [SDK 服务端接入文档](https://sdkftp.4399doc.com/external/operate/3.17/server_api_2.html) ## 集成流程 ### 准备 首次接入 SDK,要在 [4399 开放平台](https://open.4399.cn/) 注册应用,主要是提交APK、素材等信息。 完成后,开发者将得到 SDK 的基础参数:`game key`或`GameKey`,游戏在 4399 平台的运营标识(开发者平台的游戏信息页面显示为“游戏ID”) ### 引入依赖 根据游戏需要,以下三种方式可选其一 - *在线aar* 若使用*在线aar*,开发者只要在使用 SDK 的 module 的 `build.gradle`中引入以下内容即可 ```groovy repositories { maven { // 4399 SDK 开放仓库:正式 url 'https://mvn.4399doc.com/repository/maven-releases' } maven { // 4399 SDK 开放仓库:快照 url 'https://mvn.4399doc.com/repository/maven-snapshots' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) // 运营 SDK:建议使用最新正式版本(release仓库中的版本),可通过浏览仓库地址或向运营咨询版本 implementation "cn.m4399.sdk:operate:3.17.0" // volley 和 support 是 SDK 使用的外部依赖,若接入方已有,可忽略 implementation 'com.android.volley:volley:1.2.1' //noinspection GradleCompatible implementation "com.android.support:support-v4:28.0.0" } ``` *注意:若使用`7.0+`版本的 gradle 及 android build 插件,仓库地址应配置在`settings.gradle`中* - *本地aar* 若使用*本地aar*,则要下载 [在线aar](https://mvn.4399doc.com/repository/maven-releases/cn/m4399/sdk/operate/3.17.0/operate-3.17.0.aar) 和 [jar+res](https://sdkftp.4399doc.com/external/operate/3.17/operate-3.17.0+655.zip) 到本地,并将`aar`和`jar`包中的以下文件 拷贝到游戏模块的`libs`目录下 ``` operate/libs/volley-v1.2.1.jar operate/libs/support-v13-23.2.1.jar ``` Gradle 文件中注意添加本地依赖 ```groovy dependencies { implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) } ``` - *jar+res* [jar+res 依赖引入](https://sdkftp.4399doc.com/external/operate/3.17/jar_res_import.html) 比较繁琐,但是完成后,后续接口调用仍然是一样的。 #### abi 适配与 so 库 若游戏不需要支持所有 [abi](https://developer.android.google.cn/ndk/guides/abis?hl=zh-cn) ,可以按需选用。 `aar`依赖方式,按以下方式配置需要的`abi`;`jar+res`依赖方式需要手动删除**不需要**的`abi`目录 ``` android { defaultConfig { ndk { // 根据游戏需要选择 abiFilters "armeabi", "armeabi-v7a", "x86", "arm64-v8a" } } } ``` #### AndroidManifest 额外配置 在游戏项目的`AndroidManifest.xml`中,需要**注册存储权限和渠道标识** ```xml ``` 接入方应在**调用接口前,申请好运行时权限**,可以使用[EasyPemissions](https://github.com/googlesamples/easypermissions) 第三方类库快速处理 ### 接口调用:游戏用户协议弹窗 **功能简介:** 游戏调用此接口后,弹出隐私弹窗,用户同意可以调用后续初始化,否则 SDK 将自动退出游戏。 **调用时机:** 游戏启动即可调用,但要在进入游戏前 游戏调用此接口前,需要将弹窗文案和隐私地址提交给运营配置。代码示例参考下面“初始化”接口。 ### 接口调用:初始化(必须) **功能简介:** 此接口初始化 SDK 内部状态,并完成“更新、激活、防沉迷”等流程。**SDK 初始化后才能正确调用其他接口** **调用时机:** 建议在游戏 `Activity` 的开始处如 `onCreate` 方法中进行 ```java private void initSDK() { // 'this' 是游戏 Activity 实例 final Activity activity = this; // 初始化 SDK // 配置全局属性,如横竖屏配置 OperateConfig operateConfig = new OperateConfig.Builder(activity) // 设置调试模式,可选,true时打开,默认false,发布前必须设置为false或删除该行 .setDebugEnabled(false) // 设置游戏运营 key,此参数需要在原创开放平台注册应用后得到 .setGameKey(GAME_KEY) // 设置SDK页面方向,应与游戏方向一致,部分第三方页面需要在AndroidManifest中设置 .setOrientation(GAME_ORIENTATION) // 设置游戏是否兼在高于Android 9.0版本系统容全面屏,true兼容,默认false .compatNotch(GAME_COMPACT_NOTCH) .build(); OperateCenter.getInstance().gameProtocol(activity, operateConfig, new OpeResultListener() { @Override public void onResult(int code, @Nullable String message) { // 其中code:0、用户同意协议;160001、没有协议更新;用户不同意,则SDK结束进程,退出游戏 // 同意后再初始化其他部分,如初始化 4399 SDK if (code == 0 || code == 160001) { // 用户同意协议 afterGameProtocol(activity, operateConfig); }else{ // 用户不同意协议,此处建议退出游戏进程 } } }); } ``` **`afterGameProtocol(activity, operateConfig);`有以下两种实现方式,分别代表使用4399联运sdk的初始化和4399独代sdk初始化,两种方式主要区别在与:4399联运sdk初始化回调接口包含初始化成功和账号登出以及切换账号等回调,4399独代sdk仅有初始化完成的回调** **4399联运sdk初始化:** ```java private void afterGameProtocol(Activity activity, OperateConfig operateConfig) { // 4399联运sdk初始化 OperateCenter.getInstance().init(activity, operateConfig, new OperateCenter.OnInitGlobalListener() { @Override public void onInitFinished(boolean isLogin, User user) { // 4399联运sdk初始化完成 // isLogin : 是否已登录账号 ; user : 账号信息 } @Override public void onUserAccountLogout(boolean fromUserCenter) { // 退出账号回调 // fromUserCenter :是否从用户中心点击退出 ; } @Override public void onSwitchUserAccountFinished(boolean fromUserCenter, User user) { // 切换账号回调 // fromUserCenter :是否从用户中心点击切换账号 ; } }); } // 登录 public void login() { OperateCenter.getInstance().login(requireActivity(), (success, resultCode, user) -> { // success : 是否登录成功 // resultCode : 登录状态码 // user : 账号信息 }); } // 切换账号 public void switchAccount() { // 该句调用后会触发初始化回调中的onSwitchUserAccountFinished OperateCenter.getInstance().switchAccount(requireActivity(), (success, resultCode, user) -> { // success : 是否登录成功 // resultCode : 登录状态码 // user : 账号信息 }); } // 退出账号 public void logout() { // 该句调用后会触发初始化回调中的onUserAccountLogout OperateCenter.getInstance().logout(); } ``` 注意,有个隐藏的细则,4399联运sdk的悬浮球菜单中有切换账号和退出账号的按钮,点击切换账号会触发初始化接口回调的onSwitchUserAccountFinished,点击退出账号会触发初始化接口回调的onUserAccountLogout **4399独代sdk初始化:** ```java private void afterGameProtocol(Activity activity, OperateConfig operateConfig) { // 4399独代sdk初始化 ExclusiveAgent.init(activity, operateConfig, new ExclusiveAgent.OnInitGlobalListener() { /* * 初始化完成,玩家在此处可以安全进入游戏 */ @Override public void onInitFinished() { } }); } ``` 初始化的几点说明 #### 页面方向 - *游戏页面* 应如下配置`android:configChanges`等,否则系统强制方向变化(一般发生是打开外部应用,如支付宝、微信)或系统设置变化时,可能会发生异常 ```xml ``` 其中`android:configChanges` 新增的配置项,是为适配 Android 分屏模式而添加的,更多内容参考[多窗口支持](https://developer.android.google.cn/guide/topics/ui/multi-window?hl=zh-cn#config) **分屏(画中画)兼容**:游戏进入分屏再点击桌面图标启动,会重启游戏页面,而并没有完整的启动应用,这会导致部分功能异常, 为了兼容这种情况,SDK 为应用默认设置了`android:resizeableActivity="false"`,即不支持分屏模式, 若游戏出于某些原因必须要覆盖此项,则务必按最低标准设置入口 Activity 和主 Activity的`android:configChanges`,即 ```xml ``` - *SDK 页面*,**方向支持横屏或竖屏,但不支持横竖屏切换** 通过`OperateConfig.setOrientation()`设置 SDK 页面方向,值是系统类`android.content.pm.ActivityInfo`定义的常量 | 方向参数| 含义| |----------------------------------------|---------| | `SCREEN_ORIENTATION_LANDSCAPE`,`int`, 0 | 横屏| | `SCREEN_ORIENTATION_PORTRAIT`, `int`, 1 | 竖屏 | | `SCREEN_ORIENTATION_SENSOR_LANDSCAPE`,`int`, 6 | 横屏,可180度旋转 | | `SCREEN_ORIENTATION_SENSOR_PORTRAIT`,`int`, 7 | 竖屏 | - *第三方页面*,其方向一般使用 SDK 默认设置即可,需要锁定可设置`android:screenOrientation`,如下所示 ```xml ``` ### 接口调用:游戏内容相关(推荐) 初始化之后,可以调用游戏盒跳转接口,这些接口可以大大丰富游戏内容、补充游戏功能。 #### 使用活动码 **功能简介:** 此接口用于校验玩家在游戏盒中领取的活动码,校验成功后玩家获得活动资格或活动奖励。 **调用时机:** 初始化且回调后 ```java /* 使用活动码 your_activity_id: 活动ID,向运营获取 your_activity_code: 活动码,用户输入 */ ExclusiveAgent.useActivityCode("your_activity_id", "your_activity_code", new OpeResultListener() { @Override public void onResult(int code, @Nullable String message) { // code 0:成功 3:失败 5:异常中止 } }); ``` #### 使用礼包码 **功能简介:** 此接口用于校验玩家在游戏盒中领取的礼包码,校验成功后游戏会发放对应礼包。 **调用时机:** 初始化且回调后 ```java /* 使用礼包码 your_gift_id: 礼包ID,向运营获取 your_gift_code: 礼包码,用户输入 */ ExclusiveAgent.useGiftCode("your_gift_id", "your_gift_code", new OpeResultListener() { @Override public void onResult(int code, @Nullable String message) { // code 0:成功 3:失败 5:异常中止 } }); ``` #### 游戏分享 **功能简介:** 此接口用于玩家在游戏内将游戏页面、图片分享到微信、QQ、游戏盒等外部 APP。 **调用时机:** 初始化且回调后 - 分享接口接入之前,请联系相关运营人员配置分享方式 - 分享接口调用之前,需要游戏确保已申请sdcard读写权限 - 分享方式包括: - 游戏详情页分享(一般为带游戏名称、游戏LOGO、游戏说明的游戏盒H5链接) - 游戏图片分享(一般为带游戏下载二维码的游戏海报,游戏海报即一张图片,图片由游戏方提供,由运营人员上传至4399游戏后台) - 游戏方可选择其中1种或2种分享方式 ```java /* 游戏分享 */ OperateCenter.getInstance().share(EaGameActivity.this); ``` #### 游戏评分 **功能简介:** 此接口调用后,将跳转到《4399 游戏盒》内对应游戏评分页面。 **调用时机:** 初始化且回调后 ```java /* 游戏评分 */ OperateCenter.getInstance().comment(EaGameActivity.this); ``` #### 游戏盒跳转 **功能简介:** 原有的游戏盒跳转,即礼包、活动、论坛、游戏盒详情接口,迁移为通用跳转接口 **调用时机:** 初始化且回调后 游戏开发者调用接口前,需要向运营获取跳转参数,跳转方式参考下文通用跳转接口 ### 接口调用:通用跳转(推荐) 游戏可能需要多形式、更可变的跳转,对此 SDK 对此进行支持。使用这一特性需要: - 运营配置跳转参数,如打开app页面、网页、下载、消息提示等 - 配置完成后得到跳转 key,交接给游戏 - 游戏使用此 key,调用下面的跳转接口 - 后续需要变更跳转方式,由运营重新配置即可 ```java // 先判断是否支持key的协议 OperateAction.support(key, new OpeResultListener() { @Override public void onResult(int code, @Nullable String msg) { // 使用key进行跳转,0为支持,其他不支持 if (code == 0) { OperateAction.perform(key); } } }); ``` 极少数跳转需要传递额外参数,如图片分享,此时要使用另一个接口 ```java // 入口 key // 跳转intent,intent的内部是键值对 OperateAction.perform(key, intent); ``` ### 接口调用:云存档 **功能简介:** 单机类游戏中有些没有自己的服务器,但是又需要跨设备持久化;因此,SDK 提供云存档接口,游戏只需要初始化、读取、保存存档即可。 **调用时机:** 账号登录后,进入游戏主界面前 使用这些接口前,游戏开发者需要: - 与运营确认使用云存档,运营会为游戏完成必要的配置,完成后得到`public key`公钥,这是调用接口的必要参数。 - 明确账号变更时,数据保存-加载策略,一般要处理以下情况 - 账号登录时,应读取新存档 - 账号切换时,应保存旧账号的存档,并读取新账号存档 - 账号登出时,应保存存档 如此,可以保证存档不丢失、不混乱。以上情况可以参考代码下面初始化 `CloudArchive.init` 接口 #### 初始化 ```java // publicKey: 公钥,向运营获取公钥 CloudArchive.init(publicKey, new CloudArchive.InitListener() { @Override public void onSuccess() { //初始化成功 } @Override public void onFailure(int code, @NonNull String message) { //初始化故障 } @Override public void onUserChanged(@Nullable CloudArchive previousUserCloudArchive, @Nullable CloudArchive currentUserCloudArchive) { /* 未登录 -> 账号A : previousUserCloudArchive == null && currentUserCloudArchive != null 账号A -> 账号B : previousUserCloudArchive != null && currentUserCloudArchive != null 账号A -> 未登录 : previousUserCloudArchive != null && currentUserCloudArchive == null */ //登录用户改变 if (previousUserCloudArchive != null) { //保存前一个登录用户的存档 previousUserCloudArchive.save(getArchive(), new CloudArchive.SaveListener() { @Override public void onSuccess() { } @Override public void onFailure(int code, @NonNull String message) { } }); } reset();//重置游戏 if (currentUserCloudArchive != null) { //获取当前登录用户的存档列表 currentUserCloudArchive.list(new CloudArchive.ListListener() { @Override public void onSuccess(@NonNull List archiveList) { } @Override public void onFailure(int code, @NonNull String message) { } }); } } }); ``` 其中`Archive`——存档定义如下: ```java public class Archive { /** * 存档索引,建议使用[1,10]区间的正整数 */ public final int index; /** * 存档标题 */ public final String title; /** * 最后更新时间 */ public final int updateTime; /** * 存档数据 */ public final String data; ... } ``` #### 存档列表 ```java CloudArchive.getCloudArchive().list(new CloudArchive.ListListener() { @Override public void onSuccess(@NonNull List archiveList) { //获取存档列表成功 } @Override public void onFailure(int code, @NonNull String message) { //获取存档列表故障 } }); ``` #### 读取存档 ```java //index: 存档索引 CloudArchive.getCloudArchive().get(index, new CloudArchive.GetListener() { @Override public void onSuccess(@NonNull Archive archive) { //读取存档成功 } @Override public void onFailure(int code, @NonNull String message) { //读取存档故障 } }); ``` #### 保存存档 ```java //archive: 存档 CloudArchive.getCloudArchive().save(archive, new CloudArchive.SaveListener() { @Override public void onSuccess() { //保存存档成功 } @Override public void onFailure(int code, @NonNull String message) { //保存存档故障 } }); ```