Commit 1b670b5422a1fba0c81a953c21fa1e3e2456dcd2
1 parent
e3109a74
feat(lifelog/photolog): 微信好友 vs 群友区分 + 导入只导好友(默认)
4922 联系人里绝大多数是群友(共群非好友),真好友只 ~214。区分依据(真机验证): - 好友 = rcontact.type & 1 = 1 且 deleteFlag = 0(有备注的人无一 type=4,佐证 type=4=群友) - persons.wechatFriend 列(迁移 19→20,true=好友/false=群友/null=未知) - WeChatContactsImporter 加 isFriend 计算 + friendsOnly 参数;img_flag 改 LEFT JOIN (好友头像多不在 img_flag,INNER JOIN 会漏,改 LEFT 后头像可空、人照导) - WeChatEnrollWorker 落 wechatFriend - 通讯录:导入分「💬微信好友(只好友)」/「+群友(全部)」;过滤 chip 好友/群友/全部(默认好友); 每行标签 微信好友/群友/微信 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
7 changed files
with
81 additions
and
28 deletions
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt
| @@ -763,14 +763,21 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit, onOpen | @@ -763,14 +763,21 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit, onOpen | ||
| 763 | val enrolling by vm.enrolling.collectAsState() | 763 | val enrolling by vm.enrolling.collectAsState() |
| 764 | var enrollTapped by remember { mutableStateOf(false) } | 764 | var enrollTapped by remember { mutableStateOf(false) } |
| 765 | var query by remember { mutableStateOf("") } | 765 | var query by remember { mutableStateOf("") } |
| 766 | + var friendFilter by remember { mutableStateOf(0) } // 0=好友 1=群友 2=全部 | ||
| 766 | val managedCount = all.count { it.managed } | 767 | val managedCount = all.count { it.managed } |
| 768 | + val friendCount = all.count { it.wechatFriend == true } | ||
| 767 | 769 | ||
| 768 | - val sections = remember(all, query) { | 770 | + val sections = remember(all, query, friendFilter) { |
| 769 | val q = query.trim() | 771 | val q = query.trim() |
| 770 | - val filtered = if (q.isBlank()) all else all.filter { p -> | ||
| 771 | - p.name.contains(q, true) || p.nickname?.contains(q, true) == true || | 772 | + val filtered = all.filter { p -> |
| 773 | + (q.isBlank() || p.name.contains(q, true) || p.nickname?.contains(q, true) == true || | ||
| 772 | p.remark?.contains(q, true) == true || p.alias?.contains(q, true) == true || | 774 | p.remark?.contains(q, true) == true || p.alias?.contains(q, true) == true || |
| 773 | - p.pinyin?.contains(q, true) == true | 775 | + p.pinyin?.contains(q, true) == true) && |
| 776 | + when (friendFilter) { | ||
| 777 | + 0 -> p.wechatFriend == true | ||
| 778 | + 1 -> p.wechatFriend == false || (p.contactRef?.startsWith("wechat:") == true && p.wechatFriend == null) | ||
| 779 | + else -> true | ||
| 780 | + } | ||
| 774 | } | 781 | } |
| 775 | filtered.sortedWith(compareBy({ sectionLetter(it) }, { it.pinyin ?: it.name })) | 782 | filtered.sortedWith(compareBy({ sectionLetter(it) }, { it.pinyin ?: it.name })) |
| 776 | .groupBy { sectionLetter(it) } | 783 | .groupBy { sectionLetter(it) } |
| @@ -798,12 +805,29 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit, onOpen | @@ -798,12 +805,29 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit, onOpen | ||
| 798 | Button( | 805 | Button( |
| 799 | enabled = !enrolling, | 806 | enabled = !enrolling, |
| 800 | onClick = { | 807 | onClick = { |
| 801 | - android.widget.Toast.makeText(ctx, "读微信联系人中…(首次下几千头像会慢,首次需授予 root)", android.widget.Toast.LENGTH_SHORT).show() | ||
| 802 | - vm.importWeChatContactsLocal { msg -> android.widget.Toast.makeText(ctx, msg, android.widget.Toast.LENGTH_LONG).show() } | 808 | + android.widget.Toast.makeText(ctx, "读微信好友中…(首次需授予 root)", android.widget.Toast.LENGTH_SHORT).show() |
| 809 | + vm.importWeChatContactsLocal(friendsOnly = true) { msg -> android.widget.Toast.makeText(ctx, msg, android.widget.Toast.LENGTH_LONG).show() } | ||
| 803 | enrollTapped = true | 810 | enrollTapped = true |
| 804 | }, | 811 | }, |
| 805 | - ) { Text(if (enrolling) "导入中…" else "💬 微信导入") } | ||
| 806 | - OutlinedButton(onClick = onOpenPhonePicker) { Text("📱 手机通讯录") } | 812 | + ) { Text(if (enrolling) "导入中…" else "💬 微信好友") } |
| 813 | + OutlinedButton( | ||
| 814 | + enabled = !enrolling, | ||
| 815 | + onClick = { | ||
| 816 | + android.widget.Toast.makeText(ctx, "连群友一起读…(几千人+头像,会慢)", android.widget.Toast.LENGTH_SHORT).show() | ||
| 817 | + vm.importWeChatContactsLocal(friendsOnly = false) { msg -> android.widget.Toast.makeText(ctx, msg, android.widget.Toast.LENGTH_LONG).show() } | ||
| 818 | + enrollTapped = true | ||
| 819 | + }, | ||
| 820 | + ) { Text("+群友") } | ||
| 821 | + OutlinedButton(onClick = onOpenPhonePicker) { Text("📱 手机") } | ||
| 822 | + } | ||
| 823 | + // 好友/群友过滤。 | ||
| 824 | + Row( | ||
| 825 | + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 2.dp), | ||
| 826 | + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, | ||
| 827 | + ) { | ||
| 828 | + FilterChip(selected = friendFilter == 0, onClick = { friendFilter = 0 }, label = { Text("好友 $friendCount") }) | ||
| 829 | + FilterChip(selected = friendFilter == 1, onClick = { friendFilter = 1 }, label = { Text("群友") }) | ||
| 830 | + FilterChip(selected = friendFilter == 2, onClick = { friendFilter = 2 }, label = { Text("全部 ${all.size}") }) | ||
| 807 | } | 831 | } |
| 808 | // 全选/取消全选:作用于当前过滤后的列表(可先搜一批再全选)。 | 832 | // 全选/取消全选:作用于当前过滤后的列表(可先搜一批再全选)。 |
| 809 | val filtered = remember(sections) { sections.values.flatten() } | 833 | val filtered = remember(sections) { sections.values.flatten() } |
| @@ -857,7 +881,9 @@ private fun PickerRow(p: Person, onToggle: () -> Unit) { | @@ -857,7 +881,9 @@ private fun PickerRow(p: Person, onToggle: () -> Unit) { | ||
| 857 | Column(modifier = Modifier.weight(1f)) { | 881 | Column(modifier = Modifier.weight(1f)) { |
| 858 | Text(p.name, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleSmall) | 882 | Text(p.name, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleSmall) |
| 859 | val src = when { | 883 | val src = when { |
| 860 | - p.contactRef?.startsWith("wechat:") == true -> "微信" | 884 | + p.contactRef?.startsWith("wechat:") == true -> when (p.wechatFriend) { |
| 885 | + true -> "微信好友"; false -> "群友"; null -> "微信" | ||
| 886 | + } | ||
| 861 | p.contactRef?.startsWith("phone:") == true -> "手机" | 887 | p.contactRef?.startsWith("phone:") == true -> "手机" |
| 862 | else -> null | 888 | else -> null |
| 863 | } | 889 | } |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologViewModel.kt
| @@ -486,16 +486,16 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { | @@ -486,16 +486,16 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { | ||
| 486 | */ | 486 | */ |
| 487 | private val _enrolling = MutableStateFlow(false) | 487 | private val _enrolling = MutableStateFlow(false) |
| 488 | val enrolling: StateFlow<Boolean> = _enrolling | 488 | val enrolling: StateFlow<Boolean> = _enrolling |
| 489 | - fun importWeChatContactsLocal(onDone: (String) -> Unit) { | 489 | + fun importWeChatContactsLocal(friendsOnly: Boolean = true, onDone: (String) -> Unit) { |
| 490 | if (_enrolling.value) return | 490 | if (_enrolling.value) return |
| 491 | _enrolling.value = true | 491 | _enrolling.value = true |
| 492 | val app = getApplication<Application>() | 492 | val app = getApplication<Application>() |
| 493 | viewModelScope.launch { | 493 | viewModelScope.launch { |
| 494 | - val res = withContext(Dispatchers.IO) { com.lemon.lifelog.vision.WeChatContactsImporter.importContacts(app) } | 494 | + val res = withContext(Dispatchers.IO) { com.lemon.lifelog.vision.WeChatContactsImporter.importContacts(app, friendsOnly) } |
| 495 | if (res.contacts > 0) enrollWeChat() // 触发 upsert + 人脸锚点 | 495 | if (res.contacts > 0) enrollWeChat() // 触发 upsert + 人脸锚点 |
| 496 | _enrolling.value = false | 496 | _enrolling.value = false |
| 497 | onDone( | 497 | onDone( |
| 498 | - if (res.contacts > 0) "已读取 ${res.contacts} 个联系人 + ${res.avatars} 头像,正在建档/认人…" | 498 | + if (res.contacts > 0) "已读取 ${res.contacts} 人(好友 ${res.friends})+ ${res.avatars} 头像,正在建档/认人…" |
| 499 | else (res.error ?: "没读到联系人"), | 499 | else (res.error ?: "没读到联系人"), |
| 500 | ) | 500 | ) |
| 501 | } | 501 | } |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt
| @@ -28,7 +28,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase | @@ -28,7 +28,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase | ||
| 28 | Topic::class, | 28 | Topic::class, |
| 29 | TopicMention::class, | 29 | TopicMention::class, |
| 30 | ], | 30 | ], |
| 31 | - version = 19, | 31 | + version = 20, |
| 32 | exportSchema = false, | 32 | exportSchema = false, |
| 33 | ) | 33 | ) |
| 34 | abstract class AppDatabase : RoomDatabase() { | 34 | abstract class AppDatabase : RoomDatabase() { |
| @@ -216,6 +216,13 @@ abstract class AppDatabase : RoomDatabase() { | @@ -216,6 +216,13 @@ abstract class AppDatabase : RoomDatabase() { | ||
| 216 | } | 216 | } |
| 217 | } | 217 | } |
| 218 | 218 | ||
| 219 | + /** v0.8 微信好友/群友区分:persons.wechatFriend(rcontact.type&1=1 且未删 = 好友)。 */ | ||
| 220 | + private val MIGRATION_19_20 = object : Migration(19, 20) { | ||
| 221 | + override fun migrate(db: SupportSQLiteDatabase) { | ||
| 222 | + db.execSQL("ALTER TABLE `persons` ADD COLUMN `wechatFriend` INTEGER") | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 219 | fun get(ctx: Context): AppDatabase = | 226 | fun get(ctx: Context): AppDatabase = |
| 220 | instance ?: synchronized(this) { | 227 | instance ?: synchronized(this) { |
| 221 | instance ?: Room.databaseBuilder( | 228 | instance ?: Room.databaseBuilder( |
| @@ -223,7 +230,7 @@ abstract class AppDatabase : RoomDatabase() { | @@ -223,7 +230,7 @@ abstract class AppDatabase : RoomDatabase() { | ||
| 223 | AppDatabase::class.java, | 230 | AppDatabase::class.java, |
| 224 | "lifelog.db", | 231 | "lifelog.db", |
| 225 | ) | 232 | ) |
| 226 | - .addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19) | 233 | + .addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20) |
| 227 | // 不再 fallbackToDestructiveMigration:以后改 schema 必须写 Migration, | 234 | // 不再 fallbackToDestructiveMigration:以后改 schema 必须写 Migration, |
| 228 | // 否则启动直接抛 IllegalStateException(fail-loud),绝不静默清掉真实录音。 | 235 | // 否则启动直接抛 IllegalStateException(fail-loud),绝不静默清掉真实录音。 |
| 229 | .build() | 236 | .build() |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt
| @@ -591,6 +591,10 @@ interface PersonDao { | @@ -591,6 +591,10 @@ interface PersonDao { | ||
| 591 | @Query("UPDATE persons SET name = :name WHERE id = :id") | 591 | @Query("UPDATE persons SET name = :name WHERE id = :id") |
| 592 | suspend fun updateName(id: Long, name: String) | 592 | suspend fun updateName(id: Long, name: String) |
| 593 | 593 | ||
| 594 | + /** 微信好友/群友标记(enroll 时设:true=好友,false=群友)。 */ | ||
| 595 | + @Query("UPDATE persons SET wechatFriend = :friend WHERE id = :id") | ||
| 596 | + suspend fun setWechatFriend(id: Long, friend: Boolean?) | ||
| 597 | + | ||
| 594 | /** 合并人物:写回合并后的整条记录(按主键更新)。 */ | 598 | /** 合并人物:写回合并后的整条记录(按主键更新)。 */ |
| 595 | @Update | 599 | @Update |
| 596 | suspend fun update(p: Person) | 600 | suspend fun update(p: Person) |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Entities.kt
| @@ -108,6 +108,8 @@ data class Person( | @@ -108,6 +108,8 @@ data class Person( | ||
| 108 | val pinyin: String? = null, // 全拼(排序) | 108 | val pinyin: String? = null, // 全拼(排序) |
| 109 | val pyInitial: String? = null, // 首字母(A–Z 索引) | 109 | val pyInitial: String? = null, // 首字母(A–Z 索引) |
| 110 | val phone: String? = null, // 手机号(短信关联 / 手机通讯录去重键,存纯数字) | 110 | val phone: String? = null, // 手机号(短信关联 / 手机通讯录去重键,存纯数字) |
| 111 | + /** 微信关系:true=真好友(rcontact.type&1=1 且未删),false=群友/陌生人,null=非微信/未知。 */ | ||
| 112 | + val wechatFriend: Boolean? = null, | ||
| 111 | // ----- 手填资料 ----- | 113 | // ----- 手填资料 ----- |
| 112 | val realName: String? = null, // 真名(微信昵称往往是网名,真名手填) | 114 | val realName: String? = null, // 真名(微信昵称往往是网名,真名手填) |
| 113 | val resume: String? = null, // 简历 / 备注(职业、公司、关系等手填长文) | 115 | val resume: String? = null, // 简历 / 备注(职业、公司、关系等手填长文) |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/vision/WeChatContactsImporter.kt
| @@ -22,35 +22,42 @@ import java.util.concurrent.TimeUnit | @@ -22,35 +22,42 @@ import java.util.concurrent.TimeUnit | ||
| 22 | object WeChatContactsImporter { | 22 | object WeChatContactsImporter { |
| 23 | private const val TAG = "WeChatContacts" | 23 | private const val TAG = "WeChatContacts" |
| 24 | 24 | ||
| 25 | - // 真人联系人 + 高清头像 URL(移植自 Mac extract-wechat-avatars.py)。 | ||
| 26 | - private const val REAL_PERSON_SQL = """ | 25 | + // 真人联系人 + 头像 URL + 好友判定(rcontact.type&1=1 且未删 = 好友,否则群友/陌生人)。 |
| 26 | + // LEFT JOIN img_flag:好友头像很多不在 img_flag(那是群里看过被缓存的),所以头像可空、人照常导。 | ||
| 27 | + private fun realPersonSql(friendsOnly: Boolean): String = """ | ||
| 27 | SELECT r.username AS username, | 28 | SELECT r.username AS username, |
| 28 | COALESCE(NULLIF(r.conRemark,''), NULLIF(r.nickname,''), r.username) AS name, | 29 | COALESCE(NULLIF(r.conRemark,''), NULLIF(r.nickname,''), r.username) AS name, |
| 29 | r.nickname AS nickname, r.conRemark AS remark, r.alias AS alias, | 30 | r.nickname AS nickname, r.conRemark AS remark, r.alias AS alias, |
| 30 | r.pyInitial AS pyInitial, r.quanPin AS quanPin, | 31 | r.pyInitial AS pyInitial, r.quanPin AS quanPin, |
| 31 | - f.reserved1 AS avatar | ||
| 32 | - FROM rcontact r JOIN img_flag f ON f.username = r.username | 32 | + f.reserved1 AS avatar, |
| 33 | + CASE WHEN (r.type & 1) = 1 AND r.deleteFlag = 0 THEN 1 ELSE 0 END AS isFriend | ||
| 34 | + FROM rcontact r LEFT JOIN img_flag f ON f.username = r.username | ||
| 33 | WHERE r.verifyFlag = 0 | 35 | WHERE r.verifyFlag = 0 |
| 34 | AND r.username NOT LIKE '%@chatroom' | 36 | AND r.username NOT LIKE '%@chatroom' |
| 35 | AND r.username NOT LIKE 'gh_%' | 37 | AND r.username NOT LIKE 'gh_%' |
| 36 | AND r.username NOT LIKE '%@openim' | 38 | AND r.username NOT LIKE '%@openim' |
| 39 | + AND r.username NOT LIKE '%@stranger' | ||
| 37 | AND r.username NOT IN ('weixin','filehelper','fmessage','medianote','floatbottle', | 40 | AND r.username NOT IN ('weixin','filehelper','fmessage','medianote','floatbottle', |
| 38 | 'qmessage','qqmail','newsapp','officialaccounts','helper_entry','exmail_tool', | 41 | 'qmessage','qqmail','newsapp','officialaccounts','helper_entry','exmail_tool', |
| 39 | 'mphelper','brandsessionholder') | 42 | 'mphelper','brandsessionholder') |
| 40 | - AND f.reserved1 LIKE 'http%' | 43 | + ${if (friendsOnly) "AND (r.type & 1) = 1 AND r.deleteFlag = 0" else ""} |
| 41 | """ | 44 | """ |
| 42 | 45 | ||
| 43 | - data class Result(val contacts: Int, val avatars: Int, val error: String? = null) | 46 | + data class Result(val contacts: Int, val friends: Int, val avatars: Int, val error: String? = null) |
| 44 | 47 | ||
| 45 | private data class Row(val username: String, val obj: JSONObject, val avatarUrl: String?) | 48 | private data class Row(val username: String, val obj: JSONObject, val avatarUrl: String?) |
| 46 | 49 | ||
| 47 | - /** 解密读联系人 + 下头像,写 `wx_enroll/`。**必须 IO 线程调用**(含 su/解密/网络/文件 IO)。 */ | ||
| 48 | - fun importContacts(ctx: Context): Result { | 50 | + /** |
| 51 | + * 解密读联系人 + 下头像,写 `wx_enroll/`。[friendsOnly]=true 只导真好友(~几百),false 连群友(几千)。 | ||
| 52 | + * **必须 IO 线程调用**(含 su/解密/网络/文件 IO)。 | ||
| 53 | + */ | ||
| 54 | + fun importContacts(ctx: Context, friendsOnly: Boolean = true): Result { | ||
| 49 | val (opened, err) = WeChatDb.openEnMicroMsg(ctx) | 55 | val (opened, err) = WeChatDb.openEnMicroMsg(ctx) |
| 50 | - if (opened == null) return Result(0, 0, err) | 56 | + if (opened == null) return Result(0, 0, 0, err) |
| 51 | val rows = ArrayList<Row>() | 57 | val rows = ArrayList<Row>() |
| 58 | + var friends = 0 | ||
| 52 | try { | 59 | try { |
| 53 | - opened.db.rawQuery(REAL_PERSON_SQL, null).use { c -> | 60 | + opened.db.rawQuery(realPersonSql(friendsOnly), null).use { c -> |
| 54 | val iU = c.getColumnIndexOrThrow("username") | 61 | val iU = c.getColumnIndexOrThrow("username") |
| 55 | val iName = c.getColumnIndexOrThrow("name") | 62 | val iName = c.getColumnIndexOrThrow("name") |
| 56 | val iNick = c.getColumnIndexOrThrow("nickname") | 63 | val iNick = c.getColumnIndexOrThrow("nickname") |
| @@ -59,8 +66,11 @@ object WeChatContactsImporter { | @@ -59,8 +66,11 @@ object WeChatContactsImporter { | ||
| 59 | val iPyInit = c.getColumnIndexOrThrow("pyInitial") | 66 | val iPyInit = c.getColumnIndexOrThrow("pyInitial") |
| 60 | val iQuanPin = c.getColumnIndexOrThrow("quanPin") | 67 | val iQuanPin = c.getColumnIndexOrThrow("quanPin") |
| 61 | val iAvatar = c.getColumnIndexOrThrow("avatar") | 68 | val iAvatar = c.getColumnIndexOrThrow("avatar") |
| 69 | + val iFriend = c.getColumnIndexOrThrow("isFriend") | ||
| 62 | while (c.moveToNext()) { | 70 | while (c.moveToNext()) { |
| 63 | val username = c.getString(iU)?.takeIf { it.isNotBlank() } ?: continue | 71 | val username = c.getString(iU)?.takeIf { it.isNotBlank() } ?: continue |
| 72 | + val isFriend = c.getInt(iFriend) == 1 | ||
| 73 | + if (isFriend) friends++ | ||
| 64 | val o = JSONObject() | 74 | val o = JSONObject() |
| 65 | .put("username", username) | 75 | .put("username", username) |
| 66 | .put("name", c.getString(iName) ?: username) | 76 | .put("name", c.getString(iName) ?: username) |
| @@ -69,16 +79,17 @@ object WeChatContactsImporter { | @@ -69,16 +79,17 @@ object WeChatContactsImporter { | ||
| 69 | .put("alias", c.getString(iAlias) ?: "") | 79 | .put("alias", c.getString(iAlias) ?: "") |
| 70 | .put("pyInitial", c.getString(iPyInit) ?: "") | 80 | .put("pyInitial", c.getString(iPyInit) ?: "") |
| 71 | .put("quanPin", c.getString(iQuanPin) ?: "") | 81 | .put("quanPin", c.getString(iQuanPin) ?: "") |
| 82 | + .put("isFriend", isFriend) | ||
| 72 | .put("hasFace", false) | 83 | .put("hasFace", false) |
| 73 | rows.add(Row(username, o, c.getString(iAvatar)?.takeIf { it.startsWith("http") })) | 84 | rows.add(Row(username, o, c.getString(iAvatar)?.takeIf { it.startsWith("http") })) |
| 74 | } | 85 | } |
| 75 | } | 86 | } |
| 76 | } catch (t: Throwable) { | 87 | } catch (t: Throwable) { |
| 77 | opened.close() | 88 | opened.close() |
| 78 | - return Result(0, 0, "读 rcontact/img_flag 失败:${t.message}") | 89 | + return Result(0, 0, 0, "读 rcontact/img_flag 失败:${t.message}") |
| 79 | } | 90 | } |
| 80 | opened.close() | 91 | opened.close() |
| 81 | - if (rows.isEmpty()) return Result(0, 0, "没读到真人联系人") | 92 | + if (rows.isEmpty()) return Result(0, 0, 0, "没读到联系人") |
| 82 | 93 | ||
| 83 | val enrollDir = File(ctx.filesDir, "wx_enroll").apply { mkdirs() } | 94 | val enrollDir = File(ctx.filesDir, "wx_enroll").apply { mkdirs() } |
| 84 | val avatarDir = File(enrollDir, "avatars").apply { mkdirs() } | 95 | val avatarDir = File(enrollDir, "avatars").apply { mkdirs() } |
| @@ -88,8 +99,8 @@ object WeChatContactsImporter { | @@ -88,8 +99,8 @@ object WeChatContactsImporter { | ||
| 88 | val arr = JSONArray() | 99 | val arr = JSONArray() |
| 89 | rows.forEach { arr.put(it.obj) } | 100 | rows.forEach { arr.put(it.obj) } |
| 90 | File(enrollDir, "enroll.json").writeText(arr.toString()) | 101 | File(enrollDir, "enroll.json").writeText(arr.toString()) |
| 91 | - Log.i(TAG, "联系人 ${rows.size},头像下载 ${downloaded.size}") | ||
| 92 | - return Result(rows.size, downloaded.size) | 102 | + Log.i(TAG, "联系人 ${rows.size}(好友 $friends),头像下载 ${downloaded.size}") |
| 103 | + return Result(rows.size, friends, downloaded.size) | ||
| 93 | } | 104 | } |
| 94 | 105 | ||
| 95 | /** 并发下载头像(跳过已存在的,方便增量/幂等)。返回成功下载/已存在的 username 集合。 */ | 106 | /** 并发下载头像(跳过已存在的,方便增量/幂等)。返回成功下载/已存在的 username 集合。 */ |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/vision/WeChatEnrollWorker.kt
| @@ -49,17 +49,20 @@ class WeChatEnrollWorker(ctx: Context, params: WorkerParameters) : CoroutineWork | @@ -49,17 +49,20 @@ class WeChatEnrollWorker(ctx: Context, params: WorkerParameters) : CoroutineWork | ||
| 49 | val alias = o.optString("alias").ifBlank { null } | 49 | val alias = o.optString("alias").ifBlank { null } |
| 50 | val py = o.optString("quanPin").ifBlank { null } | 50 | val py = o.optString("quanPin").ifBlank { null } |
| 51 | val pyInit = o.optString("pyInitial").ifBlank { null } | 51 | val pyInit = o.optString("pyInitial").ifBlank { null } |
| 52 | + val isFriend = if (o.has("isFriend")) o.optBoolean("isFriend") else null | ||
| 52 | val ex = repo.persons.getByContactRef(ref) | 53 | val ex = repo.persons.getByContactRef(ref) |
| 53 | if (ex == null) { | 54 | if (ex == null) { |
| 54 | repo.persons.insert( | 55 | repo.persons.insert( |
| 55 | Person( | 56 | Person( |
| 56 | name = name, contactRef = ref, avatarPath = avatarPath, | 57 | name = name, contactRef = ref, avatarPath = avatarPath, |
| 57 | nickname = nick, remark = remark, alias = alias, pinyin = py, pyInitial = pyInit, | 58 | nickname = nick, remark = remark, alias = alias, pinyin = py, pyInitial = pyInit, |
| 59 | + wechatFriend = isFriend, | ||
| 58 | ), | 60 | ), |
| 59 | ) | 61 | ) |
| 60 | created++ | 62 | created++ |
| 61 | } else { | 63 | } else { |
| 62 | repo.persons.updateContactInfo(ex.id, name, nick, remark, alias, py, pyInit, avatarPath) | 64 | repo.persons.updateContactInfo(ex.id, name, nick, remark, alias, py, pyInit, avatarPath) |
| 65 | + if (isFriend != null) repo.persons.setWechatFriend(ex.id, isFriend) | ||
| 63 | } | 66 | } |
| 64 | } | 67 | } |
| 65 | Log.i(TAG, "资料 upsert:新建 $created(共 ${arr.length()})") | 68 | Log.i(TAG, "资料 upsert:新建 $created(共 ${arr.length()})") |