# 安卓手册 ## 简介 本文档主要介绍加加移动服务平台Android SDK的接入和使用。 * SDK目录结构 ``` com.anji.appsp.sdk ├── base // 基础请求数据,基础数据处理类,基础controller │ ├── http // 网络请求,使用HttpURLConnection请求 │ ├── model // 接口返回的数据,以及需要返回给前台的数据 │ ├── notice // 公告模块 │ └── controller // 公告功能的处理 │ └── impl // 公告接口实现 │ └── service // 公告接口 ├── util // 工具,比如加密工具、获取设备信息工具 │ ├── version // 版本模块 │ └── controller // 版本功能的处理 │ └── impl // 版本接口实现 │ └── service // 版本接口 AppSpConfig // SDK对外提供的配置入口 ``` * 支持版本说明 > * Android SDK >= 15 > * Android Studio 3.0+ > * JDK 1.8 ## Android SDK集成 ### 情景一 AAR Maven依赖 * 创建library,此处略 * 上传AAR到github 1,在library下的build.gradle加入 ```java apply plugin: 'maven' ext { GITHUB_REPO_PATH = "XXX"//XXX:打包aar成功后所在目录,比如我的是 D:/Projects/AARMaven PUBLISH_GROUP_ID = 'anji.sdk.appsp'//aar的组名 PUBLISH_ARTIFACT_ID = 'aar'//包名 PUBLISH_VERSION = '0.0.2'//版本名,通过上述配置,生成的aar目录是 anji/sdk/appsp/aar/0.0.2 } //打包 uploadArchives { repositories { mavenDeployer { //本地maven仓库地址,也可以使用远程maven仓库 def deployPath = file(project.GITHUB_REPO_PATH) repository(url: "file://${deployPath.absolutePath}") pom.project { groupId project.PUBLISH_GROUP_ID artifactId project.PUBLISH_ARTIFACT_ID version project.PUBLISH_VERSION } } } } // 源代码一起打包 task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } artifacts { archives androidSourcesJar } ``` 2,生成aar并上传至github 执行`gradlew uploadArchives`,即可本地生成aar资源,从目录anji开始,全部传到github所在的仓库 3,集成aar 3.1 host配置 ``` 针对Windows环境,C:\Windows\System32\drivers\etc 的host文件加入 185.199.108.133 raw.githubusercontent.com 针对Mac环境,在/etc/hosts下加入 ``` 3.2 在项目的build.gradle中加入 ```java buildscript { repositories { google() jcenter() //关键代码 mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:3.4.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() //关键代码,XX表示github的组织或账号,我们是anji-plus,YY是branch,比如我们是main //我们完整的路径是 https://raw.githubusercontent.com/anji-plus/aj_android_appsp_aar/main maven { url "https://raw.githubusercontent.com/XX/aj_android_appsp_aar/YY" } } } ``` 3.3 在app/build.gradle中引入 ```java dependencies { //引用sdk implementation 'anji.sdk.appsp:aar:0.0.2' } ``` 到此,AAR上传和集成就结束了。 ### 情景二 本地library集成 * 创建library,此处略 * app/build.gradle 下引用 ```java dependencies { //XXX是library的名称 implementation project(':XXX') } ``` ## 快速接入使用 ### 初始化服务 AppKey的获取见 **操作手册** ```java public class AppContext extends Application { private static AppContext mInstance; //appkey是在Appsp创建应用生成的 public static final String appKey = "860dc3bd5faa489bb7ab6d087f8ee6e5"; public static AppContext getInstance() { return mInstance; } @Override public void onCreate() { super.onCreate(); mInstance = this; //关键代码 AppSpConfig.getInstance() .init(this, appKey) //可修改基础请求地址 .setHost("https://openappsp.anji-plus.com") //正式环境可以禁止日志输出,通过Tag APP-SP过滤看日志 .setDebuggable(BuildConfig.DEBUG ? true : false) //务必要初始化,否则后面请求会报错 .deviceInit(); } } ``` ### 版本更新服务 * 权限 ```java ``` * file_paths.xml代码 ```xml ``` * 调用 ```java private void checkVersion() { AppSpConfig.getInstance().getVersion(new IAppSpVersionCallback() { @Override public void update(AppSpModel spModel) { //因为是异步,注意当前窗口是否活跃 if (!isActive()) { return; } //当前在子线程,注意 } @Override public void error(String code, String msg) { //当前在子线程,注意 } }); } ``` **请求返回数据结构如下** **spModel** 数据详情 ```java { "repCode": "0000", //业务返回码,0000表示成功 "repMsg": "成功", //业务日志 "repData": { "downloadUrl": "app下载地址", "mustUpdate": false, //是否强制更新,true为强制更新 "showUpdate": true, //是否允许弹出更新 "updateLog": "更新日志" } } ``` *** | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |String | 业务返回码,0000表示成功 | | repMsg |String | 业务日志、异常信息 | | repData | Object | 请求业务数据包、详情见下 | **repData** 数据详情 | 字段| 类型| 说明 | | :-----| :---- | :---- | | downloadUrl |String | app下载地址 | | mustUpdate | boolean | 是否强制更新,true为强制更新; false为非强制更新 | | showUpdate | boolean | 是否提示更新:允许弹出更新 | | updateLog |String | 更新日志 | *** **errorInfo** 数据详情 ```java repCode: 抛出异常Code repMsg: 异常信息 ``` | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |String | 抛出异常Code ; 1001: 请求异常;1202: appKey为空;1203: appKey校验失败;1207: 系统版本号不能为空;1208: 应用版本号不能为空 | | repMsg |String | 异常信息 | ### 公告服务 ```java private void checkNotice() { AppSpConfig.getInstance().getNotice(new IAppSpNoticeCallback() { @Override public void notice(AppSpModel> spModel) { //因为是异步,注意当前窗口是否活跃 if (!isActive()) { return; } //当前在子线程,注意 } @Override public void error(String code, String msg) { //当前在子线程,注意 } }); ``` **请求返回数据结构如下** *** **spModel** 数据详情 ```java { "repCode": "0000", //业务返回码,0000表示成功 "repMsg": "成功", //业务日志 "repData": [ // 返回数据为 List { "title": "公告标题", "details": "公告内容", "templateType": "dialog", //公告类型( 弹窗:dialog; 水平滚动:horizontal_scroll) "templateTypeName": "公告"//公告模板名称 } ] } ``` | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |String | 业务返回码,0000表示成功 | | repMsg |String | 业务日志、异常信息 | | repData |Object | 请求业务数据包、详情见下 | **repData** 数据详情 | 字段| 类型| 说明 | | :-----| :---- | :---- | | title |String | 公告标题 | | details |String | 公告内容 | | templateType |String | 公告类型( 弹窗:dialog; 水平滚动:horizontal_scroll)| | templateTypeName |String | 公告模板名称 | *** **errorInfo** 数据详情 ```java repCode: 抛出异常Code repMsg: 异常信息 ``` | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |string | 抛出异常Code ; 1001: 请求异常;1202: appKey为空;1203: appKey校验失败;1207: 系统版本号不能为空;1208: 应用版本号不能为空 | | repMsg |string | 异常信息 | ## Android SDK解析 * 采取service/impl/controller/handler 的结构,方便模块功能扩展 * AppSpConfig 主类,对外提供可配置方法 ```java /** * @param host 请求的基本地址 */ public AppSpConfig setHost(String host) { AppSpRequestUrl.Host = host; return appSpConfig; } /** * 设置是否打开debug开关 * * @param debug true表示打开debug */ public AppSpConfig setDebuggable(boolean debug) { AppSpLog.setDebugged(debug); return appSpConfig; } /** * 初始化 * * @param context 可以是 activity/fragment/view * @param appKey 用于标识哪个APP,唯一标识 */ public AppSpConfig init(Context context, String appKey) { this.context = context; this.appKey = appKey; return appSpConfig; } ``` * 对外提供功能 ```java /** * 获取设备信息 */ public void deviceInit() { AppSpVersionController appSpVersionController = new AppSpVersionController(context, appKey); appSpVersionController.initDevice(); } /** * 版本更新回调 * * @param iAppSpVersionCallback */ public void getVersion(IAppSpVersionCallback iAppSpVersionCallback) { AppSpVersionController appSpVersionController = new AppSpVersionController(context, appKey); appSpVersionController.getVersion(iAppSpVersionCallback); } /** * 公告回调 * * @param iAppSpNoticeCallback */ public void getNotice(IAppSpNoticeCallback iAppSpNoticeCallback) { AppSpNoticeController appSpNoticeController = new AppSpNoticeController(context, appKey); appSpNoticeController.getNotice(iAppSpNoticeCallback); } ``` ### 数据RSA加密 * 见RSAUtil.java ```java /** * RSA加密 * * @param data 待加密数据 * @param publicKey 公钥 * @return */ public static String encrypt(String data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); int inputLen = data.getBytes().length; ByteArrayOutputStream out = new ByteArrayOutputStream(); int offset = 0; byte[] cache; int i = 0; // 对数据分段加密 while (inputLen - offset > 0) { if (inputLen - offset > MAX_ENCRYPT_BLOCK) { cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK); } else { cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset); } out.write(cache, 0, cache.length); i++; offset = i * MAX_ENCRYPT_BLOCK; } byte[] encryptedData = out.toByteArray(); out.close(); // 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串 // 加密后的字符串 return Base64Util.encode(encryptedData); } ``` ### 设备信息获取 * 因为这个功能就是一次性的,所以放在version模块 * 接口 接口路径:https://openappsp.anji-plus.com/sp/phone/deviceInit ```java public interface IAppSpVersionService { //获取版本信息 void getVersion(); } ``` * 实现 ```java @Override public void initDevice() { //appKey不能为空 if(TextUtils.isEmpty(appKey)){ AppSpLog.e("initDevice Appkey is null or empty"); return; } //请求基础数据 AppSpPostData appSpPostData = getPostEncryptData(); AppSpHttpClient client = new AppSpHttpClient(); //请求 client.request(AppSpRequestUrl.Host + AppSpRequestUrl.initDevice, appSpPostData, new AppSpCallBack() { @Override public void onSuccess(String data) { AppSpLog.d("initDevice success"); } @Override public void onError(String code, String msg) { AppSpLog.d("initDevice error"); } }); } ``` ### 版本信息获取 * 接口 接口路径:https://openappsp.anji-plus.com/sp/phone/appVersion ```java public interface IAppSpVersionService { //获取版本信息 void getVersion(); } ``` * 实现 ```java @Override public void getVersion() { //appKey不能为空 if(TextUtils.isEmpty(appKey)){ AppSpLog.e("getVersion Appkey is null or empty"); return; } //请求基础数据 AppSpPostData appSpPostData = getPostEncryptData(); AppSpHttpClient client = new AppSpHttpClient(); //请求 client.request(AppSpRequestUrl.Host + AppSpRequestUrl.getAppVersion, appSpPostData, new AppSpCallBack() { @Override public void onSuccess(String data) { //数据解析 if (appSpVersionHandler != null) { appSpVersionHandler.handleVersionSuccess(data); } } @Override public void onError(String code, String msg) { //数据解析 if (appSpVersionHandler != null) { appSpVersionHandler.handleUpdateException(code, msg); } } }); } ``` * 数据解析,详见AppSpVersionHandler.java ```java /** * 版本更新获取数据成功 * json解析 */ public void handleVersionSuccess(String data) { if (appSpVersionCallback != null) { synchronized (appSpVersionCallback) { //Update try { AppSpModel spVersionModel = new AppSpModel<>(); JSONObject jsonObject = new JSONObject(data); spVersionModel.setRepCode(getStringElement(jsonObject.opt("repCode"))); spVersionModel.setRepMsg(getStringElement(jsonObject.opt("repMsg"))); Object repDtaObj = jsonObject.opt("repData"); if (repDtaObj != null && !TextUtils.isEmpty(repDtaObj.toString()) && !"null".equalsIgnoreCase(repDtaObj.toString())) { JSONObject repData = new JSONObject(repDtaObj.toString()); if (repData != null) { AppSpVersion appSpVersion = new AppSpVersion(); appSpVersion.setDownloadUrl(getStringElement(repData.opt("downloadUrl"))); appSpVersion.setUpdateLog(getStringElement(repData.opt("updateLog"))); appSpVersion.setShowUpdate(getBooleanElement(repData.opt("showUpdate"))); appSpVersion.setMustUpdate(getBooleanElement(repData.opt("mustUpdate"))); spVersionModel.setRepData(appSpVersion); } } AppSpLog.d("版本更新返回客户端数据 " + spVersionModel); //结果返回给调用者 appSpVersionCallback.update(spVersionModel); } catch (Exception e) { AppSpLog.d("版本更新返回客户端数据 Exception e " + e.toString()); } } } } /** * 版本更新异常处理 * * @param code * @param msg */ public void handleUpdateException(String code, String msg) { if (appSpVersionCallback != null) { synchronized (appSpVersionCallback) { //Update error //结果返回给调用者 appSpVersionCallback.error(code, msg); } } } ``` ### 公告信息获取 * 接口 接口路径:https://openappsp.anji-plus.com/sp/phone/appNotice ```java public interface IAppSpNoticeService { void getNotice(); } ``` * 实现 ```java @Override public void getNotice() { //appKey不为空 if (TextUtils.isEmpty(appKey)) { AppSpLog.e("getNotice Appkey is null or empty"); return; } //请求基础数据 AppSpPostData appSpPostData = getPostEncryptData(); AppSpHttpClient client = new AppSpHttpClient(); client.request(AppSpRequestUrl.Host + AppSpRequestUrl.getAppNotice, appSpPostData, new AppSpCallBack() { @Override public void onSuccess(String data) { //数据处理 if (appSpNoticeHandler != null) { appSpNoticeHandler.handleNoticeSuccess(data); } } @Override public void onError(String code, String msg) { //数据处理 if (appSpNoticeHandler != null) { appSpNoticeHandler.handleNoticeExcption(code, msg); } } }); } ``` * 数据解析,详见AppSpNoticeHandler.java ```java /** * 公告获取数据成功 */ public void handleNoticeSuccess(String data) { if (appSpNoticeCallback != null) { synchronized (appSpNoticeCallback) { //Notice try { AppSpModel> spVersionModel = new AppSpModel<>(); JSONObject jsonObject = new JSONObject(data); spVersionModel.setRepCode(getStringElement(jsonObject.opt("repCode"))); spVersionModel.setRepMsg(getStringElement(jsonObject.opt("repMsg"))); Object repDtaObj = jsonObject.opt("repData"); if (repDtaObj != null && !TextUtils.isEmpty(repDtaObj.toString()) && !"null".equalsIgnoreCase(repDtaObj.toString())) { JSONArray repData = new JSONArray(repDtaObj.toString()); List items = new ArrayList<>(); if (repData != null) { for (int i = 0; i < repData.length(); i++) { AppSpNoticeModelItem item = new AppSpNoticeModelItem(); JSONObject value = repData.getJSONObject(i); item.setTitle(getStringElement(value.opt("title"))); item.setDetails(getStringElement(value.opt("details"))); item.setTemplateType(getStringElement(value.opt("templateType"))); item.setTemplateTypeName(getStringElement(value.opt("templateTypeName"))); items.add(item); } spVersionModel.setRepData(items); appSpNoticeCallback.notice(spVersionModel); } } AppSpLog.d("通知返回客户端数据 " + spVersionModel); } catch (Exception e) { AppSpLog.d("通知返回客户端数据 Exception e " + e.toString()); } } } } /** * 公告异常处理 * * @param code * @param msg */ public void handleNoticeExcption(String code, String msg) { if (appSpNoticeCallback != null) { synchronized (appSpNoticeCallback) { //结果返回给调用者 appSpNoticeCallback.error(code, msg); } } } ``` ### Android 混淆 主工程下proguard-rules.pro文件加入: ```java -dontwarn com.anji.appsp.sdk.** -keep class com.anji.appsp.sdk.**{*;} ``` ## Demo下载 Android SDK地址: [https://gitee.com/anji-plus/appsp/tree/master/demo/android/aj_android_appsp](https://gitee.com/anji-plus/appsp/tree/master/demo/android/aj_android_appsp) ## SDK下载 Android SDK地址: [https://gitee.com/anji-plus/aj-appsp/sdk/android](https://gitee.com/anji-plus/appsp/tree/master/sdk/android/appsplibrary)