Commit 1b670b5422a1fba0c81a953c21fa1e3e2456dcd2

Authored by yibo
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>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt
@@ -763,14 +763,21 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; Unit, onOpen @@ -763,14 +763,21 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; 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: () -&gt; Unit, onOpen @@ -798,12 +805,29 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; 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: () -&gt; Unit) { @@ -857,7 +881,9 @@ private fun PickerRow(p: Person, onToggle: () -&gt; 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()})")