From 46a9add5cbb6ffea4fdeb115be8602b1aa03f807 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 22 Oct 2026 23:47:13 +0800 Subject: [PATCH] =?UTF-8?q?build+fix:=20debug=20=E5=8C=85=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E4=B8=BA=20.dev=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE=E7=BC=93=E5=AD=98=E6=B0=B8=E8=BF=9C?= =?UTF-8?q?=E6=9F=A5=E4=B8=8D=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 包名: - debug 使用 com.lxh.qingting_player.dev(应用名"清听 Dev"),可与 release 并存 - release 保持 com.lxh.qingting_player - Android 包名每一段必须以字母开头,因此用 .dev 而非 _dev - AGP 9 需显式开启 buildFeatures.resValues;移除 manifest 中已废弃的 package 属性 元数据(两个独立原因叠加): 1. song_key 之前用 String.hashCode / 本地临时文件 mtime,都不稳定; 2. songs.remote_path 存的是完整 URL,而所有查询都用相对路径(song.id), getSongByPath 永远 miss,UI 只能回退到文件名。 - 新增 utils/song_key.dart:songKeyFor(url,size)=md5(url|size),跨进程稳定 - metadata_service: remote_path 改存 fileId(相对路径),并改用稳定 song_key - audio_service / file_provider: 同步使用稳定键 - 数据库升级 v5:清空 songs / metadata_cache(可再生缓存;歌单与收藏按路径存储,不受影响) - device-check.ps1: 支持 -Package,默认校验 dev 包,启动组件改为向 PackageManager 解析 验证:两次独立 dart 进程 song_key 输出一致;真机 debug/release 并存安装,单实例自检通过。 --- android/app/build.gradle.kts | 16 ++++++++++++++-- android/app/src/main/AndroidManifest.xml | 6 +++--- lib/database/song_database.dart | 10 +++++++++- lib/metadata/file_provider.dart | 3 ++- lib/metadata/metadata_service.dart | 14 ++++++++------ lib/services/audio_service.dart | 15 +++++++-------- lib/utils/song_key.dart | 22 ++++++++++++++++++++++ tools/device-check.ps1 | 16 ++++++++++++---- 8 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 lib/utils/song_key.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c5384a7..1476e29 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -42,10 +42,22 @@ android { } } + // AGP 9 默认关闭 resValue 生成,这里显式打开(debug 需要 "清听 Dev" 的应用名) + buildFeatures { + resValues = true + } + buildTypes { + debug { + // 测试包与正式包并存安装:com.lxh.qingting_player.dev + // 说明:Android 包名每一段必须以字母开头,所以没法直接写 "_dev", + // 这里用 ".dev" 后缀,等价于给 debug 打上 dev 标记。 + applicationIdSuffix = ".dev" + resValue("string", "app_name", "清听 Dev") + } release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. + // TODO: 上线前换成正式签名(当前沿用 debug keystore) + resValue("string", "app_name", "清听") signingConfig = signingConfigs.getByName("debug") } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f933c72..25c40bd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + + @@ -12,7 +12,7 @@ getFile(String url); @@ -17,7 +18,7 @@ class WebDAVFileProvider implements FileProvider { try { // 1. 获取缓存目录 final cacheDir = await getTemporaryDirectory(); - final cachePath = '${cacheDir.path}/metadata_${url.hashCode}.tmp'; + final cachePath = '${cacheDir.path}/metadata_${songKeyForUrl(url)}.tmp'; final cacheFile = File(cachePath); // 2. 如果缓存存在且未过期,直接返回 diff --git a/lib/metadata/metadata_service.dart b/lib/metadata/metadata_service.dart index 0039466..7a21d4b 100644 --- a/lib/metadata/metadata_service.dart +++ b/lib/metadata/metadata_service.dart @@ -9,6 +9,7 @@ import 'metadata_normalizer.dart'; import 'metadata_cache.dart'; import 'file_provider.dart'; import '../utils/artwork_helper.dart'; +import '../utils/song_key.dart'; class MetadataService { static final MetadataService _instance = MetadataService._internal(); @@ -20,9 +21,8 @@ class MetadataService { final MetadataCache _memoryCache = MetadataCache(); final SongDatabase _db = SongDatabase(); - String _generateSongKey(String url, int fileSize, int modifiedTime) { - final raw = '$url|$fileSize|$modifiedTime'; - return raw.hashCode.toString(); + String _generateSongKey(String url, int fileSize) { + return songKeyFor(url, fileSize); } FileProvider _getProvider(String url) { @@ -68,8 +68,7 @@ class MetadataService { } final stat = await file.stat(); - final songKey = - _generateSongKey(url, stat.size, stat.modified.millisecondsSinceEpoch); + final songKey = _generateSongKey(url, stat.size); // 3. SQLite 查询(高置信度直接使用) if (!forceRefresh) { @@ -133,9 +132,12 @@ class MetadataService { } // 8. 保存到 SQLite + // 注意:remote_path 必须存「应用内的歌曲路径」(即 fileId / song.id), + // 不能存完整 URL。playback_controller 与 AudioService 都是用相对路径 + // 调 getSongByPath() 的,存 URL 会导致永远查不到、元数据看起来“没吃上”。 await _db.insertSong({ 'song_key': songKey, - 'remote_path': url, + 'remote_path': fileId, 'file_size': stat.size, 'modified_time': stat.modified.millisecondsSinceEpoch, 'title': finalMetadata.title, diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index dffe4b3..56c1ccf 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -10,6 +10,7 @@ import 'playback_service.dart'; import '../metadata/metadata_service.dart'; import '../database/song_database.dart'; import '../utils/artwork_helper.dart'; +import '../utils/song_key.dart'; import '../repositories/playlist_repository.dart'; import '../models/playlist.dart'; import 'webdav_service.dart'; @@ -351,9 +352,8 @@ class AudioService extends ChangeNotifier { return {}; } - String _generateSongKey(String url, int fileSize, int modifiedTime) { - final raw = '$url|$fileSize|$modifiedTime'; - return raw.hashCode.toString(); + String _generateSongKey(String url, int fileSize) { + return songKeyFor(url, fileSize); } // ════════════════════════════════════════════════════════════ @@ -442,13 +442,12 @@ class AudioService extends ChangeNotifier { final file = await provider.getFile(url); if (file != null && await file.exists()) { final stat = await file.stat(); - songKey = _generateSongKey( - url, stat.size, stat.modified.millisecondsSinceEpoch); + songKey = _generateSongKey(url, stat.size); } else { - songKey = url.hashCode.toString(); + songKey = songKeyForUrl(url); } } catch (e) { - songKey = url.hashCode.toString(); + songKey = songKeyForUrl(url); } final db = SongDatabase(); @@ -461,7 +460,7 @@ class AudioService extends ChangeNotifier { try { final cacheDir = await getTemporaryDirectory(); - final cachePath = '${cacheDir.path}/metadata_${url.hashCode}.tmp'; + final cachePath = '${cacheDir.path}/metadata_${songKeyForUrl(url)}.tmp'; final cacheFile = File(cachePath); if (await cacheFile.exists()) { await cacheFile.delete(); diff --git a/lib/utils/song_key.dart b/lib/utils/song_key.dart new file mode 100644 index 0000000..f150ffb --- /dev/null +++ b/lib/utils/song_key.dart @@ -0,0 +1,22 @@ +// lib/utils/song_key.dart +import 'dart:convert'; +import 'package:crypto/crypto.dart'; + +/// 歌曲缓存键(song_key)。 +/// +/// 只使用「远程路径 + 文件大小」这类稳定信息: +/// - 不能用 String.hashCode:语义上不保证跨运行稳定(虽然当前 VM 实现是稳定的), +/// 用它做持久化键属于依赖未定义行为; +/// - 不能用本地临时文件的 mtime:WebDAV 的元数据预览文件会被系统清缓存、 +/// 或超过 24h 后重新下载,mtime 每次都变,键跟着变 → 缓存永远 miss。 +/// +/// 因此这里用 md5(url|size)。文件被替换但大小不变时不会自动失效, +/// 由“清除单曲缓存 / forceRefresh”兜底。 +String songKeyFor(String url, int fileSize) { + return md5.convert(utf8.encode('$url|$fileSize')).toString(); +} + +/// 只依赖 URL 的稳定键,用于临时文件命名、拿不到 stat 的场景。 +String songKeyForUrl(String url) { + return md5.convert(utf8.encode(url)).toString(); +} diff --git a/tools/device-check.ps1 b/tools/device-check.ps1 index 96eb07f..93fd13d 100644 --- a/tools/device-check.ps1 +++ b/tools/device-check.ps1 @@ -17,13 +17,15 @@ param( [string]$DeviceId = '', [string]$Apk = '', - [int]$WaitSeconds = 12 + [int]$WaitSeconds = 12, + # 默认校验 debug 包(_dev 后缀),与 release 包并存安装 + [string]$Package = 'com.lxh.qingting_player.dev' ) $ErrorActionPreference = 'Continue' # adb 会往 stderr 写正常提示,用 Stop 会误判 $root = Split-Path -Parent $PSScriptRoot if (-not $Apk) { $Apk = Join-Path $root 'build\app\outputs\flutter-apk\app-debug.apk' } -$pkg = 'com.lxh.qingting_player' +$pkg = $Package function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } function Write-Ok($msg) { Write-Host " OK $msg" -ForegroundColor Green } @@ -82,7 +84,13 @@ for ($attempt = 1; $attempt -le 2; $attempt++) { Start-Sleep -Milliseconds 300 } & adb -s $DeviceId logcat -c - & adb -s $DeviceId shell am start -W -n "$pkg/.MainActivity" 2>&1 | Out-Null + # 用 MAIN/LAUNCHER 意图启动:applicationId 带 .dev 后缀时, + # "am start -n $pkg/.MainActivity" 会把 .MainActivity 拼到 .dev 上,导致找不到类。 + # 解析真正的启动组件再启动:applicationId 带 .dev 后缀时, + # "$pkg/.MainActivity" 会把 .MainActivity 拼到 .dev 上导致找不到类; + # 而 "am start -p $pkg" 在部分 ROM 上又解析不到,所以直接问 PackageManager。 + $component = (& adb -s $DeviceId shell cmd package resolve-activity --brief $pkg 2>&1 | Select-Object -Last 1).Trim() + & adb -s $DeviceId shell am start -W -n $component 2>&1 | Out-Null Start-Sleep -Seconds $WaitSeconds & adb -s $DeviceId logcat -d -v time > $logPath # 注意:PS 5.1 的 > 重定向写出的是 UTF-16LE(带 BOM),读取时不要强制 utf8 @@ -106,7 +114,7 @@ if ($players.Count -eq 1) { Write-Ok "Player 唯一:$($players[0])" } else { W # ---------- 系统侧 ---------- Write-Step '系统 MediaSession' $sessionIds = @(& adb -s $DeviceId shell dumpsys media_session 2>&1 | - Select-String 'media-session com\.lxh\.qingting_player/media-session/(\d+)' | + Select-String ("media-session " + [regex]::Escape($pkg) + "/media-session/(\d+)") | ForEach-Object { $_.Matches[0].Groups[1].Value } | Sort-Object -Unique) if ($sessionIds.Count -eq 1) { Write-Ok "MediaSession 唯一:$($sessionIds[0])" } elseif ($sessionIds.Count -eq 0) { Write-Host ' -- 还没有 MediaSession(未开始播放,属正常)' -ForegroundColor DarkGray }