[toc]

前言

在更换框架之后就是对基类进行封装,在封装过程中我突然想到我在写完需求后要进行打包、上传到应用托管网站、提测这几个流程,往往忙起来会忘记上传到托管网站,或者是忘记发到群聊中,这是一个经常性错误,所以我想要将他们结合为一个流程进
行自动化。最后经过数天的日夜奋战终于实现了这个功能,这个功能你们在网上是绝对是搜索不到的。

功能效果

使用Gradle进行编写插件执行任务实现自动打包上传到Fir并且发送Fir链接到钉钉,下面是效果视频,你看一先看一下视频,如果感兴趣接着看下去,反而你也不用浪费时间。

在实现效果之前我们先来说一些基础的东西,不然你就算实现了这个效果但是你对其中的原理还是不懂,后续根据自己的项目进行修改的时候只要发生错误你就会感到束手无策。

Gradle自动化构建语言

Gradle相比大家都不陌生,Gradle不止我们Android在使用,像spring boot,spring cloud都可以使用Gradle,在最早以前我们用的构建工具是Ant和Maven,现在Android基本都是Gradle,很少有使用Maven的,在后端使用的可能会比较多,而Ant可以说已经绝迹了。

对于Android来说,我们在创建一个新项目的时候会自动生成三个Gradle文件

  1. 根目录下的settings.gradleBuild.gradle
  2. 每个module下的 build.gradle

这几个Gradle是什么意思我不想做过多的说明,你可以直接到Gradle官网去了解,如果你不想去看官网文档的话你可以去CSDN、简书、稀土掘金社区这些网站查找资料,我这里有找了个不错的文章大家可以看一下。

文章:彻底弄明白Android开发Gradle相关配置
来源:CSDN
作者:Loongxu
文章地址: https://blog.csdn.net/heng615975867/article/details/80346723

我还是建议大家到官网去查看相关资料,因为现在Gradle已经更新到6.+。

1.创建buildSrc文件夹

Gradle文档中

当您运行Gradle时,它将检查是否存在名为buildSrc的目录。
然后Gradle自动编译并测试此代码,并将其放入构建脚本的类路径中。
您不需要提供任何进一步的说明。

这里官网有详细的说明,我就不过于多说了。
创建Gradle后我们在AndroidStudio中执行Make Project。

Make Project

执行后可以看到buildSrc自动生成了一些文件

BuildSrc

这个文件夹就是我们写插件的地方,既然module会有build.gradle进行配置,那么我们的插件当然也需要使用gradle进行配置。

2.配置buildSrc

要进行配置的话先需要在buildSrc目录下创建一个build.gradle。
首先是需要配置我们这个插件是属于哪一种语言的插件,Gradle支持kotlin,java,C/C++还有Groovy等语言,我们做安卓的熟悉的当然是kotlin和java了, 其他语言的配置可以自己到官网去看。
配置java插件:

plugins{
    id 'java'
    id 'java-gradle-plugin'
}

配置完插件所使用的语言后我们需要配置插件的依赖,我们的插件是依赖于Gradle和AndroidGradle进行开发,所以这里需要引入这两个依赖。

dependencies{
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:3.5.3'
}

设置完依赖之后我们需要设置我们依赖的仓库在哪里,我们的依赖的仓库是googlejcentermavenCentral如果不告诉在那个仓库就会找不到依赖。

allprojects{
    repositories{
        google()
        jcenter()
        mavenCentral()
    }
}

配置完插件的build.gradle后我们还需定义插件的名字,插件的名字是需要在src/main/resources/META-INF/gradle-plugins下创建(这个目录是固定的不可修改)。新建文件的名称就是我们插件的名称,比如我们的插件叫com.demo.gradle,那么我们文件的名称就是com.demo.gradle.properties。

3.创建Gradle插件的扩展类

src/main/java/com.demo.gradle下创建一个扩展类,如:DemoPlugin.java,并实现接口Plugin,泛型对象通常为Project。Plugin和Project都在org.gradle.api包下。

package com.demo.gradle;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class DemoPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {

    }
}

点击实现的接口可以看到接口Plugin里只有一个apply方法。

创建好扩展类后需要在上一步的 com.demo.gradle.operties 文件中设置我们的插件执行的类。

implementation-class=com.demo.gradle.DemoPlugin

4.配置扩展参数类

插件在使用的时候是在:app中使用,打开app目录可以看到有build.gradle这样一个文件,这个文件我们也是非常熟悉了。
在这个文件中我们在发布进版本和导入依赖时会经常使用,在发布新版本中会修改版本号,那我们的插件是不是也需要接受一些参数进行打包呢?这是肯定的,所以我们这里就要写一个接收扩展参数的类。在src/main/java/com.demo.gradle下创建DemoExtension。
这里演示上传到Fir和通过钉钉群机器人发送消息,相关的API接口可以到Fir钉钉查看。

在Fir的官方接口文档中看到上传应用需要三个步骤:

  1. 获取应用上传凭证。
    所需参数: typebundle_idapi_token
  2. 上传Icon。
    所需参数:upload_urlkeytoken file
  3. 上传应用。
    所需参数: upload_urlkey token file x:name x:version x:build x:release_type x:changelog

在钉钉官方接口文档中看到发送群消息只需要调用一个接口,但是需要添加机器人:

  1. 添加机器人可以参照文档,这里我只添加一个自定义机器人测试。
  2. 添加机器人后获取到机器人的webhook就可以开始发送消息,消息的类型有很多种,这里我只选择link。
    所需参数:msgtype,link{ text,title,picUrl,messageUrl }

配置DemoExtension

Fir参数
获取凭证:type只有android和ios,所以不需要。bundleId可以直接获取到,也不需要。apiToken从配置信息传入,便于更改。
上传Icon:uploadUrl,key,token 在返回的凭证中获取,不需要。file 需要图片路径。
上传Apk:uploadUrl,key,token 在返回凭证中获取,不需要。file 可以直接获取到打包后的apk路径,不需要。xName 自己定义的App名称,需要。xVersion版本号,不需要,xBuild,不需要。releaseType,针对ios,不需要。changelog 更新日志,需要。
所以我们的DemoExtension只有apiToken、iconFilePath、appName、changelog这四个字段。
钉钉参数(按Json请求)
发送消息:msgtype,固定类型,不需要。link对象,链接配置,不需要。text,链接内容,需要,也可以使用fir的changelog。title,需要,也可以使用fir的appName。picUrl,网络Icon地址,需要。messageUrl,跳转地址,需要。
这里只需要:text,title,messageUrl。

package com.demo.gradle;

/**
 *  Fir参数
 *  获取凭证:type只有android和ios,所以不需要。bundleId可以直接获取到,也不需要。apiToken从配置信息传入,便于更改。
 *  上传Icon:uploadUrl,key,token 在返回的凭证中获取,不需要。file 需要图片路径。
 *  上传Apk:uploadUrl,key,token 在返回凭证中获取,不需要。file 可以直接获取到打包后的apk路径,不需要。xName 自己定义的App名称,需要。xVersion版本号,不需要,xBuild,不需要。releaseType,针对ios,不需要。changelog 更新日志,需要。
 *  所以我们的DemoExtension只有apiToken、iconFilePath、appName、changelog这四个字段。
 *  钉钉参数(按Json请求)
 *  发送消息:msgtype,固定类型,不需要。link对象,链接配置,不需要。text,链接内容,需要,也可以使用fir的changelog。title,需要,也可以使用fir的appName。picUrl,网络Icon地址,需要。messageUrl,跳转地址,需要。
 *  这里只需要:text,title,messageUrl。
*/
public class DemoExtension {
    //upload fir
    public String bundleId;
    public String firApiToken;
    public String iconFilePath;
    public String appName;
    public String changelog;

    //send msg ding
    public String text;
    public String title;
    public String messageUrl;
    public String picUrl;
}

配置后需要在DemoPlugin中创建一个新扩展并将其添加到此容器,并且添加要在评估此项目后立即执行的操作。

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class DemoPlugin implements Plugin<Project> {
    private final String pluginTaskName = "demoTask";
    @Override
    public void apply(Project project) {
        project.getExtensions().create(pluginTaskName,DemoExtension.class);
        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {

            }
        });
    }
}

设置完这些以后就可以在:app下的build.gradle中进行配置扩展参数。
直接在build.gradle中添加

apply plugin:'com.demo.gradle'
demoTask {
    //upload fir
    firApiToken = "cbf2f42a4418195c830ea7cxxxxxxxxx"
    appName = "PluginDemo"
    changelog = "更新了第一个版本。"
    iconFilePath = rootProject.projectDir.getAbsolutePath() + "app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png"

    //send msg ding
    text = "注意:新版APK,请测试人员进行下载测试"
    title = ((AppExtension) rootProject.project.getExtensions().findByName("android")).getDefaultConfig().versionName + "已更新"
    messageUrl = "https://fir.im/12j5"
    picUrl = "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2534644760,1293822824&fm=26&gp=0.jpg"
}

5.创建上传到Fir的任务

这里需要用到其他的网络框架进行网络请求,也可以使用HttpUrlConnection,直接在 buildSrc下的build.gradle中引用依赖。

dependencies{
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:3.5.3'
//====================OkHttp3===========================
    implementation 'com.squareup.okhttp3:okhttp:4.2.2'
//====================fastJson===========================
    implementation 'com.alibaba:fastjson:1.1.71.android'
}

引入依赖后在新建一个tasks文件夹,在该文件夹下新建UploadFirTask.java,只是为了便于阅读,与文件夹名无关。UploadFirTask继承DefaultTask,需要两个对象,一个是Project,一个是BaseVariant,获取到这两个对象后就可以开始上传包到fir了,写一个需要执行任务的方法,在方法上添加@TaskAction。 后面的就是我们正常的网络请求和文件上传步骤,这里不多说了.

package com.demo.gradle.tasks;

import com.alibaba.fastjson.JSONObject;
import com.android.build.gradle.AppExtension;
import com.android.build.gradle.api.BaseVariant;
import com.android.build.gradle.api.BaseVariantOutput;
import com.demo.gradle.DemoExtension;
import com.demo.gradle.OkHttpUtil;
import com.demo.gradle.entity.UploadFirEntity;

import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.tasks.TaskAction;

import java.io.File;
import java.util.HashMap;

import okhttp3.MediaType;
import okhttp3.Response;

public class UploadFirTask extends DefaultTask {
    private Project project;
    private BaseVariant variant;

    public void initUpload(Project project, BaseVariant variant) {
        this.project = project;
        this.variant = variant;
        setDescription("upload fir task");
        setGroup("releaseDemoTask");
    }

    @TaskAction
    public void uploadTask() {
        AppExtension appExtension = (AppExtension) project.getExtensions().findByName("android");
        for (BaseVariantOutput output : variant.getOutputs()) {
            File apkFile = output.getOutputFile();
            if (apkFile == null || !apkFile.exists()) {
                throw new GradleException(apkFile + "is not exists");
            } else {
                System.out.println(apkFile.getAbsoluteFile());
            }
            DemoExtension config = DemoExtension.getConfig(project);
            System.out.println("#########################################################################################");
            System.out.println("#  applicationId : #" + variant.getMergedFlavor().getApplicationId());
            System.out.println("#  uploadFileName: #" + apkFile.getAbsoluteFile());
            System.out.println("#  versionName   : #" + appExtension.getDefaultConfig().getVersionName());
            System.out.println("#  versionCode   : #" + appExtension.getDefaultConfig().getVersionCode());
            System.out.println("#  appName       : #" + config.appName);
            System.out.println("#  changLog      : #" + config.changelog);
            System.out.println("#########################################################################################");
            try {
                //1.get fir token
                UploadFirEntity uploadFirEntity = new UploadFirEntity();
                uploadFirEntity.setBundle_id(variant.getMergedFlavor().getApplicationId());
                uploadFirEntity.setApi_token(config.firApiToken);
                HashMap<String, String> getFirTokenMap = new HashMap<>();
                getFirTokenMap.put("type", uploadFirEntity.getType());
                getFirTokenMap.put("bundle_id", uploadFirEntity.getBundle_id());
                getFirTokenMap.put("api_token", uploadFirEntity.getApi_token());
                Response firToken = OkHttpUtil.getInstance().postData("http://api.fir.im/apps", getFirTokenMap);
                JSONObject tokenJB = JSONObject.parseObject(firToken.body().string());
                System.out.println("################################### get fir token ##############################################");
                System.out.println(tokenJB.toJSONString());
                System.out.println("################################################################################################");

                //2.upload apk icon
                File iconFile = new File(config.iconFilePath);
                HashMap<String, String> uploadIcon = new HashMap<>();
                uploadIcon.put("key", tokenJB.getJSONObject("cert").getJSONObject("icon").getString("key"));
                uploadIcon.put("token", tokenJB.getJSONObject("cert").getJSONObject("icon").getString("token"));
                Response firIcon = OkHttpUtil.getInstance().postFile(tokenJB.getJSONObject("cert").getJSONObject("icon").getString("upload_url"), MediaType.parse("image/*"),
                        uploadIcon, iconFile);
                System.out.println("################################## upload fir icon #############################################");
                System.out.println(firIcon.body().string());
                System.out.println("################################################################################################");

                //3.upload apk file
                HashMap<String, String> uploadApk = new HashMap<>();
                uploadApk.put("key", tokenJB.getJSONObject("cert").getJSONObject("binary").getString("key"));
                uploadApk.put("token", tokenJB.getJSONObject("cert").getJSONObject("binary").getString("token"));
                uploadApk.put("x:name", config.appName);
                uploadApk.put("x:version", appExtension.getDefaultConfig().getVersionName());
                uploadApk.put("x:build", String.valueOf(appExtension.getDefaultConfig().getVersionCode()));
                uploadApk.put("x:changelog", config.changelog);
                Response firApk = OkHttpUtil.getInstance().postFile(tokenJB.getJSONObject("cert").getJSONObject("icon").getString("upload_url"),
                        MediaType.parse("file/apk"), uploadApk, apkFile);
                System.out.println("################################### upload fir apk #############################################");
                System.out.println(firApk.body().string());
                System.out.println("################################################################################################");
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("appTokenCall ERROR !!! ----------- " + e.getMessage());
            }
        }
    }
}

第二个发送消息到钉钉也是同样的道理

package com.demo.gradle.tasks;

import com.alibaba.fastjson.JSONObject;
import com.android.build.gradle.api.BaseVariant;
import com.demo.gradle.DemoExtension;
import com.demo.gradle.OkHttpUtil;
import com.demo.gradle.entity.SendMsgDingEntity;

import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.tasks.TaskAction;

public class SendMsgDingTask extends DefaultTask {
    private Project project;
    private BaseVariant variant;
    public void initSendMsgDing(Project project,BaseVariant variant){
        this.project = project;
        this.variant = variant;
        setDescription("send msg ding task");
        setGroup("releaseDemoTask");
    }
    @TaskAction
    public void sendMsgDing(){
        try {
            DemoExtension config = DemoExtension.getConfig(project);
            SendMsgDingEntity sendMsgDingEntity = new SendMsgDingEntity();
            sendMsgDingEntity.getLink().setTitle(config.title);
            sendMsgDingEntity.getLink().setText(config.text);
            sendMsgDingEntity.getLink().setMessageUrl(config.messageUrl);
            sendMsgDingEntity.getLink().setPicUrl(config.picUrl);
            String sendDing = OkHttpUtil.getInstance().postJson("https://oapi.dingtalk.com/robot/send?access_token=1a8d39b1261fe8595a64d4b97f646c8380a5d696e6fc5fa828a2bfbbxxxxxxxx",
                    JSONObject.toJSONString(sendMsgDingEntity));
            System.out.println("################################ send Message To Ding ##########################################");
            System.out.println(sendDing);
            System.out.println("################################################################################################");
        } catch (Exception e) {
            System.out.println("sendMessageToDingTask ERROR !!!!! ----------------- " + e.getMessage());
        }
    }
}

创建完成后就需要对任务进行关联,任务关联要在DemoPlugin中进行操作。
需要注意的是,我们前面的所有任务都是同步执行。

关联需要获取到ApplicationVariant,在打包中判断是release还是debug版本,或者是其他渠道的打包。然后创建上传apk的任务和发送消息的任务。最后是对任务进行关联。任务之间互相依赖。

Collection<ApplicationVariant> applicationVariants = ((AppExtension) project.getExtensions().findByName("android")).getApplicationVariants();
                for (ApplicationVariant variant :
                        applicationVariants) {
                    if (variant.getBuildType().getName().equalsIgnoreCase("release")) {
                        //create upload fir
                        String variantName = variant.getName().substring(0, 1).toUpperCase() + variant.getName().substring(1);
                        UploadFirTask firUploadTask = project.getTasks().create("firUpload" + variantName, UploadFirTask.class);
                        firUploadTask.initUpload(project,variant);

                        //create send msg ding task
                        SendMsgDingTask dingSendMessageTask = project.getTasks().create("sendMsgDing" + variantName,SendMsgDingTask.class);
                        dingSendMessageTask.initSendMsgDing(project,variant);

                        //Task dependencies
                        variant.getAssembleProvider().get().dependsOn(project.getTasks().findByName("clean"));
                        //     ||
                        //     /
                        firUploadTask.dependsOn(variant.getAssembleProvider().get());
                        //     ||
                        //     /
                        dingSendMessageTask.dependsOn(firUploadTask);
                    }
                }

到这里已经结束了,我们可以在app的build.gradle配置打包信息进行打包了。

配置完打包的签名文件后在项目目录下打开dos窗口执行任务,使用命令行的原因是你可以看到更全面的日志信息,方便你调试和查看任务详情,在dos通过gradle + 任务名称 + --info或--debug执行。这里的任务名称是我们发送消息到钉钉的任务名,因为我们的任务是互相依赖关系,如果执行发送消息到钉钉那么它所依赖的任务也必须执行完毕。

执行命令

这个功能已经实现,具体效果可以看上面的视频。

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...