Android 開發 | 讓 Gradle 自動為 AAR、AAB、APK 設定客製化檔名

每次打包 AAB 或 APK,Android Studio 預設都會產出像 app-release.aab 或 app-debug.apk 這類的檔案名稱。這種預設命名在使用 Firebase App Distribution 或 Google Play 內部測試流程時,雖然不太會造成困擾,但如果你需要將 APK 或 AAB 檔案直接提供給 PM、QA 或外部合作夥伴安裝測試,那麼能夠一眼看出檔案版本與類型,會讓流程更順暢也更不容易搞混。 本文將示範如何透過 Gradle 設定,讓你在打包時自動加上版本號、Build Type 等資訊,生成便於辨識的輸出檔名。 設定方式 你可以根據不同的產出檔案類型(AAB、APK、AAR),在 app module 或 library module 的 build.gradle 中加入以下設定。 AAB:變更 App Bundle 檔名 在 build.gradle(app module)中 android 區塊內加入以下設定: 1 2 3 4 android { // 其他設定省略... setProperty("archivesBaseName", "taiwanNo1") } BTW: setProperty(key, value) 在 groovy 與 kts 都是通用的。 🚨 注意: 這個設定只會影響輸出檔案名稱的前綴,並不會完整覆蓋檔名結構。例如: 1 2 原始檔名:app-release.aab 變更後:taiwanNo1-release.aab 💡 為什麼使用 archivesBaseName ? 因為目前 Gradle 尚未提供官方 API 可以直接命名 AAB 檔。 archivesBaseName 是 Gradle 用來設定各類 archives 類檔案輸出名稱的共通屬性。 所謂的 archives 檔案包含: APK、AAR、JAR、ZIP 等。 ...

Published on April 30, 2025 · Updated on May 14, 2025 · 2 分鐘 · 283 字 · Daniel Huang

Android 開發 | 讓 Gradle 自動在版本名稱後加上 Build 時間

在開發 App 的過程中,常常需要產出不同版本的 APK,給團隊成員或測試人員驗證功能。 這時候,「快速辨識版本」就變成一件非常重要的事了。 最常見、也最直接的方法,就是利用 Gradle 的 versionNameSuffix,在 App 的版本名稱後面自動加上一些額外資訊。 以我們團隊的習慣來說,我們會直接加上Build 當下的時間作為版本流水號,這樣每個 APK 都能有獨一無二的識別。但如果每次出新版本,都還要手動去改 Gradle 設定,實在是有點麻煩。 下面就分享一個簡單的做法,讓 Gradle 在每次 Build 的時候,自動把「打包時間」加到版本名稱後面。 程式碼範例 Groovy 寫法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 產生日期時間字串 def getDate() { def date = new Date() def formattedDate = date.format('yyyyMMddHHmmss') return formattedDate } // 在buildType中就可使用 debug { applicationIdSuffix '.debug' // 設定版本名稱後綴 versionNameSuffix '-dev' + ' (' + getDate() + ')' // 其他設定... } Kotlin DSL (KTS) 寫法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 android { // 其他設定... buildTypes { // 略... debug { isMinifyEnabled = false applicationIdSuffix = ".dev" // 設定版本名稱後綴 versionNameSuffix = "-dev (${getDate()})" } } } /** * 取得目前的日期時間字串 * * @return */ fun getDate(): String { val date = Calendar.getInstance().time val formatter = SimpleDateFormat("yyyyMMdd-HHmm", Locale.getDefault()) return formatter.format(date) } 這樣設好之後,每次打包 Debug 版 APK,都會自動帶上像 -dev(20250428-1430) 這種字串, 讓相關人員清楚知道這份檔案是什麼時候打包的。 ...

Published on April 28, 2025 · 1 分鐘 · 168 字 · Daniel Huang

使用 Maps URLs 開啟 Google Maps 的路徑規劃功能

前言 工作上負責開發維護的一款 App,最近被提了一個新的需求。 客戶希望原有的 開啟 Google Maps 導航 這項功能,能夠除了起訖點外,還能在起訖點間加上停靠點。 原本以為這是件加個經緯度之類的參數,就能完成的事情。閱讀官方文件才發現,目前使用的 Google Maps Intents for Android 並不支援停靠點。 如果要有停靠點,就只能改用支援跨平台的 Maps URLs,且只有路徑規劃,沒有導航。 💡 路徑規劃跟導航差在哪裡? 路徑規劃只會顯示起訖點與各停靠點間的路線。 導航則是會以目前定位直接開啟到目的地的路線指引。 也就是說,路徑規劃比較像是 App 內執行導航的前一個步驟,使用者必須自己按下開始導航。 什麼是 Maps URLs Maps URLs 是用來開啟 Google Maps App/Web 的跨平台 URL。這裡的跨平台包含 Android、 iOS 、 Web ,三大平台都可以支援。 目前已支援開啟的 App 功能有: Search 搜尋 Directions 路徑規劃 Display a Map 顯示地圖 Display a Street View panorama 街景服務 今天這篇文章只會提到第二個的路徑規劃,其他三種功能我之後會再另開文章補上~ 路徑規劃 顧名思義,就是開啟 Google Maps 的路徑規劃功能。 呼叫之後會在 Google Maps 上開啟點到點之間的路徑,並顯示距離和時間。 路徑規劃示意圖 路徑規劃示意圖 (使用定位作為起點) 導航示意圖 以上面三張圖來說,左側與中間的圖是路徑規劃,右側的圖是導航功能。 ...

Published on February 19, 2025 · Updated on May 15, 2025 · 4 分鐘 · 653 字 · Daniel Huang

Android EncryptedSharedPreference 系統升級後無法解密的錯誤與解決方案

一、問題描述 1 2 使用者的手機為 Sony 品牌,首次安裝 App 時,系統版本為 Android 14,能夠正常使用。 近期系統升級為 Android 15 後,開啟 App 即閃退。即使移除後重新安裝,仍然無法使用。 二、查看錯誤事件 查找 Crashlytics 上的錯誤事件,全部事件只有兩筆,其中只有 BasePreference.pref_delegate$lambda$0 這筆看起來最為相關。 查看錯誤訊息的內容,確認是在實例化 EncryptedSharedPreference 時出了問題。 程式裡實例的方式是參考 Android 開發 | 加密版的 SharedPreference - EncryptedSharedPreferences。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Fatal Exception: com.google.crypto.tink.shaded.protobuf.D: Protocol message contained an invalid tag (zero). at com.google.crypto.tink.shaded.protobuf.InvalidProtocolBufferException.<init>(InvalidProtocolBufferException.java:100) at com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:100) at com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:100) at com.google.crypto.tink.proto.Keyset.parseFrom(Keyset.java:3) at com.google.crypto.tink.integration.android.SharedPrefKeysetReader.read(SharedPrefKeysetReader.java:67) at com.google.crypto.tink.CleartextKeysetHandle.read(CleartextKeysetHandle.java:67) at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.read(AndroidKeysetManager.java:67) at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readOrGenerateNewKeyset(AndroidKeysetManager.java:67) at com.google.firebase.messaging.GmsRpc.i(GmsRpc.java:17) at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:180) at // ... 錯誤訊息 Fatal Exception: com.google.crypto.tink.shaded.protobuf.D: Protocol message contained an invalid tag (zero). InvalidProtocalBufferException 代表 解密資料時解析 protobuf 格式失敗,通常是因為解密出的內容是無效的(可能是空的或亂碼)。 ...

Published on February 11, 2025 · 4 分鐘 · 642 字 · Daniel Huang

如何在 Android 上取得準確且不被用戶竄改的時間

一、前言 前陣子專案有一項需求,是希望 App 能夠取得準確且不會被使用者竄改的時間。 研究了一下 StackOverflow: How do I get the most accurate time with Android? 上面的討論,決定整理成一篇完整的筆記,也許能拯救到未來的自己? XD 1-1 在 Android 中取得當下時間 大家應該都會直覺地想到以下幾個方式: System.currentTimeMillis()。 Calendar.getInstance() Instant.now() 但這幾個方式,拿到的時間都是所謂的系統時間,只要使用者到系統設定中手動調整,就可以輕易地修改這幾個方式回傳的時間。 二、Android SDK 的 SystemClock Android 的 SystemClock 類別中,有提供以下兩個方法可以拿到準確的時間。 2-1 SystemClock.currentGnssTimeClock() 時間跟稍後會提到的LocationManager.GPS_PROVIDER拿到的時間一樣。 官方文件 SystemClock.currentGnssTimeClock() Returns a Clock that starts at January 1, 1970 00:00:00.0 UTC, synchronized using the device’s location provider. API 29 以上 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import android.os.Build import android.os.SystemClock import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter fun getGpsApiDateTime(): String { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { try { val zoneClock = SystemClock.currentGnssTimeClock().withZone(ZoneId.of("Asia/Taipei")) ZonedDateTime.now(zoneClock).format(dateTimeFormatter) } catch (e: Exception) { "GNSS NOT AVAILABLE" } } else { "SYSTEM NOT AVAILABLE" } } :::danger 📢 注意! 在部分裝置上,GNSS API 回傳的時間有可能是錯誤的… 細節請看 問題-GNSS-API-與-Location-Provider-所拿到的時間可能是錯誤的。 ::: ...

Published on August 26, 2024 · 6 分鐘 · 1093 字 · Daniel Huang