Android

[Android/Kotlin] 안드로이드 주소록 앱(Contacts)에서 연락처 상세 정보 가져오기, cursor의 count가 0일 때 or moveToFirst() 실행 안될 때 문제 해결

haehyun 2022. 1. 27. 21:12
728x90

안드로이드 기본 앱 중 하나인 주소록(Contacts) 앱에서 사용자 이름(DISPLAY_NAME), 전화번호(NUMBER) 정보를 가져와 메인 액티비티에 TextView로 표시할 것이다.

1. 메인 액티비티 / 2. 주소록 실행, 선택 / 3. 주소록 데이터 출력

<관련 개념>

  • 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. 앱 실행 결과

 

MainActivity.kt
0.00MB
AndroidManifest.xml
0.00MB
activity_main.xml
0.00MB

 


문제 상황

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

 

728x90