Android打包、上传蒲公英、发通知到钉钉一条龙服务

752 阅读11分钟

本文将介绍Android通过gradle进行打包,上传,以及发送通知到钉钉群聊中的实现流程,闲暇之下摸索摸索,涨涨姿势

Gradle

打包

打包相信大家都很熟悉了,在平时的开发过程中有很多时候都在打包,调试等,而打包的这套流程Android已经给咱们提供了,就在我们平时运行的时候,build列表中的一个个task,而咱们只需执行assembleDebug/assembleRelease命令就可以打包了,这两个task在AS的gradle栏中也可以看到,在这里双击也可以进行打包,这里显示的可能与你的不一样,是根据自己项目中配置的buildTypesproductFlavors决定的。

image.png

打包后的apk文件就保存在项目中的app/build/outputs/apk/ 目录下

上传蒲公英(摘自蒲公英文档说明:www.pgyer.com/doc/view/ap…)

新版本的蒲公英分为三步:

  1. 获取上传的 token, api地址(www.pgyer.com/apiv2/app/g…)通过该接口,可以获取预上传 url 和相关的签名参数

    POST参数

    参数类型说明
    _api_keyString(必填) API Key 点击获取_api_key
    buildTypeString(必填) 需要上传的应用类型,如果是iOS类型请传iosipa,如果是Android类型请传androidapk
    overseaInteger(选填) 是否使用海外加速上传,值为:1 使用海外加速上传,2 国内加速上传;留空根据 IP 自动判断海外加速或国内加速
    buildInstallTypeInteger(选填)应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装
    buildPasswordString(选填) 设置App安装密码,密码为空时默认公开安装
    buildUpdateDescriptionString(选填) 版本更新描述,请传空字符串,或不传。
    buildInstallDateInteger(选填)是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置
    buildInstallStartDateString(选填)安装有效期开始时间,字符串型,如:2018-01-01
    buildInstallEndDateString(选填)安装有效期结束时间,字符串型,如:2018-12-31
    buildChannelShortcutString(选填)所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd

    返回数据

    参数 | 类型 | 说明 | | -------- | ------ | ---------------------------------------------- | | key | String | key 上传文件存储标识唯一 key | | endpoint | String | 上传文件的 URL | | params | Object | 上传文件需要的参数,包含signature、x-cos-security-token、key

  2. 上传文件到第一步获取的 URL(字段名:endpoint),在这一步中上传 App 成功后,App 会自动进入服务器后台队列继续后续的发布流程。所以,在这一步中 App 上传完成后,并不代表能及时的获取到成功的响应。一般来说,一般1分钟以内就能完成发布。所以要多次进行检查是否发布完成,直到收到发布成功的返回,检查接口调用下一步中的 API。

    POST参数

    key | String | (必填) 从上一步响应中得到 | | -------------------- | ------ | ------------------------------- | | signature | String | (必填) 从上一步响应中得到 | | x-cos-security-token | String | (必填) 从上一步响应中得到 | | x-cos-meta-file-name | String | (选填) 上传的原始文件名,如 app-release.apk | | file | File | (必填) App 文件的本地路径

    返回数据

    如果上传成功:返回 http 状态码为 204 No Content; 如果上传失败:返回相应错误信息说明

  3. 检测应用是否发布完成,并获取发布应用的信息 api地址(www.pgyer.com/apiv2/app/b…),如果返回 code = 1246 ,可间隔 3s ~ 5s 重新调用 URL 进行检测,直到返回成功或失败。

    参数

    参数类型说明
    _api_keyString(必填) API Key 点击获取_api_key
    buildKeyString(必填) 第一步中返回的key

    返回的值分为以下 3 种情况:

    发布成功返回数据

    参数类型说明
    buildKeyStringBuild Key是唯一标识应用的索引ID
    buildTypeInteger应用类型(1:iOS; 2:Android)
    buildIsFirstInteger是否是第一个App(1:是; 2:否)
    buildIsLastestInteger是否是最新版(1:是; 2:否)
    buildFileSizeIntegerApp 文件大小
    buildNameString应用名称
    buildVersionString版本号, 默认为1.0 (是应用向用户宣传时候用到的标识,例如:1.1、8.2.1等。)
    buildVersionNoString上传包的版本编号,默认为1 (即编译的版本号,一般来说,编译一次会变动一次这个版本号, 在 Android 上叫 Version Code。对于 iOS 来说,是字符串类型;对于 Android 来说是一个整数。例如:1001,28等。)
    buildBuildVersionInteger蒲公英生成的用于区分历史版本的build号
    buildIdentifierString应用程序包名,iOS为BundleId,Android为包名
    buildIconString应用的Icon图标key,访问地址为 www.pgyer.com/image/view/…]
    buildDescriptionString应用介绍
    buildUpdateDescriptionString应用更新说明
    buildScreenShotsString应用截图的key,获取地址为 www.pgyer.com/image/view/…]
    buildShortcutUrlString应用短链接
    buildQRCodeURLString应用二维码地址
    buildCreatedString应用上传时间
    buildUpdatedString应用更新时间

    发布成功失败返回数据

    参数类型说明
    codeInteger错误码,1216 应用发布失败
    messageString信息提示

    正在发布返回数据

    参数类型说明
    codeInteger错误码,1247 应用正在发布中
    messageString信息提示

    如果返回 code = 1246 ,可间隔 3s ~ 5s 重新调用 URL 进行检测,直到返回成功或失败。

实现代码

获取上传Token

/**
 *  获取上传的 token
 *  key     String     key 上传文件存储标识唯一 key
 *  endpoint   String 上传文件的 URL
 *  params Object 上传文件需要的参数,包含signature、x-cos-security-token、key
 */
private fun getToken(
    apiKey: String,
    updateDesc: String,
    buildInstallType: String,
    httpCallback: HttpCallback
) {
    val multipartBody = MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("_api_key", apiKey)
        .addFormDataPart("buildUpdateDescription", updateDesc)
        .addFormDataPart(
            "buildInstallType",
            buildInstallType
        )//(必填)应用安装方式,默认为1 公开安装 1:公开安装,2:密码安装,3:邀请安装
        .addFormDataPart("buildPassword", "111111")//安装密码  buildInstallType为2时需要此参数
        .addFormDataPart("buildType", "android")//平台类型  
        .build()

    val okHttpClient = OkHttpClient()
    val request = Request.Builder()
        .url(GET_TOKEN_URL)
        .post(multipartBody)
        .build()
    val response = okHttpClient.newCall(request).execute()
    if (!response.isSuccessful) {
        throw IOException("上传失败,http code is ${response.code}")
    }
    val responseBody = response.body?.string()
    val result = Gson().fromJson(responseBody, JsonObject::class.java)
    println("请求token结果为:$result")

    val code = result.get("code").asInt
    if (code != SUCCESS_CODE) {
        println(responseBody)
        val errorMsg = result.get("message").asString
        httpCallback.onError(code, errorMsg)
        throw IOException("上传失败,$errorMsg")
    }

    val paramsObject = result["data"].asJsonObject["params"].asJsonObject
    val key = result["data"].asJsonObject["key"].asString
    val endpoint = result["data"].asJsonObject["endpoint"].asString
    httpCallback.onSuccess(code, paramsObject, key, endpoint)
}

Token获取成功后上传apk

/**
 * 上传文件到蒲公英
 */
private fun uploadFileToServer(
    uploadBody: MultipartBody,
    url: String,
    uploadFileToServiceCallback: UploadFileToServiceCallback
) {
    val request = Request.Builder()
        .url(url)
        .post(uploadBody)
        .build()
    val response = OkHttpClient().newCall(request).execute()
    if (!response.isSuccessful) {
        uploadFileToServiceCallback.onUploadError(response.code, "上传失败!")
        throw IOException("上传失败,http code is ${response.code}")
    }

    //如果上传成功:返回 http 状态码为 204 No Content; 如果上传失败:返回相应错误信息说明
    if (response.code == FILE_UPLOAD_SUCCESSFUL) {
        uploadFileToServiceCallback.onUploadBack(FILE_UPLOAD_SUCCESSFUL, "文件上传成功等待同步数据");
    } else {
        uploadFileToServiceCallback.onUploadError(response.code, "上传失败!");
    }

}

检查上传结果

/**
 * 检查
 */
private fun checkUploadResult(checkUploadResultUrl: String) {
    println("请耐心等待一会~")
    Sleep().doSleep(3000)

    val request = Request.Builder()
        .url(checkUploadResultUrl)
        .get()
        .build()

    val response = OkHttpClient().newCall(request).execute()
    if (!response.isSuccessful) {
        throw IOException("检查失败,http code is ${response.code}")
    }

    val responseBody = response.body?.string()
    val result = Gson().fromJson(responseBody, JsonObject::class.java)
    val code = result["code"].asInt
    val message = result["message"].asString
    when (code) {
        //需要过会再次请求,给蒲公英一个同步的时间
        CHECK_UPLOAD_ERROR_CODE_1027, CHECK_UPLOAD_ERROR_CODE_1026 -> {
            println(message)
            checkUploadResult(checkUploadResultUrl)
        }
        //获取结果成功
        SUCCESS_CODE -> {
            println("上传结果:$result")
            prepareNotify(result)
        }
        else -> {
            println("上传结果:$result")
        }
    }
}

说明:代码的存放位置是在buildSrc中,调用方式通过task来调用。

发送通知到钉钉

发送通知到钉钉这一步就是在我们上传apk到蒲公英成功后进行,而发送通知自然也是需要调用钉钉的接口,接口就是咱们在群里创建的钉钉机器人生成的webHook

1、添加机器人:

第一步

2、自定义关键词:我这里使用的是自定义关键词(在使用自定义关键词时,调用接口发送通知的时候也需将关键词带上,不然接口会报错)

第二步

3、复制生成的webHook:

第三步

创建好之后就是接入,其实就是调用咱们上边生成的webHook,这里可以看下钉钉的文档,里边有详细介绍: open.dingtalk.com/document/gr…

发送消息类型(msgType)支持:
文本 (text)
链接 (link)
markdown(markdown)
ActionCard
FeedCard

发送通知实现具体代码:(我这里发送通知的格式是markdown格式的消息)

/**
 * 发送钉钉消息
 * https://open.dingtalk.com/document/group/custom-robot-access  推送消息钉钉文档
 */
private fun sendDingDingMessage(
    title: String,//通知标题
    message: String,//通知内容
    dingDingRobotUrl: String,//webHook
    buildQRCodeURL:String//上传apk到蒲公英后生成的二维码地址
) {

    val contentJob = JsonObject()
    contentJob.addProperty("title","## $title")
    contentJob.addProperty("text", "$message   111 \n@123,@321")
    contentJob.addProperty("messageUrl","https://www.pgyer.com/xxx")
    contentJob.addProperty("picUrl", buildQRCodeURL)

    val mobiles = JsonArray()
    mobiles.add(123)
    mobiles.add(321)
    val atJob = JsonObject()
    atJob.addProperty("isAtAll",false) //是否@所有人 如果不是则需要传具体@人的手机号
    atJob.add("atMobiles",mobiles)//at人的手机号  (在发送内容中也需要有艾特人的手机号)

    val body = JsonObject()
        .apply {
            addProperty("msgtype", "markdown") //发送消息的类型
            add("markdown",contentJob)
            add("at",atJob)
        }.toString()
    val request = Request
        .Builder()
        .url(dingDingRobotUrl)// 机器人地址
        .post(body.toRequestBody("application/json".toMediaType()))
        .build()
    val response = OkHttpClient().newCall(request).execute()
    if (!response.isSuccessful) {
        throw IOException("发送消息失败,http code:${response.code}")
    }
    val resultText = response.body!!.string()
    val result = Gson().fromJson(resultText, JsonObject::class.java)
    //println("sendDingDingMessage: $result")
    require(result.get("errcode").asInt == 0) { "发送消息失败,${resultText}" }
    println("发送钉钉通知成功")
}

这里附上效果图来进行比较:

image.png

补充

我这里将调用上传和发送通知的代码封装在了一个类中(PygerTask),这个类存放的目录是在buildSrc中,如果有不清楚buildSrc的小伙伴,可以查下buildSrc是什么(传送道),代码中的upload-pgyer-desc.txt,是一个更新说明填写文件,每次打包上传前将更新的内容填进去,然后代码里边会读取出来,进行上传,此文件的存放目录大家可以自定义,最好是根目录,比较好找,我这里是放在了app模块得根目录下,上传和发送通知我这里用的是okhttp

PygerTask全部代码:

/**获取上传token*/
internal const val GET_TOKEN_URL = "https://www.pgyer.com/apiv2/app/getCOSToken"

/**上传成功*/
internal const val FILE_UPLOAD_SUCCESSFUL = 204

/**检查上传结果*/
internal const val CHECK_UPLOAD_RESULT = "https://www.pgyer.com/apiv2/app/buildInfo"

/**检查上传结果code*/
internal const val CHECK_UPLOAD_ERROR_CODE_1027 = 1247
internal const val CHECK_UPLOAD_ERROR_CODE_1026 = 1246

internal const val SUCCESS_CODE = 0


open class PygerTask : DefaultTask() {
    /** Pgyer的上传apiKey */
    @Input
    var apiKey: String = ""

    /** 上传的apk路径,相对project的路径*/
    @Input
    var apkPath: String = ""

    /** 钉钉机器人通知地址 */
    @Input
    var dingDingRobotUrl: String = ""

    /** 更新描述标题 */
    @Input
    var updateTitle: String = ""

    /** 更新描述后缀 */
    @Input
    var updateDescSuffix: String = ""

    /** 更新描述文件路径,相对project的路径 */
    @Input
    var updateDescPath: String = "upload-pgyer-desc.txt"

    /**安装方式*/
    private val buildInstallType = 1 //公开安装 1:公开安装,2:密码安装,3:邀请安装

    init {
        doFirst {
            require(apiKey.isNotEmpty()) { "apiKey must not null or empty" }
            require(apkPath.isNotEmpty()) { "apkPath must not null or empty" }
            require(dingDingRobotUrl.isNotEmpty()) { "dingDingRobotUrl must not null or empty" }
            require(updateTitle.isNotEmpty()) { "updateTitle must not null or empty" }
        }
        doLast {
            performUpload()
        }
    }

    final override fun doFirst(action: Action<in Task>): Task {
        return super.doFirst(action)
    }

    final override fun doFirst(action: Closure<*>): Task {
        return super.doFirst(action)
    }

    final override fun doFirst(actionName: String, action: Action<in Task>): Task {
        return super.doFirst(actionName, action)
    }

    final override fun doLast(action: Action<in Task>): Task {
        return super.doLast(action)
    }

    final override fun doLast(action: Closure<*>): Task {
        return super.doLast(action)
    }

    final override fun doLast(actionName: String, action: Action<in Task>): Task {
        return super.doLast(actionName, action)
    }

    private fun performUpload() {

        println("上传apk到pgyer")
        //todo 先注释git检查
        //requireGitUpToDate()
        // 检查更新描述文件
        val descFile = project.file(updateDescPath)
        val updateDesc = descFile.readText(Charset.forName("utf-8"))
        println(updateDesc)

        if (updateDesc.isEmpty()) {
            throw RuntimeException("请填写更新描述到[$descFile]中")
        }

        //获取上传的apk文件路径
        val files = project.file("build/outputs/apk/$apkPath").listFiles()
        var currentApkPath = ""
        for (i in 0..files.size) {
            val file = files[i]
            if (file.name.contains(".apk")) {
                println(file.name)
                currentApkPath = file.absolutePath
                break
            }
        }

        //拿到上传的apk file
        val apkFile = project.file(currentApkPath)

        //获取上传token及所需参数
        getToken(apiKey, updateDesc, buildInstallType.toString(), object : HttpCallback {
            override fun onSuccess(
                code: Int,
                params: JsonObject?,
                key: String?,
                endpoint: String?
            ) {
                if (code == SUCCESS_CODE && params != null) {
                    //成功后拿到上传所需参数开始上传apk
                    uploadFile(endpoint ?: "", key ?: "", params, apkFile)
                } else {//获取失败
                    throw IOException("上传失败: code = $code")
                }
            }

            override fun onError(code: Int, errorMsg: String?) {
                throw IOException("上传失败: code = $code")
            }
        })
    }

    /**
     *  获取上传的 token
     *  key     String     key 上传文件存储标识唯一 key
     *  endpoint   String 上传文件的 URL
     *  params Object 上传文件需要的参数,包含signature、x-cos-security-token、key
     */
    private fun getToken(
        apiKey: String,
        updateDesc: String,
        buildInstallType: String,
        httpCallback: HttpCallback
    ) {
        val multipartBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("_api_key", apiKey)
            .addFormDataPart("buildUpdateDescription", updateDesc)
            .addFormDataPart(
                "buildInstallType",
                buildInstallType
            )//(必填)应用安装方式,默认为1 公开安装 1:公开安装,2:密码安装,3:邀请安装
            .addFormDataPart("buildPassword", "111111")//安装密码  buildInstallType为2时需要此参数
            .addFormDataPart("buildType", "android")
            .build()

        val okHttpClient = OkHttpClient()
        val request = Request.Builder()
            .url(GET_TOKEN_URL)
            .post(multipartBody)
            .build()
        val response = okHttpClient.newCall(request).execute()
        if (!response.isSuccessful) {
            throw IOException("上传失败,http code is ${response.code}")
        }
        val responseBody = response.body?.string()
        val result = Gson().fromJson(responseBody, JsonObject::class.java)
        println("请求token结果为:$result")

        val code = result.get("code").asInt
        if (code != SUCCESS_CODE) {
            println(responseBody)
            val errorMsg = result.get("message").asString
            httpCallback.onError(code, errorMsg)
            throw IOException("上传失败,$errorMsg")
        }

        val paramsObject = result["data"].asJsonObject["params"].asJsonObject
        val key = result["data"].asJsonObject["key"].asString
        val endpoint = result["data"].asJsonObject["endpoint"].asString
        httpCallback.onSuccess(code, paramsObject, key, endpoint)
    }

    /**
     * 上传文件
     * @param url 上传apk的url
     */
    private fun uploadFile(
        url: String,
        key: String,
        params: JsonObject,
        apkFile: File
    ) {
        if (url.isEmpty()) return
        //创建组装上传所需参数
        val uploadBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("key", key)
            .addFormDataPart("signature", params["signature"].asString)
            .addFormDataPart(
                "x-cos-security-token",
                params["x-cos-security-token"].asString
            )
            .addFormDataPart(
                "file",
                apkFile.name,
                NewProgressesRequestBody(
                    apkFile.asRequestBody(),
                    NewConsoleProgressListener(
                        apkFile.name,
                        services[ProgressLoggerFactory::class.java]
                    )
                )
            ).build()

        uploadFileToServer(uploadBody, url, object : UploadFileToServiceCallback {
            override fun onUploadBack(code: Int, msg: String?) {
                if (code == FILE_UPLOAD_SUCCESSFUL) {
                    //蒲公英官方文档上注明了数据同步需要时间,所以检查接口可能会多次调用
                    println(msg)
                    val checkUploadResultUrl =
                        "$CHECK_UPLOAD_RESULT?_api_key=$apiKey&buildKey=$key"
                    checkUploadResult(checkUploadResultUrl)
                }
            }

            override fun onPackageSizeComputed(param1Long: Long) {
            }

            override fun onProgressChanged(param1Long: Float) {
            }

            override fun onUploadError(code: Int, error: String?) {

            }
        })
    }

    /**
     * 上传文件到蒲公英
     */
    private fun uploadFileToServer(
        uploadBody: MultipartBody,
        url: String,
        uploadFileToServiceCallback: UploadFileToServiceCallback
    ) {
        val request = Request.Builder()
            .url(url)
            .post(uploadBody)
            .build()
        val response = OkHttpClient().newCall(request).execute()
        if (!response.isSuccessful) {
            uploadFileToServiceCallback.onUploadError(response.code, "上传失败!")
            throw IOException("上传失败,http code is ${response.code}")
        }

        //如果上传成功:返回 http 状态码为 204 No Content; 如果上传失败:返回相应错误信息说明
        if (response.code == FILE_UPLOAD_SUCCESSFUL) {
            uploadFileToServiceCallback.onUploadBack(FILE_UPLOAD_SUCCESSFUL, "文件上传成功等待同步数据");
        } else {
            uploadFileToServiceCallback.onUploadError(response.code, "上传失败!");
        }

    }

    /**
     * 检查
     */
    private fun checkUploadResult(checkUploadResultUrl: String) {
        println("请耐心等待一会~")
        Sleep().doSleep(3000)

        val request = Request.Builder()
            .url(checkUploadResultUrl)
            .get()
            .build()

        val response = OkHttpClient().newCall(request).execute()
        if (!response.isSuccessful) {
            throw IOException("检查失败,http code is ${response.code}")
        }

        val responseBody = response.body?.string()
        val result = Gson().fromJson(responseBody, JsonObject::class.java)
        val code = result["code"].asInt
        val message = result["message"].asString
        when (code) {
            //需要过会再次请求,给蒲公英一个同步的时间
            CHECK_UPLOAD_ERROR_CODE_1027, CHECK_UPLOAD_ERROR_CODE_1026 -> {
                println(message)
                checkUploadResult(checkUploadResultUrl)
            }
            //获取结果成功
            SUCCESS_CODE -> {
                println("上传结果:$result")
                prepareNotify(result)
            }
            else -> {
                println("上传结果:$result")
            }
        }
    }

    /**
     * 准备通知内容
     */
    private fun prepareNotify(result: JsonObject) {
        val descFile = project.file(updateDescPath)
        val updateDesc = descFile.readText(Charset.forName("utf-8"))

        val dataObject = result["data"].asJsonObject
        val qrCodeUrl = dataObject["buildQRCodeURL"].asString
        val buildCreatedDate = dataObject["buildCreated"].asString
        val buildName = dataObject["buildName"].asString
        val buildVersion = dataObject["buildVersion"].asString
        val buildShortcutUrl = dataObject["buildShortcutUrl"].asString

        //通知内容
        val notifyMessage =
            "#### 应用名称:$buildName-Android\n" +
                    "#### 版本信息:$buildVersion\n" +
                    "#### 更新时间:$buildCreatedDate\n" +
                    "#### 更新内容:$updateDesc\n" +
                    "####  [点击下载 \uD83D\uDC48](https://www.pgyer.com/$buildShortcutUrl)\n" +
                    "#### 扫码下载 \uD83D\uDE0A\n" +
                    "![二维码]($qrCodeUrl)\n$updateDescSuffix"
        println("二维码地址:$qrCodeUrl")
        // 发送通知
        sendDingDingMessage(updateTitle, notifyMessage, dingDingRobotUrl, qrCodeUrl)
        // 删除重置描述文件
        descFile.delete()
        descFile.createNewFile()
        println("上传完成!")
    }

}

private class NewProgressesRequestBody(
    private val originRequestBody: RequestBody,
    private val progressListener: NewProgressListener? = null
) : RequestBody() {
    override fun contentType(): MediaType? = originRequestBody.contentType()
    override fun contentLength(): Long {
        return originRequestBody.contentLength()
    }

    override fun isDuplex(): Boolean {
        return originRequestBody.isDuplex()
    }

    override fun isOneShot(): Boolean {
        return originRequestBody.isOneShot()
    }

    override fun writeTo(sink: BufferedSink) {
        val bufferedSink = ProgressSink(sink).buffer()
        originRequestBody.writeTo(bufferedSink)
        bufferedSink.flush()
    }

    inner class ProgressSink(delegate: Sink) : ForwardingSink(delegate) {
        private var byteLength = 0L
        override fun write(source: Buffer, byteCount: Long) {
            super.write(source, byteCount)
            this.byteLength += byteCount
            progressListener?.update(byteLength, contentLength(), byteLength == contentLength())
        }
    }

}

private interface NewProgressListener {
    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
}


private class NewConsoleProgressListener(
    private val fileName: String,
    progressLoggerFactory: ProgressLoggerFactory
) : NewProgressListener {

    private var percent = 0L
    private val progressLogger = progressLoggerFactory.newOperation("上传apk文件")

    private var started = false
    override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
        val percent = bytesRead * 100 / contentLength
        if (this.percent == percent /*|| percent - this.precent < 20*/) {
            return
        }
        this.percent = percent

        if (!started) {
            started = true
            progressLogger.start("上传apk文件", "上传中")
        }
        progressLogger.progress("\r正在上传${fileName},已完成${this.percent}%")
        if (done) {
            progressLogger.completed("上传成功", false)
        }

    }

}

/**
 * 发送钉钉消息
 * https://open.dingtalk.com/document/group/custom-robot-access  推送消息钉钉文档
 */
private fun sendDingDingMessage(
    title: String,
    message: String,
    dingDingRobotUrl: String,
    buildQRCodeURL: String
) {

    val contentJob = JsonObject()
    contentJob.addProperty("title", "## $title")
    contentJob.addProperty("text", "$message   android \n@x,@x")
    contentJob.addProperty("messageUrl", "https://www.pgyer.com/xx")
    contentJob.addProperty("picUrl", buildQRCodeURL)

    val mobiles = JsonArray()
    mobiles.add(x)
    mobiles.add(x)
    val atJob = JsonObject()
    atJob.addProperty("isAtAll", false) //是否@所有人 如果不是则需要传具体@人的手机号
    atJob.add("atMobiles", mobiles)//at人的手机号  (在发送内容中也需要有艾特人的手机号)

    val body = JsonObject()
        .apply {
            addProperty("msgtype", "markdown") //发送消息的类型
            add("markdown", contentJob)
            add("at", atJob)
        }.toString()
    val request = Request
        .Builder()
        .url(dingDingRobotUrl)// 机器人地址
        .post(body.toRequestBody("application/json".toMediaType()))
        .build()
    val response = OkHttpClient().newCall(request).execute()
    if (!response.isSuccessful) {
        throw IOException("发送消息失败,http code:${response.code}")
    }
    val resultText = response.body!!.string()
    val result = Gson().fromJson(resultText, JsonObject::class.java)
    //println("sendDingDingMessage: $result")
    require(result.get("errcode").asInt == 0) { "发送消息失败,${resultText}" }
    println("发送钉钉通知成功")

}

/**
 * 检查git 是否已经和远程同步
 */
private fun requireGitUpToDate() {
    println("执行git status检查,防止发布时丢失commit")
    val result = CommandUtil.exec("git status")
    require(result.contains("up to date", true)) { "请确认本地的修改已经提交push到远程分支" }
}


/**
 * http 请求回调
 */
internal interface HttpCallback {
    fun onSuccess(code: Int, params: JsonObject?, key: String?, endpoint: String?)
    fun onError(code: Int, errorMsg: String?)
}


/**
 * 上传文件监听回调
 */
internal interface UploadFileToServiceCallback {
    //上传成功 或者 同步数据接口成功返回
    fun onUploadBack(code: Int, msg: String?)

    //上传文件大小
    fun onPackageSizeComputed(param1Long: Long)

    //上传文件进度
    fun onProgressChanged(param1Long: Float)

    //上传失败返回
    fun onUploadError(code: Int, error: String?)
}

最终代码调用就是在app模块下的build.gradle中:

def pgyerApiKey = "xxx"
def dingDingUrl = "xxx"
task uploadTest(type: PygerTask) {
    group = "upload"
    description = "上传debug apk文件到pgyer平台"
    dependsOn("assembleDebug")//dependsOn 这里是先执行打包的task,执行完后才会执行下面的内容
    mustRunAfter("clean")
    apiKey(pgyerApiKey)
    apkPath("build/outputs/apk/xxx/debug/app-debug.apk")
    updateTitle("app-debug测试包")
    dingDingRobotUrl(dingDingUrl)
    updateDescSuffix("附加内容")
}

最后谢谢大家耐心的看完,如果有帮助可以点赞转发哟😋~