본문 바로가기

Android/정리 노트

[Android/kotlin] 전화번호로 주소록 이름 가져오기Get contact display name by phone number

작성 기준

sdk 31

Notice

이 포스팅은 검색 기능의 구현에는 적합하지 않습니다.
단순히 전화번호로 주소록에 등록된 이름을 가져오는 내용입니다.
ex) search "0101234" result "01012345678", "01012341234"... >> X

 

 

전화 관련 서비스를 만들기 위해 기술조사를 하며 예제를 만들어 보다가
전화번호는 받아도 주소록에 등록된 이름은 받아오지 못하여 따로 기능을 만들기 위해 찾아봄

 

Reference) 연락처 정보 가져오기 공식 문서

위 문서에는 예제로 주소록에 등록된 이름(Contact Display Name)을 기준으로 검색하여 보여주고,
링크된 하단에 Column 상관없이 검색할 수 있는 내용이 있어 사용함

 

권한

<uses-permission android:name="android.permission.READ_CONTACTS" />

Runtime Permission이므로 실행중에 사용자에게 요청하여 허용 받아야 함
1. 직접 구현하려면 앱 권한 허용 공식 문서
2. 권한 허용 라이브러리 이용 (Ex. TedPermission)
3. 단순 예제 구현용이라면 Build & Run 후 앱 정보가서 허용해주면 문제없음

 

 

contactDisplayName 을 가져오기 위해서는 두가지 방법이 있다.

1. LoaderManager를 이용한 방법

2. Context.contentResolver를 이용한 방법

2번이 간단하고 써먹기는 좋지만 LoadManager로 구현하고 싶은 경우도 있을 것 같아 작성(은 삽질이 아까워서..)

LoaderManager를 이용한 방법

LoaderManager.LoadCallbacks 구현

Option 1. 변수로 관리할 경우

private val loaderCallback = object : LoaderManager.LoaderCallbacks<Cursor> {
    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        TODO("Not yet implemented")
    }

    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
        TODO("Not yet implemented")
    }

    override fun onLoaderReset(loader: Loader<Cursor>) {
        TODO("Not yet implemented")
    }
}

Option 2. Activity나 Fragment등에 구현할 경우

class SomeActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor> {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_some)
    }

    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        TODO("Not yet implemented")
    }

    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
        TODO("Not yet implemented")
    }

    override fun onLoaderReset(loader: Loader<Cursor>) {
        TODO("Not yet implemented")
    }
}

이 글에서는 Option 1을 기준으로 작성

initLoader

//companion object 영역에 선언
const var LOADER_ID = 12345//Unique id you want


//처음 로더를 호출할 경우
LoaderManager.getInstance(this).initLoader(LOADER_ID, null, loaderCallback)

//한번 만들어진 로더를 다른 전화번호를 검색하기 위해 다시 사용할 경우 restartLoader를 사용
LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, loaderCallback)

//만들어진 로더를 다 사용하고 날리기 위한 경우
LoaderManager.getInstance(this).destroyLoader(LOADER_ID)

initLoader를 원하는 시점에서 호출하면 되는데, 한번의 요청 후 다른 번호로 요청을 할 경우 restartLoader를 이용해 주어야 하고

액티비티의 onDestroy등의 시점에서 destroyLoader를 호출하여 날려주어야 함

initLoader와 restartLoader를 맞는 상황에 쓰기 번거로울 수 있는데 아래 나올 onLoadFinished에서 destroyLoader를 써주면

initLoader로 다시 요청할수는 있음

onCreateLoader

override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
    return CursorLoader(
        context,
        Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
        arrayOf(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY),//projection : 원하는 column만 가져오기 위해 설정. null일 경우 모든 항목 가져옴
        null,//selection
        null,//selectionArgs
        null//sortOrder
    )
}

어떤 정보로 어떤 column을 가져올지 설정하는 함수

uri의 phoneNumber는 "010-1234-1234"와 "01012341234" 두 형태 모두 가능

3번째 인자는 전화번호로 가져올 정보를 나타내는데 DISPLAY_NAME_PRIMARY는 전화번호부에 등록된 이름을 나타내며

이름 뿐 아니라 다른 정보도 가져오고싶다면 array에 추가하거나 null을 넣어 모든 정보를 가져올 수 있다.

ContactsContract.Contacts.DISPLAY_NAME_PRIMARY는
Os 3.0 Honeycomb (sdk version 11)이상에서 사용되고 이하는 DISPLAY_NAME을 사용한다.
minSdk가 10이하인 경우 버전 분기를 해주어야 한다.

onLoadFinished

override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
    data?.apply {
        moveToFirst()
        while (!isAfterLast) {
            getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME).takeIf { it >= 0 }?.let {
                getString(it)
            }?.let {
                Log.d("contactDisplayName", it)
            }
            moveToNext()
        }
        close()
    }
    //restartLoader를 사용하기 싫다면 아래 코드 주석 해제
    //LoaderManager.getInstance(this).destroyLoader(LOADER_ID)
}

Cursor.getString(int)함수가 인자로 음수를 받을 수 없어 takeIf로 처리

onLoadReset

override fun onLoaderReset(loader: Loader<Cursor>) {
    // Delete the reference to the existing Cursor
    // Empty in this example
}

예제 테스트 중에는 위 함수가 호출되지 않아 어떤 상황에서 호출되는지는 확인하지 못함

공식문서에서는 cursor를 참조하는 부분이 있다면 더이상 사용하지 못하게 하는 코드를 작성함 참고

이 예제에서는 cursor의 외부 참조를 남겨놓지 않으므로 빈 함수로 구현

displayName말고도 다른 값을 가져오는 방법

override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
    return CursorLoader(
        this@SomeActivity,//or Context
        Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
        null,
        null,
        null,
        null
    )
}

override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
    data?.apply {
        Log.d("Columns", data.columnNames.joinToString())
        moveToFirst()
        while (!isAfterLast) {
            val arr = mutableListOf<String?>()
            repeat(columnCount){
                arr.add(getString(it))
            }
            Log.d("Data", arr.joinToString())
            moveToNext()
        }
        close()
    }
}

위 처럼 수정 후 돌려보면 로그에 받아올 수 있는 column 이름과 불러온 주소록의 데이터를 전부 확인 할 수 있다.

결과

작성후 로그를 보면 이름이 두번 찍히는 것을 알 수 있다(os10, lg v40 기준).

displayName말고도 다른 값을 찍어보면 completeMatch field가 0과 1로 다른 것을 제외하고는 전부 같은 값을 보여준다.

다른 기기나 os도 마찬가지라면 onLoadFinished를 고쳐 한번만 받아오게 하거나 두번 실행되면 안되는 상황을 고려해서 작성이 필요해 보인다.

 

contentResolver를 이용하는 방법

private fun getContactDisplayName(context: Context, phoneNumber: String): String? =
    context.contentResolver.query(
        Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
        arrayOf(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME),
        null,
        null,
        null
    )?.run {
        var contactName: String? = null
        moveToFirst()
        while (!isAfterLast) {
            getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME).takeIf { it >= 0 }?.let {
                contactName = getString(it)
            }
            moveToNext()
        }
        close()
        contactName
    }
    
//Usage
Log.d("display name", getContactDisplayName("010-1234-5678") ?: "Not Found")
Log.d("display name", getContactDisplayName("01012345678") ?: "Not Found")

사용하기도 쉽고 손이 제일 덜가는 방법