概述
前言
每个项目都需要测试,没有测试的项目是无法发布到线上的
而由于安卓的碎片化,公司里测试需要测几种不同版本的系统和不同厂商(型号)的手机,所以我平时发的测试包必须放到某个服务器或网站上,通过二维码的方式给测试,这样才能让测试流程更方便
之前的流程都是,先打包,然后将包上传到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,所以下面不需要显式声明receiver
uploadToFir("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_token
val 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网站上提供的api
val 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_token
val 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网站上提供的api
val 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.apkMeta
updateInfo.androidManifest = file.manifestXml
updateInfo.versionName = meta.versionName
updateInfo.versionCode = meta.versionCode
updateInfo.packageName = meta.packageName
updateInfo.appName = meta.name
updateInfo.channel = getChannelName(file.manifestXml)
updateInfo.path = path
return 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.rootElement
val iterator = rootElement.elementIterator("application")
while (iterator.hasNext()) {
val next = iterator.next() as Element
val temp = next.elementIterator("meta-data")
while (temp.hasNext()) {
val meta = temp.next() as Element
val 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 true
version1 ?: return true
version2 ?: return true
val split = version1.split(".")
val split2 = version2.split(".")
if (split.size > split2.size)
return true
else if (split.size < split2.size)
return false
for (i in split.indices) {
if (split[i].toIntOrNull() ?: 0 > split2[i].toIntOrNull() ?: 0)
return true
else 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 = contentType
var res = ""
var conn: HttpURLConnection? = null
// boundary就是request头和上传文件内容的分隔符
val BOUNDARY = "---------------------------123821742118716"
try {
val url = URL(urlStr)
conn = url.openConnection() as HttpURLConnection
conn.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())
// text
if (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 String
val inputValue = entry.value as String? ?: continue
strBuf.append("rn").append("--").append(BOUNDARY).append("rn")
strBuf.append("Content-Disposition: form-data; name="$inputName"rnrn")
strBuf.append(inputValue)
}
out.write(strBuf.toString().toByteArray())
}
// file
if (fileMap != null) {
val iter: Iterator<*> = fileMap.entries.iterator()
while (iter.hasNext()) {
val entry = iter.next() as Map.Entry<*, *>
val inputName = entry.key as String
val inputValue = entry.value as String? ?: continue
val file = File(inputValue)
val filename: String = file.getName()
//没有传入文件类型,同时根据文件获取不到类型,默认采用application/octet-stream
contentType = 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("rn").append("--").append(BOUNDARY).append("rn")
strBuf.append("Content-Disposition: form-data; name="$inputName"; filename="$filename"rn")
strBuf.append("Content-Type:$contentTypernrn")
out.write(strBuf.toString().toByteArray())
val `in` = DataInputStream(FileInputStream(file))
var bytes = 0
val bufferOut = ByteArray(1024)
while (`in`.read(bufferOut).also { bytes = it } != -1) {
out.write(bufferOut, 0, bytes)
}
`in`.close()
}
}
val endData = "rn--$BOUNDARY--rn".toByteArray()
out.write(endData)
out.flush()
out.close()
// 读取返回数据
val strBuf = StringBuffer()
val reader = BufferedReader(InputStreamReader(conn.getInputStream()))
var line: String? = null
while (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
最后
以上就是微笑方盒为你收集整理的Gradle自动化之自动打包并上传到fir测试网站前言正文的全部内容,希望文章能够帮你解决Gradle自动化之自动打包并上传到fir测试网站前言正文所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复