Android 创建 Gradle Task 自动打包并上传至蒲公英

前言

  • Android 项目日常开发过程中,经常需要打包给到非开发人员验收或调试,例如测试阶段,就要经常基于测试服务器地址,打包安装包,给到组内测试人员进行测试,并且 BUG 修复完成之后也需要再次打包给到测试人员回测。
  • 为了减免机械性的重复步骤,为项目配置不同的 渠道(Product Flavors),不同渠道对应不同的服务器地址,并且为每一个渠道创建一个 Gradle Task 执行打包并上传至蒲公英的操作,同时在蒲公英中配置 Webhook ,最终可实现:执行对应 **渠道(Product Flavors)**的 Gradle Task,即可自动打包并上传至蒲公英,并将包更新信息同步至企业微信、钉钉、飞书等工作群组,使得包更新流程可视化,并简化了开发和测试联调流程。

实现步骤

1.创建 pgyer-upload.gradle 文件

每个渠道的 Task 执行内容一致:打包并记录更新信息后上传至蒲公英 ,所以抽取公共内容(方法)创建如下 pgyer-upload.gradle 文件

import groovy.json.JsonSlurperimport java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.text.SimpleDateFormatext.uploadApk = this.&uploadApk/*** Sept 1 创建执行任务*//*** 蒲公英 ApiKey* https://www.pgyer.com/account/api* TODO 替换成自己的蒲公英 APIkey*/
static String getApiKey() {return "myApiKey"
}/*** 上传 apk 到蒲公英* @apk 安装包文件* @flavorName 渠道名*/
def uploadApk(File apk, String flavorName) {if (apk == null) {throw new RuntimeException("apk file not exists!")}def apkName = apk.nameprintln "*************** Upload Init ***************"//渠道信息String buildFlavorName = flavorNameif (flavorName == "DemoRelease") {buildFlavorName = "Test"} else if (flavorName == "DevRelease") {buildFlavorName = "Dev"} else if (flavorName == "ProduceRelease") {buildFlavorName = "Produce"} else if (flavorName == "BetaRelease") {buildFlavorName = "Beta"}println "flavorName = ${buildFlavorName}  apkName = ${apkName}  apkSize = ${apk.size()}"// Git 提交信息String commitLogStr = getGitCommitLogByCount(5)
//    String commitLogStr = getGitCommitLogByToDay()def appModule = project.rootProject.project(':app')def appVersionName = appModule.android.defaultConfig.versionNamedef appVersionCode = appModule.android.defaultConfig.versionCode//更新信息String updateDescription = "\n[${getBranchName()}]:${buildFlavorName}-${appVersionName}-${appVersionCode}" +"\n${commitLogStr}"println "updateDescription : ${updateDescription}"println "*************** Upload Get Token ***************"//组装cosToken需要的的参数,见https://www.pgyer.com/doc/view/api#fastUploadAppList<KeyValue> cosTokenParams = new ArrayList<>()//API KEYcosTokenParams.add(new KeyValue("_api_key", apiKey))//属于android平台cosTokenParams.add(new KeyValue("buildType", "android"))//更新描述cosTokenParams.add(new KeyValue("buildUpdateDescription", updateDescription))// 获取上传的 token ,见 https://www.pgyer.com/doc/view/api#fastUploadAppHttpResponse<String> response = postFormData("https://www.pgyer.com/apiv2/app/getCOSToken", cosTokenParams)def resp = new JsonSlurper().parseText(response.body())println ">>>> Get Token Response :\n${response.body()}"println "*************** Uploading Apk File ***************"// 上传文件到第上一步获取的 URL,参数从上一步获取,这里需要解析参数String paramsString = String.valueOf(resp.data.params)String[] params = paramsString.substring(1, paramsString.length() - 1).split(',')List<KeyValue> list = new ArrayList<>()if (params != null) {for (i in 0..<params.length) {String rawParam = params[i].trim()String parsedKey = rawParam.substring(0, rawParam.indexOf("="))String parsedValue = rawParam.substring(rawParam.indexOf("=") + 1, rawParam.length())//添加参数list.add(new KeyValue(parsedKey, parsedValue))}}// 添加apk文件list.add(new KeyValue("file", apk.getPath(), true))HttpResponse<String> uploadResponse = postFormData(resp.data.endpoint, list)if (uploadResponse.statusCode() == 204) {println(">>>> Upload Success ")} else {println(">>>> Upload Fail :" + uploadResponse.body())}println "*************** Upload Completed ***************"
}static HttpResponse<String> postFormData(String url, List<KeyValue> list) {long requestStartTime = System.nanoTime()String boundary = "*********"// Result request bodyList<byte[]> byteArrays = new ArrayList<>()// Separator with boundarybyte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(StandardCharsets.UTF_8)// Iterating over data partsfor (i in 0..<list.size()) {// Opening boundarybyteArrays.add(separator)def entry = list[i]// If value is type of Path (file) append content type with file name and file binaries, otherwise simply append key=valueif (entry.isFile) {java.nio.file.Path path = new File(entry.getValue()).toPath()String mimeType = Files.probeContentType(path)byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName()+ "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8))byteArrays.add(Files.readAllBytes(path))byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8))} else {byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n").getBytes(StandardCharsets.UTF_8))}}byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8))def publisher = HttpRequest.BodyPublishers.ofByteArrays(byteArrays)HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(url)).version(HttpClient.Version.HTTP_1_1).header("Content-Type", "multipart/form-data;boundary=" + boundary).POST(publisher).build()return HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString())
}/*** 获取当天提交日志* @return*/
static String getGitCommitLogByToDay() {//获取 git 提交日志Calendar calendar = Calendar.getInstance()String endTime = new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())calendar.add(Calendar.DATE, -1)String startTime = new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())//git 命令String gitCommand = "git log --pretty=\"%s\" --since=\"${startTime}\" --before=\"${endTime}\""//println "getUpdateDescription() --> gitCommand = ${gitCommand}"String description = gitCommand.execute().text.trim()return description
}/*** 获取最近 n 条提交日志*/
static String getGitCommitLogByCount(int count) {//git 命令String gitCommand = "git log -${count} --pretty=format:\"%s\""//println "getUpdateDescription() --> gitCommand = ${gitCommand}"String description = gitCommand.execute().text.trim()return description
}/*** 获取分支名*/
static String getBranchName() {String gitCommand = "git rev-parse --abbrev-ref HEAD"return gitCommand.execute().text.trim()
}class KeyValue {String keyString valueboolean isFileKeyValue(String key, String value) {this(key, value, false)}KeyValue(String key, String value, boolean isFile) {this.key = keythis.value = valuethis.isFile = isFile}@OverrideString toString() {return "{key:" + key + ", value:" + value + ", isFile:$isFile}"}
}

该代码主要执行 APK 上传至蒲公英的操作,并上传指定更新内容:

  • [ Git 分支名]:渠道名-APP版本名-APP版本号
  • 5 条 Git Commit 信息

2.引用自定义 Gradle 文件

在项目的 build.gradle 文件最外层执行

apply from: "pgyer-upload.gradle"

3.配置渠道(非必须)

app modulebuild.gradle 文件 android 层内执行

    /*** Sept 3 配置渠道 (非必须)*/flavorDimensions "channel"productFlavors {// 生产环境produce {buildConfigField "String", "HOST", "\"${HOST}\""}// 预生产环境beta {buildConfigField "String", "HOST", "\"${HOST_BETA}\""}// 开发环境dev {buildConfigField "String", "HOST", "\"${HOST_DEV}\""}// 测试demo {buildConfigField "String", "HOST", "\"${HOST_TEST}\""}productFlavors.all {// 遍历 productFlavors 多渠道,设置渠道号flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)}}

此处代码中的 produce、beta 、dev、demo 均为自定义的渠道名,可根据自身业务需求进行增删修改,其中 HOST 为自定义的变量名,不同渠道引用值不一样(示例中的值来源配置在项目的 gradle.properties 文件中,如下所示),项目编译后,在代码中即可通过 BuildConfig.FLAVORBuildConfig.HOST 获取当前编译环境的 渠道名自定义变量 HOST 的值

# gradle.properties 文件内
# Host
HOST=https://hao123.com
HOST_BETA=https://hao123.com:1008
HOST_TEST=https://hao123.com:1010
HOST_DEV=https://hao123.com:1024

Android Studio 中,可通过左下方 Build Variants 手动切换编译渠道
切换编译渠道

4.创建打包 Task

同样在 app modulebuild.gradle 文件 android 层内执行

    /*** Sept 4 创建打包 Task* 遍历所有可执行的 variants 创建对应的打包 Task* 生成后的路径及名称:Tasks/build/pushApk[productFlavorsName][Release/Debug]* eg:Tasks/build/pushApkDevRelease*/android.applicationVariants.all { variant ->String taskSuffix = variant.name.capitalize()if (taskSuffix.contains("Release") || taskSuffix.contains("Debug")) {task("pushApk${taskSuffix}") {dependsOn ":app:assemble${taskSuffix}"group 'build'description 'Custom task for gradle'doLast {variant.outputs.all { output ->// 执行脚本任务uploadApk(output.outputFile, taskSuffix)}}}}}

5.执行 Gradle Task

按上述步骤操作之后,先执行 Sync Project with Gradle Files 生成不同渠道对应的打包 Task
然后可通过以下两种方式进行执行打包并上传的脚本任务

  • 直接执行 Task : Tasks/build/pushApkDevRelease
    直接执行 Task 步骤 1
    直接执行 Task 步骤 2

  • 通过 Gradle 命令执行 Task : gradle pgyerUploadDevRelease
    通过 Gradle 命令执行 Task

5.1执行效果

Gradle Task 执行完毕
企业微信群更新信息示例

6.补充说明

6.1 关于变种(variants)

在 Android 应用程序构建过程中,变种variants)是指基于不同构建配置或渠道进行构建的应用程序版本。
Android Gradle 插件使用变种来生成不同版本的应用程序,以满足不同的需求,如不同的构建类型、不同的渠道或不同的产品变体等。
每个变种具有自己的构建配置和特定的属性设置,例如包名、应用图标、应用名称等。通过创建不同的变种,可以实现以下目标:

1.构建类型(Build Types):构建类型定义了不同的构建环境和配置,例如调试版(Debug)和发布版(Release)。每个构建类型可以具有自己的代码、资源、签名证书、编译标志等。
2.渠道(Product Flavors):渠道是为了满足不同目标市场或用户群体的需求而定义的版本变体。通过渠道,可以为不同的渠道定制应用程序的内容,如应用程序图标、名称、启动画面、配置文件等。
3.变体(Build Variants):变体是构建类型和渠道的组合,表示一个具体的应用程序版本。每个变体都有其自己的构建输出,如 APK 文件或可安装的应用程序包。

通过定义和配置不同的变种,开发人员可以轻松地构建适用于不同需求的不同版本的应用程序,以便进行测试、发布和分发。

6.2 上传 APK 文件失败

上传文件至蒲公英失败
该问题产生的原因是当前项目 JDK 版本 > 11 ,而当前 Gradle Task 内执行的文件 POST 操作是自行封装的请求参数,该封装在 JDK 版本 > 11 的情况下,无法被服务器正常识别,猜测原因是自定义包装请求参数过程中出现了偏差导致。
最后的解决方案是:

1.降低项目 JDK 版本为 11 或以下即可解决该问题(大部分情况下需要同步修改项目的 Gradle 和 Gradle Plugin 版本,改动较大,不建议
2.改用自定义 Gradle 插件形式创建 Task ,即可在 Task 中引用第三方的网络请求库如 OKHttp ,使用第三方的网络请求库封装 form-data,解决该问题(无需改动项目 JDK、Gradle 及 Gradle Plugin 版本,推荐使用该方案

方案 2 的具体实施,将在下一篇文章中进行演示
PS:如果你们可以在自定义 Gradle 文件中引用到第三方的网络请求库或者是正确编写携带文件的 form-data 网络请求,则可以自行更改部分代码后修复该问题

6.3 Gradle 控制台中文显示异常

studio64.exe.vmoptions 文件中输入

# 解决 gradle 控制台中文乱码问题
-Dfile.encoding=utf-8

打开 studio64.exe.vmoptions 文件
编辑 studio64.exe.vmoptions 文件

6.4 Android Studio 右侧 Gradle 栏内无法看到 Tasks 列表

在 Android studio 的 Setting 中找到最底部 Experimental ,取消如下图中的勾选并应用
显示 Gradle Tasks 列表

6.5 蒲公英 Webhook 配置

参考 https://seed.pgyer.com/WGNQkEpP

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/19687.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

差分隐私 MP-SPDZ框架安装

ubuntu虚拟机安装MP-SPDZ框架 1.下载安装包到虚拟机内 https://github.com/data61/MP-SPDZ/releases 安装git 报错Waiting for cache lock: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 4402(unattended-upgr) 解决方案 #杀死进程 sudo k…

JAVA 反编译工具

Releases deathmarine/Luyten GitHub 安装exe 打开拖入文件即可

Express接口

1.创建基本的服务器 // 导入express模块 const express require(express); const send require(send);// 创建express的 服务器实例 const app express()// 启动服务器 app.listen(80, () > {console.log(express server running at );})2.创建API路由接口 // 导入expr…

「网络编程」传输层协议_ TCP协议学习_及原理深入理解(二 - 完结)[万字详解]

「前言」文章内容大致是传输层协议&#xff0c;TCP协议讲解的第二篇&#xff0c;续上篇TCP。 「归属专栏」网络编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 二、TCP协议2.9 TCP连接管理机制2.9.1 三次握手2.9.2 四次挥手2.9.3 演示查看TIME_WAIT和CLOSE_WAIT状态2.9.…

MySQL 远程操作mysql

可以让别人在他们的电脑上操作我电脑上的数据库 create user admin identified with mysql_native_password by admin; //设置账号密码都为admingrant all on *.* to admin; //给admin账号授权 授权完成

使用elementplus实现文本框的粘贴复制

需求&#xff1a; 文本框仅用于显示展示数据并且用户可以进行复制&#xff0c;并不会进行修改和编辑&#xff0c; 注意点&#xff1a; 1.首先且文本为多行。所以不能使用普通的el-input&#xff0c;这种一行超出就会隐藏了&#xff0c;如果多行超出行数也会隐藏&#xff08;…

用blender做一层石墨烯

文章目录 1 创建正六边形2 复制正六边形3 阵列4 球棍模型 1 创建正六边形 ShiftA->网格->圆环->左下角出现添加圆环菜单&#xff0c;将顶点设为6&#xff0c;得到一个正六边形。按下tab键进入编辑模式->快捷键F填充&#xff0c;得到下图 2 复制正六边形 首先将轴…

Django的FBV和CBV

Django的FBV和CBV 基于django开发项目时&#xff0c;对于视图可以使用 FBV 和 CBV 两种模式编写。 FBV&#xff0c;function base views&#xff0c;其实就是编写函数来处理业务请求。 from django.contrib import admin from django.urls import path from app01 import view…

Redis主从复制、哨兵机制、集群分片

一.主从复制 1.概述 主从复制&#xff0c;是指将一台Redis服务器的数据&#xff0c;复制到其他的Redis服务器。前者称为主节点(master)&#xff0c;后者称为从节点(slave)。 数据的复制是单向的&#xff0c;只能由主节点到从节点默认情况下&#xff0c;每台Redis服务器都是主节…

机器学习深度学习——权重衰减

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——模型选择、欠拟合和过拟合 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你…

Vue2.0基础

1、概述 Vue(读音/vju/&#xff0c;类似于view)是一套用于构建用户界面的渐进式框架&#xff0c;发布于2014年2月。与其它大型框架不同的是&#xff0c;Vue被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff08;也就是可以理解为HTMLCSSJS&#xff09;&#xff…

使用Idea提交项目到远程仓库

使用Idea提交项目到远程仓库 1.在Idea中打开本地要推送的项目2.创建远程仓库并提交 1.在Idea中打开本地要推送的项目 tips: 首先你得有git工具&#xff0c;没有的话可以参考下面的这篇文章 git与gitee结合使用&#xff0c;提交代码&#xff0c;文件到远程仓库 从导航栏中选择 V…

uC-OS2 V2.93 STM32L476 移植:系统移植篇

前言 上一篇已经 通过 STM32CubeMX 搭建了 NUCLEO-L476RG STM32L476RG 的 裸机工程&#xff0c;并且下载了 uC-OS2 V2.93 的源码&#xff0c;接下来&#xff0c;开始系统移植 开发环境 win10 64位 Keil uVision5&#xff0c;MDK V5.36 uC-OS2 V2.93 开发板&#xff1a;NUC…

安全狗V3.512048版本绕过

安全狗安装 安全狗详细安装、遇见无此服务器解决、在windows中命令提示符中进入查看指定文件夹手动启动Apache_安全狗只支持 glibc_2.14 但是服务器是2.17_黑色地带(崛起)的博客-CSDN博客 安全狗 safedogwzApacheV3.5.exe 右键电脑右下角安全狗图标-->选择插件-->安装…

untiy代码打压缩包,可设置密码

1、简单介绍&#xff1a; 用的是一个插件SharpZipLib&#xff0c;在vs的Nuget下载&#xff0c;也可以去github下载https://github.com/icsharpcode/SharpZipLib 用这个最主要的是因为&#xff0c;这个不用请求windows的文件读写权限&#xff0c;关于这个权限我搞了好久&#…

【设计模式——学习笔记】23种设计模式——命令模式Command(原理讲解+应用场景介绍+案例介绍+Java代码实现)

案例引入 有一套智能家电&#xff0c;其中有照明灯、风扇、冰箱、洗衣机&#xff0c;这些智能家电来自不同的厂家&#xff0c;我们不想针对每一种家电都安装一个手机App来分别控制&#xff0c;希望只要一个app就可以控制全部智能家电要实现一个app控制所有智能家电的需要&…

Jenkins 自动化部署实例讲解,另附安装教程!

【2023】Jenkins入门与安装_jenkins最新版本_丶重明的博客-CSDN博客 也可以结合这个互补看 前言 你平常在做自己的项目时&#xff0c;是否有过部署项目太麻烦的想法&#xff1f;如果你是单体项目&#xff0c;可能没什么感触&#xff0c;但如果你是微服务项目&#xff0c;相…

JVM的组件、自动垃圾回收的工作原理、分代垃圾回收过程、可用的垃圾回收器类型

详细画的图片 https://www.processon.com/diagraming/64c8aa11c07d99075d934311 官方网址 https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html 相关概念 年轻代是所有新对象被分配和老化的地方。当年轻代填满时&#xff0c;这会导致minor …

Rust dyn - 动态分发 trait 对象

dyn - 动态分发 trait 对象 dyn是关键字&#xff0c;用于指示一个类型是动态分发&#xff08;dynamic dispatch&#xff09;&#xff0c;也就是说&#xff0c;它是通过trait object实现的。这意味着这个类型在编译期间不确定&#xff0c;只有在运行时才能确定。 practice tr…