Skip to content

Commit 90c89b3

Browse files
committed
update
1 parent c16e2ea commit 90c89b3

14 files changed

Lines changed: 563 additions & 48 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ android {
2828
applicationId = "com.m0h31h31.bamburfidreader"
2929
minSdk = 28
3030
targetSdk = 36
31-
versionCode = 309
32-
versionName = "3.0.9"
31+
versionCode = 310
32+
versionName = "3.1.0"
3333
val eventApiKey = localProperties.getProperty("EVENT_API_KEY", "")
3434
buildConfigField("String", "EVENT_API_KEY", "\"${escapeBuildConfigString(eventApiKey)}\"")
3535
val tagPackageKey = localProperties.getProperty("TAG_PACKAGE_KEY", "")

app/src/main/assets/AppConfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "3.0.9",
2+
"version": "3.1.0",
33
"boostLink": {
44
"type": "scheme",
55
"value": "bambulab://bbl/design/model/detail?design_id=2020787&appSharePlatform=copy"

app/src/main/java/com/m0h31h31/bamburfidreader/MainActivity.kt

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,16 @@ class MainActivity : ComponentActivity() {
13981398
autoShareTag = enabled
13991399
uiPrefs.edit().putBoolean(KEY_AUTO_SHARE_TAG, enabled).apply()
14001400
},
1401+
onCheckDownloadPermission = {
1402+
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
1403+
checkTagDownloadPermission()
1404+
}
1405+
},
1406+
onDownloadTagPackage = { brand, onProgress, onImportStatus ->
1407+
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
1408+
downloadAndImportTagPackage(brand, onProgress, onImportStatus)
1409+
}
1410+
},
14011411
hideCopiedTags = hideCopiedTags,
14021412
onHideCopiedTagsChange = { enabled ->
14031413
hideCopiedTags = enabled
@@ -1483,9 +1493,9 @@ class MainActivity : ComponentActivity() {
14831493
cModifyInProgress = pendingCModifyItem != null,
14841494
formatInProgress = pendingClearFuid,
14851495
anomalyUids = anomalyUids,
1486-
onReportAnomaly = { trayUid, cardUid ->
1496+
onReportAnomaly = { cardUid ->
14871497
lifecycleScope.launch(Dispatchers.IO) {
1488-
com.m0h31h31.bamburfidreader.utils.AnalyticsReporter.reportAnomaly(applicationContext, trayUid, cardUid)
1498+
com.m0h31h31.bamburfidreader.utils.AnalyticsReporter.reportAnomaly(applicationContext, cardUid)
14891499
}
14901500
},
14911501
onTagScreenEnter = {
@@ -2026,7 +2036,7 @@ class MainActivity : ComponentActivity() {
20262036
.toMutableSet()
20272037

20282038
val zipEntries = extractZipEntries(uri)
2029-
if (zipEntries.isEmpty() && !cacheDir.listFiles()?.any { it.name.startsWith("import_") } ?: false) {
2039+
if (zipEntries.isEmpty() && !(cacheDir.listFiles()?.any { it.name.startsWith("import_") } ?: false)) {
20302040
return "读取标签包失败(文件损坏或密码错误)"
20312041
}
20322042
for ((entryName, bytes) in zipEntries) {
@@ -2088,6 +2098,106 @@ class MainActivity : ComponentActivity() {
20882098
}
20892099
}
20902100

2101+
// ── 在线下载共享标签包 ──────────────────────────────────────────────────────
2102+
2103+
// ── 在线下载权限检查 ──────────────────────────────────────────────────────
2104+
2105+
/** 检查当前设备是否有资格下载标签包。IO 线程调用。返回 null 表示允许,非 null 为拒绝原因。 */
2106+
private fun checkTagDownloadPermission(): String? {
2107+
val installId = com.m0h31h31.bamburfidreader.utils.AnalyticsReporter.getInstallId(this)
2108+
val endpoint = com.m0h31h31.bamburfidreader.utils.ConfigManager.getTagCanDownloadEndpoint(this)
2109+
return try {
2110+
val url = java.net.URL("$endpoint?install_id=${java.net.URLEncoder.encode(installId, "UTF-8")}")
2111+
val conn = url.openConnection() as java.net.HttpURLConnection
2112+
conn.connectTimeout = 10000
2113+
conn.readTimeout = 10000
2114+
val code = conn.responseCode
2115+
val body = if (code in 200..299)
2116+
conn.inputStream.use { it.readBytes() }.toString(Charsets.UTF_8)
2117+
else
2118+
conn.errorStream?.use { it.readBytes() }?.toString(Charsets.UTF_8) ?: ""
2119+
conn.disconnect()
2120+
val json = org.json.JSONObject(body)
2121+
if (json.optBoolean("allowed", false)) null
2122+
else json.optString("reason", getString(R.string.download_tag_package_failed))
2123+
} catch (e: Exception) {
2124+
logDebug("checkTagDownloadPermission error: ${e.message}")
2125+
getString(R.string.download_tag_package_failed)
2126+
}
2127+
}
2128+
2129+
// ── 在线下载并导入标签包 ──────────────────────────────────────────────────
2130+
2131+
/**
2132+
* 下载指定品牌的标签包并导入(suspend)。
2133+
* onProgress(0..100) 在 IO 线程回调,调用方用 withContext(Main) 更新 UI。
2134+
*/
2135+
private suspend fun downloadAndImportTagPackage(
2136+
brand: String,
2137+
onProgress: (Int) -> Unit,
2138+
onImportStatus: (String) -> Unit
2139+
): String {
2140+
val installId = com.m0h31h31.bamburfidreader.utils.AnalyticsReporter.getInstallId(this)
2141+
val endpoint = com.m0h31h31.bamburfidreader.utils.ConfigManager.getTagDownloadEndpoint(this)
2142+
val payload = org.json.JSONObject().apply {
2143+
put("install_id", installId)
2144+
put("brand", brand)
2145+
}
2146+
val tmp = java.io.File(cacheDir, "dl_${brand}_${System.currentTimeMillis()}.zip")
2147+
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
2148+
return try {
2149+
val result = com.m0h31h31.bamburfidreader.utils.NetworkUtils.postJsonDownloadToFile(
2150+
endpoint, payload, tmp, onProgress = onProgress
2151+
) ?: return getString(R.string.download_tag_package_failed)
2152+
2153+
val (code, errBody) = result
2154+
if (code !in 200..299) {
2155+
val detail = try {
2156+
org.json.JSONObject(errBody!!.toString(Charsets.UTF_8)).optString("detail", "")
2157+
} catch (_: Exception) { "" }
2158+
return if (detail.isNotBlank()) detail
2159+
else getString(R.string.download_tag_package_failed)
2160+
}
2161+
importTagPackageFromZipFile(tmp, snapmaker = brand == "snapmaker") { cur, total ->
2162+
mainHandler.post { onImportStatus("正在导入 ($cur/$total)…") }
2163+
}
2164+
} finally {
2165+
tmp.delete()
2166+
}
2167+
}
2168+
2169+
/**
2170+
* 直接从 zip 文件导入标签包(不经过 ContentResolver/FileProvider),
2171+
* 复用现有解析逻辑,适用于下载后的临时文件。
2172+
*/
2173+
private fun importTagPackageFromZipFile(
2174+
zipFile: java.io.File,
2175+
snapmaker: Boolean,
2176+
onProgress: ((cur: Int, total: Int) -> Unit)? = null
2177+
): String {
2178+
val dbHelper = filamentDbHelper ?: return "数据库不可用"
2179+
val db = dbHelper.writableDatabase
2180+
return try {
2181+
// 用 zip4j 直接打开文件(与 extractZipEntries 相同逻辑,跳过 URI 拷贝)
2182+
val zip4j = net.lingala.zip4j.ZipFile(zipFile)
2183+
if (zip4j.isEncrypted) {
2184+
zip4j.setPassword(computeTagZipPassword().toCharArray())
2185+
}
2186+
val entries = mutableListOf<Pair<String, ByteArray>>()
2187+
for (header in zip4j.fileHeaders) {
2188+
if (!header.isDirectory) {
2189+
val bytes = zip4j.getInputStream(header).use { it.readBytes() }
2190+
entries.add(Pair(header.fileName, bytes))
2191+
}
2192+
}
2193+
if (snapmaker) processSnapmakerZipEntries(entries, db, dbHelper, onProgress)
2194+
else processBambuZipEntries(entries, db, dbHelper, onProgress)
2195+
} catch (e: Exception) {
2196+
logDebug("importTagPackageFromZipFile error: ${e.message}")
2197+
"导入失败: ${e.message.orEmpty()}"
2198+
}
2199+
}
2200+
20912201
// ── 快造标签包导入 ──────────────────────────────────────────────────────────
20922202

20932203
private fun openSnapmakerTagPackagePicker() {
@@ -2158,6 +2268,117 @@ class MainActivity : ComponentActivity() {
21582268
}
21592269
}
21602270

2271+
// ── zip entries 通用处理(供 URI 导入和文件直接导入共用)───────────────────
2272+
2273+
private fun processBambuZipEntries(
2274+
entries: List<Pair<String, ByteArray>>,
2275+
db: android.database.sqlite.SQLiteDatabase,
2276+
dbHelper: FilamentDbHelper,
2277+
onProgress: ((cur: Int, total: Int) -> Unit)? = null
2278+
): String {
2279+
val helper = dbHelper
2280+
var extractedCount = 0; var skippedCount = 0
2281+
var invalidCount = 0; var overwrittenCount = 0
2282+
val txtEntries = entries.filter { it.first.lowercase(Locale.US).endsWith(".txt") }
2283+
val total = txtEntries.size
2284+
var processed = 0
2285+
val existingRows = helper.getAllShareTagRows(db)
2286+
val existingFileUidSet = existingRows.map { it.fileUid.uppercase(Locale.US) }.toMutableSet()
2287+
val existingTrayUidSet = existingRows
2288+
.mapNotNull { it.trayUid?.uppercase(Locale.US)?.ifBlank { null } }.toMutableSet()
2289+
for ((entryName, bytes) in txtEntries) {
2290+
val incomingUid = File(entryName).nameWithoutExtension.uppercase(Locale.US)
2291+
val alreadyExists = incomingUid.isNotBlank() && existingFileUidSet.contains(incomingUid)
2292+
if (alreadyExists && !forceOverwriteImport) { skippedCount++; processed++; onProgress?.invoke(processed, total); continue }
2293+
val content = String(bytes, Charsets.UTF_8)
2294+
val rawBlocks = parseHexTagFileStrict(content)
2295+
if (rawBlocks == null) { invalidCount++; processed++; onProgress?.invoke(processed, total); continue }
2296+
if (!isValidBambuTag(rawBlocks)) { invalidCount++; processed++; onProgress?.invoke(processed, total); continue }
2297+
val preview = NfcTagProcessor.parseForPreview(rawBlocks, filamentDbHelper) { }
2298+
val trayUid = preview.trayUidHex.trim()
2299+
if (trayUid.isNotBlank() && existingTrayUidSet.contains(trayUid.uppercase())) {
2300+
skippedCount++; processed++; onProgress?.invoke(processed, total); continue
2301+
}
2302+
if (alreadyExists && forceOverwriteImport && incomingUid.isNotBlank()) {
2303+
helper.deleteShareTagByFileUid(db, incomingUid)
2304+
}
2305+
val normalized = content.trim().lines()
2306+
.map { it.trim() }.filter { it.isNotBlank() }.take(64).joinToString("\n")
2307+
val productionDate = extractProductionDate(rawBlocks)
2308+
helper.insertShareTag(
2309+
db, fileUid = incomingUid, trayUid = trayUid.ifBlank { null },
2310+
materialType = preview.displayData.type.ifBlank { null },
2311+
colorUid = preview.displayData.colorCode.ifBlank { null },
2312+
colorName = preview.displayData.colorName.ifBlank { null },
2313+
colorType = preview.displayData.colorType.ifBlank { null },
2314+
colorValues = preview.displayData.colorValues.joinToString(",").ifBlank { null },
2315+
rawData = normalized, productionDate = productionDate
2316+
)
2317+
extractedCount++
2318+
if (alreadyExists && forceOverwriteImport) overwrittenCount++
2319+
if (incomingUid.isNotBlank()) existingFileUidSet.add(incomingUid)
2320+
if (trayUid.isNotBlank()) existingTrayUidSet.add(trayUid.uppercase())
2321+
processed++
2322+
onProgress?.invoke(processed, total)
2323+
}
2324+
return when {
2325+
extractedCount == 0 && skippedCount == 0 && invalidCount == 0 ->
2326+
"导入完成,但压缩包内未发现标签数据"
2327+
extractedCount == 0 ->
2328+
"导入完成:格式无效 $invalidCount 个,重复跳过 $skippedCount"
2329+
forceOverwriteImport ->
2330+
"Bambu 标签包导入完成:新增 $extractedCount 个(覆盖 $overwrittenCount 个),无效 $invalidCount 个,跳过 $skippedCount"
2331+
else ->
2332+
"Bambu 标签包导入完成:新增 $extractedCount 个,无效 $invalidCount 个,跳过 $skippedCount"
2333+
}
2334+
}
2335+
2336+
private fun processSnapmakerZipEntries(
2337+
entries: List<Pair<String, ByteArray>>,
2338+
db: android.database.sqlite.SQLiteDatabase,
2339+
dbHelper: FilamentDbHelper,
2340+
onProgress: ((cur: Int, total: Int) -> Unit)? = null
2341+
): String {
2342+
val helper = dbHelper
2343+
var extractedCount = 0; var skippedCount = 0; var invalidCount = 0
2344+
val txtEntries = entries.filter { it.first.lowercase(Locale.US).endsWith(".txt") }
2345+
val total = txtEntries.size
2346+
var processed = 0
2347+
val existingUidSet = helper.getAllSnapmakerShareTagUids(db)
2348+
.map { it.uppercase(Locale.US) }.toMutableSet()
2349+
for ((entryName, bytes) in txtEntries) {
2350+
val incomingUid = File(entryName).nameWithoutExtension.uppercase(Locale.US)
2351+
if (incomingUid.isNotBlank() && existingUidSet.contains(incomingUid)) {
2352+
skippedCount++; processed++; onProgress?.invoke(processed, total); continue
2353+
}
2354+
val content = String(bytes, Charsets.UTF_8)
2355+
val rawBlocks = parseHexTagFileStrict(content)
2356+
if (rawBlocks == null) { invalidCount++; processed++; onProgress?.invoke(processed, total); continue }
2357+
if (!isValidSnapmakerTag(rawBlocks)) { invalidCount++; processed++; onProgress?.invoke(processed, total); continue }
2358+
val fields = parseSnapmakerShareFields(rawBlocks)
2359+
val normalized = content.trim().lines()
2360+
.map { it.trim() }.filter { it.isNotBlank() }.take(64).joinToString("\n")
2361+
helper.insertSnapmakerShareTag(
2362+
db, uid = incomingUid, vendor = fields.vendor,
2363+
manufacturer = fields.manufacturer, mainType = fields.mainType,
2364+
diameter = fields.diameter, weight = fields.weight,
2365+
rgb1 = fields.rgb1, mfDate = fields.mfDate, rawData = normalized
2366+
)
2367+
extractedCount++
2368+
existingUidSet.add(incomingUid)
2369+
processed++
2370+
onProgress?.invoke(processed, total)
2371+
}
2372+
return when {
2373+
extractedCount == 0 && skippedCount == 0 && invalidCount == 0 ->
2374+
"导入完成,但压缩包内未发现标签数据"
2375+
extractedCount == 0 ->
2376+
"导入完成:格式无效 $invalidCount 个,重复跳过 $skippedCount"
2377+
else ->
2378+
"Snapmaker 标签包导入完成:新增 $extractedCount 个,无效 $invalidCount 个,跳过 $skippedCount"
2379+
}
2380+
}
2381+
21612382
private data class SnapmakerShareFields(
21622383
val vendor: String,
21632384
val manufacturer: String,
@@ -5878,7 +6099,7 @@ class FilamentDbHelper(context: Context) :
58786099
val now = System.currentTimeMillis()
58796100
for ((uid, count) in uids) {
58806101
val cv = ContentValues()
5881-
cv.put("uid", uid.lowercase().trim())
6102+
cv.put("uid", uid.uppercase().trim())
58826103
cv.put("report_count", count)
58836104
cv.put("synced_at", now)
58846105
db.insertWithOnConflict(ANOMALY_UIDS_TABLE, null, cv, SQLiteDatabase.CONFLICT_REPLACE)

app/src/main/java/com/m0h31h31/bamburfidreader/ui/navigation/AppNavigation.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ fun AppNavigation(
117117
onAutoDetectBrandChange: (Boolean) -> Unit = {},
118118
autoShareTag: Boolean = true,
119119
onAutoShareTagChange: (Boolean) -> Unit = {},
120+
onCheckDownloadPermission: suspend () -> String? = { null },
121+
onDownloadTagPackage: suspend (brand: String, onProgress: (Int) -> Unit, onImportStatus: (String) -> Unit) -> String = { _, _, _ -> "" },
120122
hideCopiedTags: Boolean = true,
121123
onHideCopiedTagsChange: (Boolean) -> Unit = {},
122124
dualTagMode: Boolean = false,
@@ -183,7 +185,7 @@ fun AppNavigation(
183185
readerSnapmakerTagData: SnapmakerTagData? = null,
184186
readerBrandStatus: String = "",
185187
anomalyUids: Map<String, Int> = emptyMap(),
186-
onReportAnomaly: (trayUid: String, cardUid: String) -> Unit = { _, _ -> },
188+
onReportAnomaly: (cardUid: String) -> Unit = { _ -> },
187189
pendingUpdateInfo: com.m0h31h31.bamburfidreader.utils.UpdateInfo? = null,
188190
isDownloadingUpdate: Boolean = false,
189191
onStartUpdate: (com.m0h31h31.bamburfidreader.utils.UpdateInfo) -> Unit = {},
@@ -516,6 +518,8 @@ fun AppNavigation(
516518
onAutoDetectBrandChange = onAutoDetectBrandChange,
517519
autoShareTag = autoShareTag,
518520
onAutoShareTagChange = onAutoShareTagChange,
521+
onCheckDownloadPermission = onCheckDownloadPermission,
522+
onDownloadTagPackage = onDownloadTagPackage,
519523
hideCopiedTags = hideCopiedTags,
520524
onHideCopiedTagsChange = onHideCopiedTagsChange,
521525
dualTagMode = dualTagMode,

0 commit comments

Comments
 (0)