在之前的文章中,我们介绍了怎么使用Gradle插件,apk加固,上传到蒲公英。
这篇文章,主要就是把流程进一步完善,通过Gradle插件实现:打包—加固—上传蒲公英—发送钉钉消息,实现完全自动化.。
之前的文章介绍:
下面,我们看下发送钉钉通知的简单流程:
- 创建钉钉机器人。获取发送消息的凭证。
- 根据根据凭证和钉钉的需要的数据格式,来调用API
钉钉的API文档 里面有详细的流程描述,这里就不说了。
开发钉钉消息Task任务流程:
一、创建钉钉机器人
要发送钉钉消息,我们需要在钉钉群里创建一个钉钉机器人。在创建钉钉机器人后,通过这个机器人的Webhook和secret实现发消息到钉钉群。
1,点击群的智能群助手
2,点击创建机器人
点击添加机器人(可能需要权限),这个需要电脑操作,在创建机器人后,会获取到Webhook和secret。
这两个参数就是我们发送数据需要的。
创建完机器人后,我们就拿到了webhook和secret,这是发送钉钉消息的凭证。
创建完机器人后,我们根据钉钉的API文档,通过凭证,就可以调用API了。这里我没有使用官方的SDK,是根据文档的格式,通过OKHttp来发送消息的。
二,创建发送钉钉消息的Task任务
根据文档,我们知道,发送钉钉,我们必须要传的参数:webhook,secret。
然后,根据我们发送消息的类型选择不同的参数,我们这里使用link类型来发送消息:
{
"msgtype": "link",
"link": {
"text": "这个即将发布的新版本,创始人xx称它为“红树林”。
而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是“红树林”?",
"title": "时代的火车向前开",
"picUrl": "",
"messageUrl": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI"
}
}
这里我们看到,我们上传需要:text,title,picUrl,messageUrl,四个参数
picUrl,链接messageUrl,肯定是我们上传到蒲公英之后才能获取的。
这样的话,我们需要Extension来增加4个参数:webhook,secret,text,title。另外两个参数(picUrl,messageUrl)需要等到上传蒲公英成功后获取。
等上面的参数准备好了以后,我们只要通过OKHttp调用API就可以完成了。
准备工作有:
- 构建符合钉钉需要的Model类
- 给Extension添加参数(上面分析的四个参数)
- 获取上传蒲公英成功后的Url参数
- 解析Extension的参数
- 调用API发送钉钉消息
1,构建符合钉钉需要的Model类
发送钉钉是通过Json方式来传递的,我们先构建它需要的model
public static class DingTalkModel {
public String picUrl;
public String messageUrl;
public String title;
public String text;
}
public static class DingTalkRequest {
public String msgtype = "link";
public DingTalkModel link;
public DingTalkRequest(DingTalkModel link) {
this.link = link;
}
}
2,给Extension添加参数
这里的参数主要有4个:上传凭证(webhook和secret),发送消息参数(title,content)。
在Extension类添加参数。
public class Extension {
public String uKey;
public String apiKey;
public String appName;
//==========加固相关的信息
//指向加固的jar包
public File reinforceJarFile;
//登陆用户名
public String reinforceUsername;
//登陆密码
public String reinforcePassword;
//输出apk的目录
public File outputFile;
//dingding
public String dingWebhook;
public String dingSecret;
public String title;
public String content;
public Extension() {
}
public Extension(String uKey, String apiKey, String appName, File reinforceJarFile,
String reinforceUsername, String reinforcePassword, File outputFile,
String dingWebhook, String dingSecret, String title, String content) {
this.uKey = uKey;
this.apiKey = apiKey;
this.appName = appName;
this.reinforceJarFile = reinforceJarFile;
this.reinforceUsername = reinforceUsername;
this.reinforcePassword = reinforcePassword;
this.outputFile = outputFile;
this.dingWebhook = dingWebhook;
this.dingSecret = dingSecret;
this.title = title;
this.content = content;
}
public Extension(String uKey, String apiKey, String appName, File reinforceJarFile,
String reinforceUsername, String reinforcePassword, File outputFile) {
this.uKey = uKey;
this.apiKey = apiKey;
this.appName = appName;
this.reinforceJarFile = reinforceJarFile;
this.reinforceUsername = reinforceUsername;
this.reinforcePassword = reinforcePassword;
this.outputFile = outputFile;
}
public Extension(String appName, String uKey, String apiKey) {
this.uKey = uKey;
this.apiKey = apiKey;
this.appName = appName;
}
public static Extension getConfig(Project project) {
Extension extension = project.getExtensions().findByType(Extension.class);
if (extension == null) {
extension = new Extension();
}
return extension;
}
}
3,获取上传蒲公英成功后的Url参数
我们在钉钉任务中定义静态属性,当蒲公英上传任务完成后,调用setData()方法,就可以把参数传递过来。
public class DingTalkTask extends DefaultTask {
private static String sShotcutUrl, sQRCodeURL;
...
public static void setData(String shortcutUrl, String qrUrl) {
sShotcutUrl = shortcutUrl;
sQRCodeURL = qrUrl;
}
}
4,解析Extension的参数
这里就是联网前的参数准备了,我们要把需要的参数都在这里解析出来,放到model类里
private DingTalkModel initModel() {
DingTalkModel model = new DingTalkModel();
model.picUrl = sQRCodeURL;
model.messageUrl = "http://www.pgyer.com/" + sShotcutUrl;
Extension extension =Extension.getConfig(mTargetProject);
model.title = extension.title;
model.text = extension.content;
mWebhook = extension.dingWebhook;
mSecret = extension.dingSecret;
return model;
}
这里创建钉钉需要的model,然后把相应的参数都获取到。
5,调用API发送钉钉消息
这个,直接使用OKHttp调用
@TaskAction
public void sendDingtalk() {
DingTalkModel model = initModel();
String url = createDingUrl();
OkHttpClient okHttpClient = new OkHttpClient();
Gson gson = new Gson();
String json = gson.toJson(new DingTalkRequest(model));
RequestBody requestBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
, json);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
try {
Response response = okHttpClient.newCall(request).execute();
System.out.println("send message result: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* create dingtalk url
*/
private String createDingUrl() {
Long timestamp = System.currentTimeMillis();
return mWebhook + "×tamp=" + timestamp + "&sign=" + createSign(timestamp, mSecret);
}
private String createSign(Long timestamp, String secret) {
String stringToSign = timestamp + "\n" + secret;
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
到这里,我们的钉钉Task就创建完成了。贴一下完整的代码。
public class DingTalkTask extends DefaultTask {
private static String sShotcutUrl, sQRCodeURL;
private BaseVariant mVariant;
private Project mTargetProject;
private String mWebhook;
private String mSecret;
public static class DingTalkModel {
public String picUrl;
public String messageUrl;
public String title;
public String text;
}
public static class DingTalkRequest {
public String msgtype = "link";
public DingTalkModel link;
public DingTalkRequest(DingTalkModel link) {
this.link = link;
}
}
public void init(BaseVariant variant, Project project) {
this.mVariant = variant;
this.mTargetProject = project;
setDescription("send message to ding talk");
setGroup(TestJavaPlugin.PLUGIN_EXTENSION_NAME);
}
@TaskAction
public void sendDingtalk() {
DingTalkModel model = initModel();
String url = createDingUrl();
OkHttpClient okHttpClient = new OkHttpClient();
Gson gson = new Gson();
String json = gson.toJson(new DingTalkRequest(model));
RequestBody requestBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
, json);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
try {
Response response = okHttpClient.newCall(request).execute();
System.out.println("send message result: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* create dingtalk url
*/
private String createDingUrl() {
Long timestamp = System.currentTimeMillis();
return mWebhook + "×tamp=" + timestamp + "&sign=" + createSign(timestamp, mSecret);
}
private DingTalkModel initModel() {
DingTalkModel model = new DingTalkModel();
model.picUrl = sQRCodeURL;
model.messageUrl = "http://www.pgyer.com/" + sShotcutUrl;
Extension extension =Extension.getConfig(mTargetProject);
model.title = extension.title;
model.text = extension.content;
mWebhook = extension.dingWebhook;
mSecret = extension.dingSecret;
return model;
}
private String createSign(Long timestamp, String secret) {
String stringToSign = timestamp + "\n" + secret;
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static void setData(String shortcutUrl, String qrUrl) {
sShotcutUrl = shortcutUrl;
sQRCodeURL = qrUrl;
}
}
三、修改上传蒲公英Task任务
上面说了,我们在上传完成后,需要把URL传递给钉钉的Task任务。
public class PGYUploadTask extends DefaultTask {
private BaseVariant mVariant;
private Project mTargetProject;
public static class PGYRequest {
public String uKey;
public String apiKey;
//1,install by public 2,install by password 3,install by invite
public String installType;
}
public void init(BaseVariant variant, Project project) {
this.mVariant = variant;
this.mTargetProject = project;
setDescription("upload to pgy");
setGroup(TestJavaPlugin.PLUGIN_EXTENSION_NAME);
}
@TaskAction
public void uploadToPGY() {
Extension extension = Extension.getConfig(mTargetProject);
PGYRequest request = new PGYRequest();
request.apiKey = extension.apiKey;
request.uKey = extension.uKey;
File apkDir = extension.outputFile;
if (apkDir == null || !apkDir.exists()) {
upload(request);
} else {
File[] files = apkDir.listFiles();
if (files != null && files.length > 0) {
upload(request.uKey, request.apiKey, files[0]);
} else {
upload(request);
}
}
}
private void upload(PGYRequest request) {
for (BaseVariantOutput output : mVariant.getOutputs()) {
File file = output.getOutputFile();
if (file == null || !file.exists()) {
throw new GradleException("apk file is not exist!");
}
upload(request.uKey, request.apiKey, file);
}
}
private void upload(String ukey, String apiKey, File apkFile) {
//builder
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
//add part
bodyBuilder.addFormDataPart("uKey", ukey);
bodyBuilder.addFormDataPart("_api_key", apiKey);
//add file
bodyBuilder.addFormDataPart("file", apkFile.getName(), RequestBody
.create(MediaType.parse("*/*"), apkFile));
//request
Request request = new Request.Builder()
.url("http://upload.pgyer.com/apiv1/app/upload")
.post(bodyBuilder.build())
.build();
OkHttpClient client = new OkHttpClient();
try {
Response response = client.newCall(request).execute();
String result = response.body().string();
System.out.println("upload result: " + result);
parseResult(result);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private void parseResult(String result) {
if (result == null || result.length() == 0) {
throw new IllegalArgumentException("upload apk to PGY failed");
}
PGYResonpse data = new Gson().fromJson(result, PGYResonpse.class);
if (data == null || data.code != 0) {
throw new IllegalArgumentException("upload apk to PGY failed");
}
DingTalkTask.setData(data.data.appShortcutUrl, data.data.appQRCodeURL);
}
public static class PGYResonpse {
public int code;
public String message;
public PGYDetail data;
public static class PGYDetail {
public String appShortcutUrl;
public String appUpdated;
public String appQRCodeURL;
public String appVersion;
public String appVersionNo;
public String appIcon;
}
}
}
这里,首先创建蒲公英返回结果的Model类;然后,在获取到上传蒲公英结果后,就通过DingTalkTask.setData()方法,把数据传递给钉钉的Task任务。
四,修改Plugin插件里任务的依赖关系
这里,主要是修改下Plugin插件里的任务依赖关系。让钉钉的Task 依赖 上传蒲公英的Task,以便,在上传成功后,发送钉钉通知。
public class TestJavaPlugin implements Plugin<Project> {
public static final String PLUGIN_EXTENSION_NAME = "uploadHelperJava";
public static final String ANDROID_EXTENSION_NAME = "android";
@Override
public void apply(Project project) {
project.getExtensions()
.create(PLUGIN_EXTENSION_NAME, Extension.class);
//项目编译完成后,回调
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(Project project) {
DomainObjectSet<ApplicationVariant> appVariants = ((AppExtension) project
.getExtensions().findByName(ANDROID_EXTENSION_NAME)).getApplicationVariants();
for (ApplicationVariant variant : appVariants) {
//release apk
if (variant.getBuildType().getName().equalsIgnoreCase("uploadRelease")) {
String variantName =
variant.getName().substring(0, 1).toUpperCase() + variant.getName()
.substring(1);
//create task
ReinforceTask reinforceTask = project.getTasks()
.create("reinforceFor" + variantName, ReinforceTask.class);
reinforceTask.init(variant, project);
PGYUploadTask uploadTask = project.getTasks()
.create("uploadJavaFor" + variantName, PGYUploadTask.class);
uploadTask.init(variant, project);
DingTalkTask talkTask = project.getTasks()
.create("dingTalkFor" + variantName, DingTalkTask.class);
talkTask.init(variant, project);
//depend on
variant.getAssembleProvider().get()
.dependsOn(project.getTasks().findByName("clean"));
reinforceTask.dependsOn(variant.getAssembleProvider().get());
uploadTask.dependsOn(reinforceTask);
talkTask.dependsOn(uploadTask);
}
}
}
});
}
}
这里,我们把钉钉Task也创建出来了。然后,修改了依赖关系。到这里,我们的整个流程:打包–加固–上传蒲公英–发送钉钉通知,就完成了。
五,调用
发布插件,和引入插件,之前的文章都有说(开头有链接),这里就不写了。
我们看下需要调用插件的app这个module下的build.gradle
apply plugin: 'com.android.application'
android {
......
signingConfigs {
release {
storeFile file('xxx.jks')
storePassword "xxx"
keyAlias "xxx"
keyPassword "xxx"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
uploadRelease {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
......
apply plugin: 'com.liu.alone.plugin'
uploadHelperJava {
appName = "测试APP"
uKey = "xxxx"
apiKey = "xxxx"
//加固相关的信息
reinforceJarFile = file("../libs/jiagu.jar")
reinforceUsername = "18032793516"
reinforcePassword = "san7758521"
outputFile = file("${buildDir.getAbsolutePath()}\\jiagu")
//钉钉,这里需要修改成自己的值
dingWebhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx"
dingSecret= "xxx"
title = "新版本APP上传完成"
content="体验新版本APP,请到蒲公英下载"
}
任务调用:
打印结果:
> Task :app:reinforceForUploadRelease
==============start JiaGu
....
任务完成_已签名
> Task :app:uploadJavaForUploadRelease
upload result: {"code":0,"message":"","data":{"appKey":"3e95fe0e7fa74e9f7a310bbdfbd139cc","userKey":"c9d2625c0cf221d8f4a98738f4c05e9a","appType":"2","appIsLastest":"2","appFileSize":"3264349","appName":"PluginTest","appVersion":"1.0","appVersionNo":"1","appBuildVersion":"4","appIdentifier":"com.liu.plugintest","appIcon":"9664daaa8a725f726bcb6018495f14a7","appDescription":"","appUpdateDescription":"","appScreenshots":"","appShortcutUrl":"DqmH","appCreated":"2020-01-03 12:21:24","appUpdated":"2020-01-03 12:21:24","appQRCodeURL":"http:\/\/www.pgyer.com\/app\/qrcodeHistory\/9c27fd62288ce774fafb622d8f6df0bbcd9f90cf0b9ba39846c1c6864c93b135"}}
> Task :app:dingTalkForUploadRelease
send message result: {"errcode":0,"errmsg":"ok"}
这里,我们看到了三个任务已经先后执行完成:Task :app:reinforceForUploadRelease,Task :app:uploadJavaForUploadRelease,Task :app:dingTalkForUploadRelease
最后,钉钉通知
到这里,我们整个的测试发布的自动化流程:打包—加固—上传蒲公英—发送钉钉通知,就通过自定义Gradle插件实现了。
其它文件
插件的build.gradle
apply plugin: 'java-library'
apply plugin: 'maven'
dependencies {
compile gradleApi()
compile localGroovy()
compile 'com.android.tools.build:gradle:3.3.1'
implementation("com.squareup.okhttp3:okhttp:3.8.1")
}
repositories {
mavenCentral()
}
group = 'com.liu.alone.plugin'
version = '1.0.0'
archivesBaseName = 'java-plugin'
//
//upload
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../repo'))
}
}
}