Android 創(chuàng)建 Gradle Task 自動打包并上傳至蒲公英

前言

  Android 項目日常開發(fā)過程中,經常需要打包給到非開發(fā)人員驗收或調試,例如測試階段,就要經常基于測試服務器地址,打包安裝包,給到組內測試人員進行測試,并且 BUG 修復完成之后也需要再次打包給到測試人員回測。
  為了減免機械性的重復步驟,為項目配置不同的 渠道(Product Flavors),不同渠道對應不同的服務器地址,并且為每一個渠道創(chuàng)建一個 Gradle Task 執(zhí)行打包并上傳至蒲公英的操作,同時在蒲公英中配置 Webhook ,最終可實現:執(zhí)行對應 渠道(Product Flavors)Gradle Task,即可自動打包并上傳至蒲公英,并將包更新信息同步至企業(yè)微信、釘釘、飛書等工作群組,使得包更新流程可視化,并簡化了開發(fā)和測試聯(lián)調流程。

實現步驟

1.創(chuàng)建 pgyer-upload.gradle 文件

  每個渠道的 Task 執(zhí)行內容一致:打包并記錄更新信息后上傳至蒲公英 ,所以抽取公共內容(方法)創(chuàng)建如下 pgyer-upload.gradle 文件

import groovy.json.JsonSlurper

import 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.SimpleDateFormat

ext.uploadApk = this.&uploadApk

/**
 * Sept 1 創(chuàng)建執(zhí)行任務
 */

/**
 * 蒲公英 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.name
    println "*************** Upload Init ***************"
    //渠道信息
    String buildFlavorName = flavorName
    if (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.versionName
    def 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#fastUploadApp
    List<KeyValue> cosTokenParams = new ArrayList<>()
    //API KEY
    cosTokenParams.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#fastUploadApp
    HttpResponse<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 body
    List<byte[]> byteArrays = new ArrayList<>()
    // Separator with boundary
    byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(StandardCharsets.UTF_8)
    // Iterating over data parts
    for (i in 0..<list.size()) {
        // Opening boundary
        byteArrays.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=value
        if (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 key
    String value
    boolean isFile

    KeyValue(String key, String value) {
        this(key, value, false)
    }

    KeyValue(String key, String value, boolean isFile) {
        this.key = key
        this.value = value
        this.isFile = isFile
    }

    @Override
    String toString() {
        return "{key:" + key + ", value:" + value + ", isFile:$isFile}"
    }
}

  該代碼主要執(zhí)行 APK 上傳至蒲公英的操作,并上傳指定更新內容:

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

2.引用自定義 Gradle 文件

  在項目的 build.gradle 文件最外層執(zhí)行

apply from: "pgyer-upload.gradle"

3.配置渠道(非必須)

  app modulebuild.gradle 文件 android 層內執(zhí)行

    /**
     * Sept 3 配置渠道 (非必須)
     */
    flavorDimensions "channel"
    productFlavors {
        // 生產環(huán)境
        produce {
            buildConfigField "String", "HOST", "\"${HOST}\""
        }
        // 預生產環(huán)境
        beta {
            buildConfigField "String", "HOST", "\"${HOST_BETA}\""
        }
        // 開發(fā)環(huán)境
        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 均為自定義的渠道名,可根據自身業(yè)務需求進行增刪修改,其中 HOST 為自定義的變量名,不同渠道引用值不一樣(示例中的值來源配置在項目的 gradle.properties 文件中,如下所示),項目編譯后,在代碼中即可通過 BuildConfig.FLAVORBuildConfig.HOST 獲取當前編譯環(huán)境的 渠道名自定義變量 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.創(chuàng)建打包 Task

  同樣在 app modulebuild.gradle 文件 android 層內執(zhí)行

    /**
     * Sept 4 創(chuàng)建打包 Task
     * 遍歷所有可執(zhí)行的 variants 創(chuàng)建對應的打包 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 ->
                        // 執(zhí)行腳本任務
                        uploadApk(output.outputFile, taskSuffix)
                    }
                }
            }
        }
    }

5.執(zhí)行 Gradle Task

  按上述步驟操作之后,先執(zhí)行 Sync Project with Gradle Files 生成不同渠道對應的打包 Task
  然后可通過以下兩種方式進行執(zhí)行打包并上傳的腳本任務

  • 直接執(zhí)行 Task : Tasks/build/pushApkDevRelease

    直接執(zhí)行 Task 步驟 1

    直接執(zhí)行 Task 步驟 2

  • 通過 Gradle 命令執(zhí)行 Task : gradle pgyerUploadDevRelease

    通過 Gradle 命令執(zhí)行 Task

5.1執(zhí)行效果

Gradle Task 執(zhí)行完畢

企業(yè)微信群更新信息示例

6.補充說明

6.1 關于變種(variants)

  在 Android 應用程序構建過程中,變種(variants)是指基于不同構建配置或渠道進行構建的應用程序版本。
  Android Gradle 插件使用變種來生成不同版本的應用程序,以滿足不同的需求,如不同的構建類型、不同的渠道或不同的產品變體等。
  每個變種具有自己的構建配置和特定的屬性設置,例如包名、應用圖標、應用名稱等。通過創(chuàng)建不同的變種,可以實現以下目標:

1.構建類型(Build Types):構建類型定義了不同的構建環(huán)境和配置,例如調試版(Debug)和發(fā)布版(Release)。每個構建類型可以具有自己的代碼、資源、簽名證書、編譯標志等。
2.渠道(Product Flavors):渠道是為了滿足不同目標市場或用戶群體的需求而定義的版本變體。通過渠道,可以為不同的渠道定制應用程序的內容,如應用程序圖標、名稱、啟動畫面、配置文件等。
3.變體(Build Variants):變體是構建類型和渠道的組合,表示一個具體的應用程序版本。每個變體都有其自己的構建輸出,如 APK 文件或可安裝的應用程序包。

  通過定義和配置不同的變種,開發(fā)人員可以輕松地構建適用于不同需求的不同版本的應用程序,以便進行測試、發(fā)布和分發(fā)。

6.2 上傳 APK 文件失敗

上傳文件至蒲公英失敗

  該問題產生的原因是當前項目 JDK 版本 > 11 ,而當前 Gradle Task 內執(zhí)行的文件 POST 操作是自行封裝的請求參數,該封裝在 JDK 版本 > 11 的情況下,無法被服務器正常識別,猜測原因是自定義包裝請求參數過程中出現了偏差導致。
最后的解決方案是:

1.降低項目 JDK 版本為 11 或以下即可解決該問題(大部分情況下需要同步修改項目的 Gradle 和 Gradle Plugin 版本,改動較大,不建議
2.改用自定義 Gradle 插件形式創(chuàng)建 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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容