[Android/Kotlin] 안드로이드 주소록 앱(Contacts)에서 연락처 상세 정보 가져오기, cursor의 count가 0일 때 or moveToFirst() 실행 안될 때 문제 해결
안드로이드 기본 앱 중 하나인 주소록(Contacts) 앱에서 사용자 이름(DISPLAY_NAME), 전화번호(NUMBER) 정보를 가져와 메인 액티비티에 TextView로 표시할 것이다.
<관련 개념>
- Content Provider
- ContactsContracts
- Uri
- Cursor
- Activity Result API : ActivityResultLauncher, registerForActivityResult()
실습
1. 주소록(Contacts) 앱에 테스트용 연락처 추가
2. [AndroidManifest.xml] 매니페스트 파일에 주소록 읽기(READ_CONTACTS) 퍼미션 사용 설정
<uses-permission android:name="android.permission.READ_CONTACTS"/>
3. [MainActivity.kt] 파일에 퍼미션 허용 확인 코드 작성 ★
API 레벨 23 이전에는 매니페스트 파일에 <uses-permission>으로 선언만 하면 보호받는 기능을 앱에서 이용하는 데 문제가 없었으나, API 레벨 23(Android 6) 버전 부터는 정책이 허가제로 바뀌어 <uses-permission>을 선언하는 것 뿐만 아니라 앱을 실행할 때 사용자가 퍼미션을 거부했는지 확인하고 만약 거부했다면 다시 퍼미션을 허용해 달라고 요청하는 코드를 작성해야 한다.
관련 메서드 | 설명 |
checkSelfPermission(context: Context, permission: String): Int | 사용자가 퍼미션을 허용했는지 확인한다. |
requestPermission(activity: Activity, permissions: Array<String!>, requestCode: Int): Unit | 퍼미션을 요청하는 다이얼로그를 화면에 표시, 사용자에게 퍼미션을 허용해 달라고 요청한다. |
onRequestPermissionResult(requestCode: Int, permissions: Array<String!>, grantResults: IntArray): Unit | 사용자가 다이얼로그에서 퍼미션을 허용했는지 확인한다. 퍼미션 요청 다이얼로그가 닫힐 때 자동으로 호출되며, 다이얼로그에서 요청한 퍼미션의 결과값이 grantResults 매개변수로 전달된다. |
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 사용자가 퍼미션 허용했는지 확인
val status = ContextCompat.checkSelfPermission(this, "android.permission.READ_CONTACTS")
if (status == PackageManager.PERMISSION_GRANTED) {
Log.d("test", "permission granted")
} else {
// 퍼미션 요청 다이얼로그 표시
ActivityCompat.requestPermissions(this, arrayOf<String>("android.permission.READ_CONTACTS"), 100)
Log.d("test", "permission denied")
}
}
// 다이얼로그에서 퍼미션 허용했는지 확인
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d("test", "permission granted")
} else {
Log.d("test", "permission denied")
}
}
}
5. 결과를 받기 위한 액티비티(외부 앱) 실행 코드 작성
startActivityForResult() 메서드는 @Deprecated 되었기 때문에 ActivityResultLauncher를 사용하는 Activity Result API를 사용해서 주소록 앱을 실행해준다.
(※참고 : 액티비티에서 결과 가져오기, https://developer.android.com/training/basics/intents/result?hl=ko)
주소록 앱에서 연락처를 선택하면 자동으로 메인 액티비티로 돌아오며, 주소록 앱에서 되돌려준 값은 registerForActivityResult() 함수로 등록된 결과 콜백으로 받아 처리한다.
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var requestLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// ActivityResultLauncher 초기화, 결과 콜백 정의
requestLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// 주소록 상세 정보 가져오기
}
binding.button.setOnClickListener {
// 주소록 앱 실행
val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
requestLauncher.launch(intent)
}
}
// ...
}
registerForActivityResult()의 두 번째 매개변수 람다신인 결과 콜백의 단 하나뿐인 매개변수가 ActivityResult타입, 이 객체의 data 프로퍼티가 Intent이고, Intent 객체의 data 프로퍼티가 Uri 객체이다. 즉, Uri 객체에 접근하기 위해서는 it.data!!.data!! 와 같이 들어가야 한다.
6. 주소록 앱에서 전달한 데이터로 Content Provider 쿼리 실행
주소록 앱에서 연락처를 선택해 반환되는 데이터는 Uri 객체뿐이다. 즉 이 Uri객체에는 선택된 사용자의 식별자만이 path형태로 담겨 있는데(content://com.android.contacts/data/3), 이 식별자(3)를 사용해서 DB에서 사용자 정보를 조회하듯 콘텐츠 프로바이더로 쿼리를 날려야 그 사용자의 상세 정보(이름, 전화번호, 이메일 주소 등..)를 얻을 수 있다.
시스템에 등록된 콘텐츠 프로바이더를 사용할 땐 ContentResolver 객체를 이용하며, 데이터 조회는 이 객체의 query() 메서드로 할 수 있다.
public final Cursor query(
Uri url,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder
)
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var requestLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
// ...
requestLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val cursor = contentResolver.query(
it.data!!.data!!,
arrayOf<String>(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
),
null,
null,
null
)
Log.d("test", "cursor size : ${cursor?.count}")
if (cursor!!.moveToFirst()) {
val name = cursor.getString(0)
val phone = cursor.getString(1)
binding.textView.text = "name: $name, phone: $phone"
}
}
}
binding.button.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
requestLauncher.launch(intent)
}
}
// ...
}
7. 앱 실행 결과
문제 상황
ContentProvider.query() 결과로 반환되는 Cursor 객체가 null(=Empty)은 아니지만, 아무 연락처 정보를 가져오지 못하고, Cursor객체의 count 속성도 0으로 나온다. Uri 객체 값을 확인해보면 연락처가 선택은 되었으나, 주소록 앱에서 메인 액티비티로 아무 정보도 가져오기 못한 상황. 이 때문에 cursor.moveToFirst() 메서드가 계속 false를 반환한다.
주소록에 존재하는 연락처를 선택했으니, Cursor의 count 속성이 아래처럼 1로 나와야 한다.
해결
다른 문제 때문일 수도 있으나, 내 경우에는 사용자 퍼미션을 매니페스트에 선언만 해두고, 실제 다이얼로그로 요청하지 과정을 구현하지 않아 주소록 앱 읽기 권한이 거부된 상태가 원인이었다.
즉, 위에서 "3. [MainActivity.kt] 파일에 퍼미션 허용 확인 코드 작성" 과정을 처음에는 빼먹었기 때문.
주소록 앱 접근 권한이 거부 상태였기 때문에 실제 연락처 데이터를 가져오지 못한 것이다.
3과정을 진행해 퍼미션 요청 코드를 작성하고 앱을 재실행하면 아래와 같이 "Contacts", "Phone" 에 대한 퍼미션 요청 다이얼로그가 뜰 텐데, 둘 모두 [Allow]를 눌러서 허용해주자.
Contacts 앱에 대한 퍼미션 허용 여부는 [Settings] > [Apps & Notifications] 메뉴 에서도 확인할 수 있다.
참고자료
- Do it! 깡샘의 안드로이드 프로그래밍 with 코틀린
- Android Developers - Contacts Provider, https://developer.android.com/guide/topics/providers/contacts-provider?hl=ko
- Android Developers - Contacts Provider Retrieve details, https://developer.android.com/training/contacts-provider/retrieve-details?hl=ko