Gradle自动化之自动打包并上传到fir测试网站

news/2024/5/19 22:03:41/文章来源:https://blog.csdn.net/qq_33505109/article/details/117439129

前言

每个项目都需要测试,没有测试的项目是无法发布到线上的

而由于安卓的碎片化,公司里测试需要测几种不同版本的系统和不同厂商(型号)的手机,所以我平时发的测试包必须放到某个服务器或网站上,通过二维码的方式给测试,这样才能让测试流程更方便

之前的流程都是,先打包,然后将包上传到fir测试网站上,然后将二维码发给测试们,感觉很麻烦,还是写一个自动化的插件比较好,而Android开发的管理工具Gradle正好也是支持自动化的,所以就用Gradle来做一个自动打包上传测试的功能

正文

首先我们创建一个Task任务,这个Task任务就是自动打包上传测试的任务,可以在app或者跟目录的build.gradle(.kts)文件中加入task

Groovy语言:

task firDebug(type: Task) {
}

Kotlin(kts)语言:现在Gradle也支持用Kotlin脚本(kts)来写了,我不太会Groovy语法,所以就直接用的kts写的2333

tasks.register<Task>("firDebug") {
}

然后在根项目下创建buildSrc目录,来用Kotlin编写脚本代码,参考:https://mp.weixin.qq.com/s/mVqShijGTExtQ_nLslchpQ  和  https://mp.weixin.qq.com/s/xs164Y1Oi4rEZfhKCoUnGA  (感谢Benny Huo老师的文章)

目录长这个样子

在build.gradle.kts中加入如下代码:

plugins {`kotlin-dsl`//可以使用kotlin写Gradle
}repositories {maven("https://mirrors.tencent.com/nexus/repository/maven-public/")
}dependencies {implementation("com.google.code.gson:gson:2.8.6")//这几个是一会需要用到的库implementation("net.dongliu:apk-parser:2.6.9")implementation("dom4j:dom4j:1.6.1")implementation("com.squareup.okio:okio:2.10.0")implementation("javax.activation:activation:1.1.1")//ps:jdk11后需要手动引用
}

然后直接在kotlin目录下写相应的代码,先写一个入口代码,文件:Fir.kt

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/
fun Task.uploadToFir(module: String, channel: String, type: String) {
}

然后修改task任务:

Groovy

task firDebug(type: Task) {FirKt.uploadToFir(it,"app",你的渠道,"debug")//用java的方式调用kt的扩展函数
}

Kotlin(kts)

tasks.register<Task>("firDebug") {//这里的this就是task,所以下面不需要显式声明receiveruploadToFir("app", 你的渠道, "debug")
}

然后填充uploadToFir方法,上面写的有注释:

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/
fun Task.uploadToFir(module: String, channel: String, type: String) {// TODO by lt 修改fir的api_tokenval firApiToken = ""val path =":$module:assemble${channel[0].toUpperCase()}${channel.substring(1)}${type[0].toUpperCase()}${type.substring(1)}"//拼一下需要执行的打包的代码,类似这样子:app:assembleGoogleDebug"fir.uploadToFir: start assemble. path=$path".println()//执行打包dependsOn(path)doLast {//在该任务其他代码执行完毕后,在执行该lambda中的代码//获取apk包val inputFile = getApkFile(module, channel, type)//去指定目录下找到刚才打出来的apk包"fir.uploadToFir: get apk file success. file=${inputFile.absolutePath}".println()val apkInfo =parseApkFile(inputFile.absolutePath) ?: throw FileNotFoundException("找不到打出来的apk包")//获取到apk的信息"fir.uploadToFir: get apk info=${apkInfo.toJson()}".println()//下面的两个网络请求是fir网站上提供的apival tokenResult = post("http://api.bq04.com/apps", mapOf("type" to "android","bundle_id" to apkInfo.packageName!!,"api_token" to firApiToken), null, "application/octet-stream")//post请求的方法(从网上找的一个java原生网络请求)val tokenBean = tokenResult.jsonToAny<FirTokenBean>()"fir.uploadToFir: get token success. result=$tokenResult".println()val binary =tokenBean?.cert?.binary ?: throw RuntimeException("网络数据有误:${tokenBean.toJson()}")post(binary.upload_url!!,mapOf("key" to binary.key!!,"token" to binary.token!!,"x:name" to apkInfo.appName!!,"x:version" to apkInfo.versionName!!,"x:build" to apkInfo.versionCode.toString()),mapOf("file" to inputFile.absolutePath),"application/octet-stream").println()}
}

ps:最后会放出完整代码

其实很简单,就是执行一下打包命令,然后找到打出来的apk包,并获取apk包的信息,最后通过接口将apk上传到fir网站上,然后就ok了

我这里省略了将二维码或者网址发给测试的功能,如果需要的话可以建一个钉钉群,将测试拉进去,然后使用钉钉的机器人功能直接每次打完包将二维码发到群里就好了

完整代码如下: 

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/
fun Task.uploadToFir(module: String, channel: String, type: String) {// TODO by lt 修改fir的api_tokenval firApiToken = ""val path =":$module:assemble${channel[0].toUpperCase()}${channel.substring(1)}${type[0].toUpperCase()}${type.substring(1)}"//拼一下需要执行的打包的代码,类似这样子:app:assembleGoogleDebug"fir.uploadToFir: start assemble. path=$path".println()//执行打包dependsOn(path)doLast {//在该任务其他代码执行完毕后,在执行该lambda中的代码//获取apk包val inputFile = getApkFile(module, channel, type)//去指定目录下找到刚才打出来的apk包"fir.uploadToFir: get apk file success. file=${inputFile.absolutePath}".println()val apkInfo =parseApkFile(inputFile.absolutePath) ?: throw FileNotFoundException("找不到打出来的apk包")//获取到apk的信息"fir.uploadToFir: get apk info=${apkInfo.toJson()}".println()//下面的两个网络请求是fir网站上提供的apival tokenResult = post("http://api.bq04.com/apps", mapOf("type" to "android","bundle_id" to apkInfo.packageName!!,"api_token" to firApiToken), null, "application/octet-stream")//post请求的方法(从网上找的一个java原生网络请求)val tokenBean = tokenResult.jsonToAny<FirTokenBean>()"fir.uploadToFir: get token success. result=$tokenResult".println()val binary =tokenBean?.cert?.binary ?: throw RuntimeException("网络数据有误:${tokenBean.toJson()}")post(binary.upload_url!!,mapOf("key" to binary.key!!,"token" to binary.token!!,"x:name" to apkInfo.appName!!,"x:version" to apkInfo.versionName!!,"x:build" to apkInfo.versionCode.toString()),mapOf("file" to inputFile.absolutePath),"application/octet-stream").println()}
}/*** 获取apk文件中的一些数据*/
fun parseApkFile(path: String): UpdateInfo? {try {ApkFile(path).use { file ->val updateInfo = UpdateInfo()val meta = file.apkMetaupdateInfo.androidManifest = file.manifestXmlupdateInfo.versionName = meta.versionNameupdateInfo.versionCode = meta.versionCodeupdateInfo.packageName = meta.packageNameupdateInfo.appName = meta.nameupdateInfo.channel = getChannelName(file.manifestXml)updateInfo.path = pathreturn updateInfo}} catch (e: Exception) {return null}
}/*** 根据渠道,type,module取到包*/
fun getApkFile(module: String, channel: String, type: String): File =File("$module/build/outputs/apk/$channel/$type").listFiles()!!.toList().filter { it.name.endsWith(".apk") }.sortedWith { s1, s2 ->if (checkVersion(s1.name, s2.name)) 1 else -1}.last()data class UpdateInfo(var androidManifest: String? = null,var versionName: String? = null,var versionCode: Long = 0,var channel: String? = null,var packageName: String? = null,var appName: String? = null,var path: String? = null
)private fun getManifestMetaData(xml: String): List<Pair<String?, String?>> {val datas: MutableList<Pair<String?, String?>> = ArrayList()val reader = SAXReader()try {val document = reader.read(ByteArrayInputStream(xml.toByteArray(StandardCharsets.UTF_8)))val rootElement = document.rootElementval iterator = rootElement.elementIterator("application")while (iterator.hasNext()) {val next = iterator.next() as Elementval temp = next.elementIterator("meta-data")while (temp.hasNext()) {val meta = temp.next() as Elementval metaName = meta.attributeValue("name")val metaValue = meta.attributeValue("value")datas.add(Pair<String?, String?>(metaName, metaValue))}}} catch (e: DocumentException) {e.printStackTrace()}return datas
}private fun getChannelName(xml: String): String? {val metaData = getManifestMetaData(xml)val umeng_channels: List<Pair<String?, String?>> =metaData.stream().filter { (first) -> first == "UMENG_CHANNEL" }.collect(Collectors.toList())return if (!umeng_channels.isEmpty()) {umeng_channels[0].second} else null
}/*** 判断两个版本号哪个大* @return true 表示前面大或相等*/
private fun checkVersion(version1: String?, version2: String?): Boolean {try {if (version1 == version2)return trueversion1 ?: return trueversion2 ?: return trueval split = version1.split(".")val split2 = version2.split(".")if (split.size > split2.size)return trueelse if (split.size < split2.size)return falsefor (i in split.indices) {if (split[i].toIntOrNull() ?: 0 > split2[i].toIntOrNull() ?: 0)return trueelse if (split[i].toIntOrNull() ?: 0 < split2[i].toIntOrNull() ?: 0)return false}return true} catch (e: Exception) {return true}
}fun Any?.println() = println(this)fun Any?.toJson(): String? = Gson().toJson(this)inline fun <reified T> String?.jsonToAny(): T? = Gson().fromJson(this, T::class.java)data class FirTokenBean(val app_user_id: String? = null,val cert: Cert? = null,val download_domain: String? = null,val download_domain_https_ready: Boolean = false,val form_method: String? = null,val id: String? = null,val short: String? = null,val storage: String? = null,val type: String? = null,val user_system_default_download_domain: String? = null
) {data class Cert(val binary: Binary? = null,val icon: Icon? = null,val mqc: Mqc? = null,val prefix: String? = null,val support: String? = null)data class Binary(val custom_headers: CustomHeaders? = null,val key: String? = null,val token: String? = null,val upload_url: String? = null)data class Icon(val custom_callback_data: CustomCallbackData? = null,val custom_headers: CustomHeadersX? = null,val key: String? = null,val token: String? = null,val upload_url: String? = null)data class Mqc(val is_mqc_availabled: Boolean = false,val total: Int = 0,val used: Int = 0)class CustomHeaders()data class CustomCallbackData(val original_key: String? = null)class CustomHeadersX()
}/*** 上传图片* @param urlStr* @param textMap* @param fileMap* @param contentType 没有传入文件类型默认采用application/octet-stream* contentType非空采用filename匹配默认的图片类型* @return 返回response数据*/
fun post(urlStr: String, textMap: Map<String, String>?,fileMap: Map<String, String>?, contentType: String?
): String {var contentType = contentTypevar res = ""var conn: HttpURLConnection? = null// boundary就是request头和上传文件内容的分隔符val BOUNDARY = "---------------------------123821742118716"try {val url = URL(urlStr)conn = url.openConnection() as HttpURLConnectionconn.setConnectTimeout(5000)conn.setReadTimeout(30000)conn.setDoOutput(true)conn.setDoInput(true)conn.setUseCaches(false)conn.setRequestMethod("POST")conn.setRequestProperty("Connection", "Keep-Alive")// conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=$BOUNDARY")val out: OutputStream = DataOutputStream(conn.getOutputStream())// textif (textMap != null) {val strBuf = StringBuffer()val iter: Iterator<*> = textMap.entries.iterator()while (iter.hasNext()) {val entry = iter.next() as Map.Entry<*, *>val inputName = entry.key as Stringval inputValue = entry.value as String? ?: continuestrBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n")strBuf.append("Content-Disposition: form-data; name=\"$inputName\"\r\n\r\n")strBuf.append(inputValue)}out.write(strBuf.toString().toByteArray())}// fileif (fileMap != null) {val iter: Iterator<*> = fileMap.entries.iterator()while (iter.hasNext()) {val entry = iter.next() as Map.Entry<*, *>val inputName = entry.key as Stringval inputValue = entry.value as String? ?: continueval file = File(inputValue)val filename: String = file.getName()//没有传入文件类型,同时根据文件获取不到类型,默认采用application/octet-streamcontentType = MimetypesFileTypeMap().getContentType(file)//contentType非空采用filename匹配默认的图片类型if ("" != contentType) {if (filename.endsWith(".png")) {contentType = "image/png"} else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".jpe")) {contentType = "image/jpeg"} else if (filename.endsWith(".gif")) {contentType = "image/gif"} else if (filename.endsWith(".ico")) {contentType = "image/image/x-icon"}}if (contentType == null || "" == contentType) {contentType = "application/octet-stream"}val strBuf = StringBuffer()strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n")strBuf.append("Content-Disposition: form-data; name=\"$inputName\"; filename=\"$filename\"\r\n")strBuf.append("Content-Type:$contentType\r\n\r\n")out.write(strBuf.toString().toByteArray())val `in` = DataInputStream(FileInputStream(file))var bytes = 0val bufferOut = ByteArray(1024)while (`in`.read(bufferOut).also { bytes = it } != -1) {out.write(bufferOut, 0, bytes)}`in`.close()}}val endData = "\r\n--$BOUNDARY--\r\n".toByteArray()out.write(endData)out.flush()out.close()// 读取返回数据val strBuf = StringBuffer()val reader = BufferedReader(InputStreamReader(conn.getInputStream()))var line: String? = nullwhile (reader.readLine().also { line = it } != null) {strBuf.append(line).append("\n")}res = strBuf.toString()reader.close()} catch (e: Exception) {println("发送POST请求出错。$urlStr")e.printStackTrace()} finally {conn?.disconnect()}return res
}

end

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

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

相关文章

华为云服务的购买和建站

1、购买华为云服务&#xff08;选择centos7.6&#xff09;其他设置默认 2、去控制台先关机然后重置密码 3、设置安全组&#xff08;相当于门卫&#xff09; 4、常见端口 使用SSH SecureShell Client建站 使用Xshell6和Xftp6建站&#xff08;推荐&#xff09;

网站可以正常访问但ping不通

原因&#xff1a; 网站服务器为了防止DoS攻击&#xff0c;通常在防火墙里设置拦截ICMP报文&#xff0c;而ping报文正是ICMP报文的一种&#xff0c;当然ping不通了。 名称解析&#xff1a; DoS攻击&#xff1a;DoS是Denial of Service的简称&#xff0c;即拒绝服务&#xff0…

大型网站架构改进历程:存储的瓶颈(上)

&#xfeff;&#xfeff;大型网站架构改进历程&#xff1a;存储的瓶颈&#xff08;上&#xff09; width"22" height"16" src"http://hits.sinajs.cn/A1/weiboshare.html?urlhttp%3A%2F%2Fwww.csdn.net%2Farticle%2F2015-01-22%2F2823669%2F1&t…

大型网站架构演变和知识体系--转

之前也有一些介绍大型网站架构演变的文章&#xff0c;例如LiveJournal的、ebay的&#xff0c;都是非常值得参考的&#xff0c;不过感觉他们讲的更多的是每次演变的结果&#xff0c;而没有很详细的讲为什么需要做这样的演变&#xff0c;再加上近来感觉有不少同学都很难明白为什么…

白话Elasticsearch48-深入聚合数据分析之 Percentiles Aggregation-percentiles百分比算法以及网站访问时延统计及Percentiles优化

文章目录概述官方说明示例Percentiles优化 compression概述 继续跟中华石杉老师学习ES&#xff0c;第48篇 课程地址&#xff1a; https://www.roncoo.com/view/55 官方说明 Percentiles Aggregation&#xff1a; 戳这里 示例 需求&#xff1a; 网站访问时延统计 为了演示…

白话Elasticsearch49-深入聚合数据分析之 Percentile Ranks Aggregation-percentiles rank以及网站访问时延SLA统计

文章目录概述官方说明案例概述 继续跟中华石杉老师学习ES&#xff0c;第49篇 课程地址&#xff1a; https://www.roncoo.com/view/55 官方说明 Percentiles Ranks Aggregation&#xff1a;戳这里 更多请参考官网 案例 需求&#xff1a;在200ms以内的&#xff0c;有百分之多少…

怎么取html网页中的样式,从建站到拿站 -- HTML和CSS基础

总有人会陪你在慌乱无序的生活里同步前行。。。---- 网易云热评一、简介HTML指的是超文本标记语言&#xff0c;使用标记标签来描述网页&#xff0c;标签是由尖括号和关键词组成&#xff0c;并且是成对出现&#xff0c;例如。二、一个完整的html网页周杰伦《说好不哭》词&#…

如何在时间紧迫情况下进行机器学习:构建标记的新闻 数据 库 开发 标记 网站 阅读1629 原文:How we built Tagger News: machine learning on a

如何在时间紧迫情况下进行机器学习&#xff1a;构建标记的新闻 数据 库 开发 标记 网站阅读1629 原文&#xff1a;How we built Tagger News: machine learning on a tight schedule 作者&#xff1a;David Robinson 翻译&#xff1a;Diwei 译者注&#xff1a;本文介绍了作者和…

织梦网站被黑客生成html,dedecms网站被挂马怎么处理

dedecms被批量挂马后如何处理&#xff1f;我们知道一般站长选择织梦系统是因为其支持生成静态页面以便于seo优化。但是根据西部数码west263.com开发工程师刘工介绍&#xff0c;一般被挂马的网站不单单是一个页面&#xff0c;目前的织梦挂马已经逐步演变呈多个目录&#xff0c;根…

jvm性能调优实战 - 61常用的JVM调优网站

文章目录线程Dump日志分析堆Dump可视化分析GC日志分析Alibaba ArthasAliabba jvmGeneratePerfMaPerfMa - XXFox &#xff08;Java虚拟机参数分析&#xff09;PerfMa - XSheepdog (Java线程Dump分析)PerfMa - XElephant (Java内存Dump分析)线程Dump日志分析 https://fastthread…

高并发高流量网站架构详解--转载

原文地址&#xff1a;http://www.ha97.com/818.html Web2.0的兴起&#xff0c;掀起了互联网新一轮的网络创业大潮。以用户为导 向的新网站建设概念&#xff0c;细分了网站功能和用户群&#xff0c;不仅成功的造就了一大批新生的网站&#xff0c;也极大的方便了上网的人们。但We…

零基础,最完整的WordPress建站教程

网站域名空间和数据库网站程序模板 1准备材料 【域名】 网址就相当于家的住址&#xff0c;记住和找到家的位置。可在阿里云&#xff0c;新网&#xff0c;爱名网或其它IDC商购买注册。 【空间和数据库】 就是盖房子的地基。同样可在阿里云&#xff0c;新网&#xff0c;爱名网…

微软新冠: 数据分析网站 COVID Insights

洞察疫情&#xff0c;微软推出新冠数据分析网站 COVID Insights COVID Insights 网站功能亮点 持续数月的新冠疫情一路肆虐、席卷全球&#xff0c;世界各地的科研人员都在为此奋战&#xff0c;希望通过最先进的技术逐步揭开新冠病毒的神秘面纱。 近日&#xff0c;微软亚洲研…

大型网站架构演化历程

http://www.hollischuang.com/archives/728 本文内容大部分来自《大型网站技术架构》,这本书很值得一看&#xff0c;强烈推荐。 大型网站系统的特点 高并发&#xff0c;大流量 需要面对高并发用户&#xff0c;大流量访问。Google 日均 PV 35 亿&#xff0c;日 IP 访问数 3 亿&a…

大型网站架构技术一览

http://www.hollischuang.com/archives/1132 本文内容大部分来自《大型网站技术架构》,这本书很值得一看&#xff0c;强烈推荐。 网站系统架构层次如下图所示&#xff1a; 1.前端架构 前端指用户请求到达网站应用服务器之前经历的环节&#xff0c;通常不包含网站业务逻辑&#…

Lambda架构与推荐在电商网站实践

王富平 现为1号店搜索与精准化部门架构师&#xff0c;之前在百度从事数据挖掘相关工作&#xff0c;对实时处理有着深刻的研究。一直从事大数据相关研发工作&#xff0c;2013年开发了一款SQL实时处理框架&#xff0c;致力于建设高可用的大数据业务系统。一、Lambda架构Lambda架构…

【数据分析】Python :视频网站数据清洗整理和结论研究

视频网站数据清洗整理和结论研究 要求&#xff1a; 1、数据清洗 - 去除空值 要求&#xff1a;创建函数提示&#xff1a;fillna方法填充缺失数据&#xff0c;注意inplace参数 2、数据清洗 - 时间标签转化 要求&#xff1a; ① 将时间字段改为时间标签 ② 创建函数提示&#…

京东前端:PhantomJS 和NodeJS在网站前端监控平台的最佳实践

http://www.infoq.com/cn/articles/practise-of-phantomjs-and-nodejs-in-jingdong 1. 为什么需要一个前端监控系统 通常在一个大型的 Web 项目中有很多监控系统&#xff0c;比如后端的服务 API 监控&#xff0c;接口存活、调用、延迟等监控&#xff0c;这些一般都用来监控后台…

手把手教您制作并发布个人网站或主页(一)(图解教程针对小白)

很多人都像我一样&#xff0c;从小就有个梦想&#xff0c;就是能自己制作一个属于自己的网站&#xff0c;下面就让我图解一下制作过程&#xff0c;针对广大小白&#xff0c;大神绕道。 首先&#xff0c;我推荐一款类似于记事本的编译工具&#xff0c;叫sublime2 Text2&#xff…

如何在github发布个人网站或开源项目-手把手教您制作并发布个人网站或主页(二)

&#xff08;首先感谢留美博士czxttkl的技术支持maider.blog.sohu.com&#xff09; 作为开源代码库以及版本控制系统&#xff0c;Github目前拥有140多万开发者用户。随着越来越多的应用程序转移到了云上&#xff0c;Github已经成为了管理软件开发以及发现已有代码的首选方法。…