-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 연락처 작업 #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEAT] 연락처 작업 #31
Changes from 18 commits
146331b
c3f5233
fa77703
c25aa6b
1ee5d34
9b25c62
172e602
be8479c
61d1fd4
22c741b
717b0d4
ad971ef
c4ff3c9
262ded3
45513a1
7768d8f
846bbce
a053f43
18af35b
037f6d3
ffc32db
0e3ff5e
98a8d85
fa5cadc
25f2f20
22ec755
a99de45
2ae7148
1fa484d
508f2d5
5845e24
588a6a3
7360949
02458a5
6510ade
6f5853b
ade3245
c76569d
b1dc0b3
6cc6646
6e5b3d4
4beb928
1e45366
0cc546e
7fa85de
7ce6782
ee361dd
edcedd1
58f927e
175c29b
8748685
309ff01
f43e836
7c3ccdf
4392caf
6e6b67a
3e6ea19
3dd19fe
6418d94
761beb1
b71d500
d515da3
6e24a77
28bb3c8
37e03ee
c59da14
d56a591
0afb0de
3afbe18
648c33e
d8ee179
39360fe
12eb9e6
2df8fe4
7eea373
567636c
5e1e55a
d7f9a59
18c3095
abc4337
5c245f4
427e766
5505311
47c1b92
634872a
08ad462
66a6488
b0fe6b1
3917c00
ad35f20
59d478a
fddabb7
f2ab07f
32c70d8
83e9aba
90e1696
d291865
8fb3bfc
a669663
fb48a73
6165bff
2fac627
ea7a2e1
0c22958
c536bf4
dcfd9ca
3396b74
48834cf
d3c08c5
d58d85d
9d1453b
7b59095
fc190f0
1577a75
1596dab
2834cc6
b749fcb
c19802b
e798851
5cf9a2f
dc46da1
66ac5a1
93ab1c3
b6577d0
f30d851
4d59fef
d8a4c99
cd47e30
b29e18c
38045f7
25b7bd9
a35fba3
3d9cb34
7be0ca9
3c9719e
a89f88e
00bb29d
c192e13
c186b40
2b1acb2
ce51d82
c389154
2b4c1d0
4522d0f
fc13f52
5ecc798
b652c3e
9e6aad2
dc3acc4
96dec0a
158ecc5
2b53a2c
0494397
3522577
d0ae5ce
b70979a
cfbbc4b
ee6145b
a47d238
7b1828d
cd854b6
81da523
0da3c58
aefff08
d6b4f59
9d21859
c582edd
0acc37a
96e03fc
db84722
9dd5e70
fd603ed
b3eef63
c563b9d
7bea36d
4d0853e
4c95de4
0550298
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.alarmy.near.data.mapper | ||
|
|
||
| import com.alarmy.near.local.entity.ContactEntity | ||
| import com.alarmy.near.model.contact.Contact | ||
|
|
||
| fun ContactEntity.toModel(): Contact = | ||
| Contact( | ||
| id = id, | ||
| name = name, | ||
| phones = phones, | ||
| photoUri = photoUri, | ||
| birthDay = birthDay, | ||
| memo = memo, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.alarmy.near.data.repository | ||
|
|
||
| import com.alarmy.near.model.contact.Contact | ||
| import kotlinx.coroutines.flow.Flow | ||
|
|
||
| interface ContactRepository { | ||
| fun fetchAllContacts(): Flow<List<Contact>> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.alarmy.near.data.repository | ||
|
|
||
| import com.alarmy.near.data.mapper.toModel | ||
| import com.alarmy.near.local.contact.ContactLocalDataSource | ||
| import com.alarmy.near.model.contact.Contact | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.flow | ||
| import javax.inject.Inject | ||
|
|
||
| class DefaultContactRepository | ||
| @Inject | ||
| constructor( | ||
| private val contactDataSource: ContactLocalDataSource, | ||
| ) : ContactRepository { | ||
| override fun fetchAllContacts(): Flow<List<Contact>> = | ||
| flow { | ||
| emit( | ||
| contactDataSource.getAllContacts().map { | ||
| it.toModel() | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| package com.alarmy.near.local.contact | ||
|
|
||
| import android.content.ContentResolver | ||
| import android.provider.ContactsContract | ||
| import com.alarmy.near.local.entity.ContactEntity | ||
| import com.alarmy.near.local.entity.ImportantDate | ||
| import javax.inject.Inject | ||
|
|
||
| class ContactLocalDataSource | ||
| @Inject | ||
| constructor( | ||
| private val contentResolver: ContentResolver, | ||
| ) { | ||
| fun getAllContacts(): List<ContactEntity> { | ||
| val contacts = mutableListOf<ContactEntity>() | ||
|
|
||
| val cursor = | ||
| contentResolver.query( | ||
| ContactsContract.Contacts.CONTENT_URI, | ||
| null, | ||
| null, | ||
| null, | ||
| "${ContactsContract.Contacts.DISPLAY_NAME} ASC", | ||
| ) | ||
|
|
||
| cursor?.use { | ||
| val idIndex = it.getColumnIndex(ContactsContract.Contacts._ID) | ||
| val nameIndex = it.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME) | ||
| val hasPhoneIndex = it.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER) | ||
| val photoIndex = it.getColumnIndex(ContactsContract.Contacts.PHOTO_URI) | ||
|
|
||
| while (it.moveToNext()) { | ||
| val id = it.getLong(idIndex) | ||
| val name = it.getString(nameIndex) ?: "" | ||
| val hasPhone = it.getInt(hasPhoneIndex) > 0 | ||
| val photoUri = it.getString(photoIndex) | ||
|
|
||
| // 전화번호 | ||
| val phones = mutableListOf<String>() | ||
| if (hasPhone) { | ||
| val phoneCursor = | ||
| contentResolver.query( | ||
| ContactsContract.CommonDataKinds.Phone.CONTENT_URI, | ||
| null, | ||
| "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?", | ||
| arrayOf(id.toString()), | ||
| null, | ||
| ) | ||
| phoneCursor?.use { pc -> | ||
| val phoneIndex = | ||
| pc.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) | ||
| while (pc.moveToNext()) { | ||
| phones.add(pc.getString(phoneIndex)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 메모 | ||
| var memo: String? = null | ||
| val noteCursor = | ||
| contentResolver.query( | ||
| ContactsContract.Data.CONTENT_URI, | ||
| arrayOf(ContactsContract.CommonDataKinds.Note.NOTE), | ||
| "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?", | ||
| arrayOf(id.toString(), ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE), | ||
| null, | ||
| ) | ||
| noteCursor?.use { nc -> | ||
| if (nc.moveToFirst()) { | ||
| memo = nc.getString(0) | ||
| } | ||
| } | ||
|
|
||
| // 생일 | ||
| var birthDay: String? = null | ||
| val birthdayCursor = | ||
| contentResolver.query( | ||
| ContactsContract.Data.CONTENT_URI, | ||
| arrayOf(ContactsContract.CommonDataKinds.Event.START_DATE), | ||
| "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? AND ${ContactsContract.CommonDataKinds.Event.TYPE} = ?", | ||
| arrayOf( | ||
| id.toString(), | ||
| ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, | ||
| ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY | ||
| .toString(), | ||
| ), | ||
| null, | ||
| ) | ||
| birthdayCursor?.use { bc -> | ||
| if (bc.moveToFirst()) { | ||
| birthDay = bc.getString(0) | ||
| } | ||
| } | ||
|
|
||
| // 그룹 | ||
| val groups = mutableListOf<String>() | ||
| val groupCursor = | ||
| contentResolver.query( | ||
| ContactsContract.Data.CONTENT_URI, | ||
| arrayOf(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID), | ||
| "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?", | ||
| arrayOf( | ||
| id.toString(), | ||
| ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE, | ||
| ), | ||
| null, | ||
| ) | ||
| groupCursor?.use { gc -> | ||
| val groupIdIndex = | ||
| gc.getColumnIndex( | ||
| ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, | ||
| ) | ||
| while (gc.moveToNext()) { | ||
| val groupId = gc.getLong(groupIdIndex) | ||
| val groupNameCursor = | ||
| contentResolver.query( | ||
| ContactsContract.Groups.CONTENT_URI, | ||
| arrayOf(ContactsContract.Groups.TITLE), | ||
| "${ContactsContract.Groups._ID} = ?", | ||
| arrayOf(groupId.toString()), | ||
| null, | ||
| ) | ||
| groupNameCursor?.use { gnc -> | ||
| if (gnc.moveToFirst()) { | ||
| groups.add(gnc.getString(0)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 중요한 날 (기념일) | ||
| val importantDates = mutableListOf<ImportantDate>() | ||
| val eventCursor = | ||
| contentResolver.query( | ||
| ContactsContract.Data.CONTENT_URI, | ||
| arrayOf( | ||
| ContactsContract.CommonDataKinds.Event.START_DATE, | ||
| ContactsContract.CommonDataKinds.Event.TYPE, | ||
| ContactsContract.CommonDataKinds.Event.LABEL, | ||
| ), | ||
| "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?", | ||
| arrayOf( | ||
| id.toString(), | ||
| ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, | ||
| ), | ||
| null, | ||
| ) | ||
| eventCursor?.use { ec -> | ||
| val dateIndex = | ||
| ec.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE) | ||
| val typeIndex = | ||
| ec.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE) | ||
| val labelIndex = | ||
| ec.getColumnIndex(ContactsContract.CommonDataKinds.Event.LABEL) | ||
|
|
||
| while (ec.moveToNext()) { | ||
| val date = ec.getString(dateIndex) | ||
| val type = ec.getInt(typeIndex) | ||
| val customLabel = ec.getString(labelIndex) | ||
|
|
||
| val label = | ||
| when (type) { | ||
| ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY -> { | ||
| continue // 위에서 생일은 포함했으므로 스킵 | ||
| } | ||
|
|
||
| ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY -> "기념일" | ||
| ContactsContract.CommonDataKinds.Event.TYPE_OTHER -> | ||
| customLabel | ||
| ?: "기타" | ||
|
|
||
| else -> "알 수 없음" | ||
| } | ||
|
|
||
| if (date != null) { | ||
| importantDates.add(ImportantDate(label, date)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| contacts.add( | ||
| ContactEntity( | ||
| id = id, | ||
| name = name, | ||
| phones = phones, | ||
| photoUri = photoUri, | ||
| birthDay = birthDay, | ||
| memo = memo, | ||
| groups = groups, | ||
| importantDates = importantDates, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| return contacts | ||
| } | ||
|
Comment on lines
+14
to
+197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 문제를 해결하려면, |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.alarmy.near.local.contact.di | ||
|
|
||
| import android.content.ContentResolver | ||
| import android.content.Context | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import dagger.hilt.components.SingletonComponent | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| object ContactDataSourceModule { | ||
| @Provides | ||
| @Singleton | ||
| fun provideContentResolver( | ||
| @ApplicationContext context: Context, | ||
| ): ContentResolver = context.contentResolver | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.alarmy.near.local.entity | ||
|
|
||
| data class ContactEntity( | ||
| val id: Long, | ||
| val name: String, | ||
| val phones: List<String>, | ||
| val photoUri: String?, // 사진 | ||
| val birthDay: String?, // 생일 | ||
| val memo: String?, // 메모 | ||
| val groups: List<String>, // 그룹 (ex. 가족, 친구, 회사) | ||
| val importantDates: List<ImportantDate>, | ||
| ) | ||
|
|
||
| data class ImportantDate( | ||
| val label: String, | ||
| val date: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.alarmy.near.model.contact | ||
|
|
||
| import android.os.Parcelable | ||
| import kotlinx.parcelize.Parcelize | ||
|
|
||
| @Parcelize | ||
| data class Contact( | ||
| val id: Long, | ||
| val name: String, | ||
| val phones: List<String>, | ||
| val photoUri: String?, // 사진 | ||
| val birthDay: String?, // 생일 | ||
| val memo: String?, // 메모 | ||
| ) : Parcelable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchAllContacts함수 내에서contactDataSource.getAllContacts()를 호출하고 있는데, 이는 디스크 I/O를 유발하는 블로킹(blocking) 작업입니다.flow빌더는 기본적으로 수집자(collector)의 코루틴 컨텍스트에서 실행되므로, 이 코드가 메인 스레드에서 실행될 경우 UI 멈춤(ANR)을 유발할 수 있습니다.flowOn(Dispatchers.IO)를 사용하여 이 작업을 백그라운드 스레드에서 실행하도록 변경하는 것이 좋습니다.