안녕하세요. 이번에는 DangerousPermission 중 READ_CONTACTS와 WRITE_CONTACTS의 권한을 획득하여 주소록 앱을 만들어보고자 합니다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.jdroid.contactsex">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<application
...
</application>
</manifest>
먼저 AndroidManifest에서 READ_CONTACTS, WRITE_CONTACTS 권한을 사용하겠다고 명시를 해줍니다.
layout xml
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f0f0f0"
tools:context=".ui.MainActivity">
<include
android:id="@+id/includeTitle"
layout="@layout/view_title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactsList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/includeTitle" />
<TextView
android:id="@+id/txtDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnPermission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="권한 허용하기"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/txtDescription"
app:layout_constraintStart_toStartOf="@id/txtDescription"
app:layout_constraintTop_toBottomOf="@id/txtDescription" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnAddContacts"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="20dp"
android:background="@drawable/ripple_btn_add_contacts"
android:text="+"
android:textColor="#DDDDDD"
android:textSize="26dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
먼저 MainActivity 디자인 입니다.
- view_title_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layoutTitleBar"
android:layout_width="match_parent"
android:background="#ffffff"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="12dp"
android:layout_marginStart="20dp"
android:textColor="#333333"
android:textSize="24dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:padding="10dp"
android:text="삭제"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#11000000"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
view_title_bar.xml은 각 액티비티에 들어가게 될 타이틀바의 디자인입니다.
- view_contacts_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ripple_bg_list">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/txtName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxEms="5"
android:maxLines="1"
android:minEms="5"
android:paddingVertical="4dp"
android:text=""
android:textColor="#333333"
android:textSize="18dp"
android:textStyle="bold"
app:autoSizeMinTextSize="14dp"
app:autoSizeTextType="uniform" />
<TextView
android:id="@+id/txtPhoneNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="16dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="20dp"
android:background="#10000000"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
view_contacts_list.xml은 메인 액티비티의 RecyclerView에서 사용될 레이아웃으로 연락처 목록을 나타내 줄 것입니다.
MainActivity.kt
class MainActivity : AppCompatActivity(), View.OnClickListener {
companion object {
const val PERMISSION_REQUEST_CODE = 100
}
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private var contactsAdapter: ContactsAdapter? = null
private var contactsList = ArrayList<ContactsData>()
...
override fun onClick(v: View?) {
when (v) {
binding.btnPermission -> {
requestPermission()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initLayout()
initListener()
onCheckContactsPermission()
}
override fun onResume() {
super.onResume()
onCheckContactsPermission()
}
private fun initLayout() {
setContentView(binding.root)
binding.includeTitle.txtTitle.text = "주소록"
}
private fun initListener() {
binding.btnPermission.setOnClickListener(this)
binding.btnAddContacts.setOnClickListener(this)
binding.includeTitle.btnDelete.setOnClickListener(this)
}
...
}
MainActivity의 일부 코드입니다.
처음부터 하나하나 살펴보도록 하겠습니다. 먼저 처음에 onCreate에서 initLayout(), initListener(), onCheckContactsPermission() 메서드를 순서대로 실행합니다.
initLayout에서는 MainActivity의 View를 지정해주고 include 했던 titleBar의 TitleText를 지정해줍니다.
initListener에서는 뷰들의 Listener를 지정해줍니다.
그리고 다음에 onCheckContactsPermission() 메서드를 실행해줍니다.
private fun onCheckContactsPermission() {
val permissionDenied = checkSelfPermission(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_DENIED
|| checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED
binding.btnPermission.isVisible = permissionDenied
binding.txtDescription.isVisible = permissionDenied
binding.btnAddContacts.isVisible = !permissionDenied
binding.contactsList.isVisible = !permissionDenied
if (permissionDenied) {
binding.txtDescription.text = "권한을 허용하셔야 이용하실 수 있습니다."
} else {
getContactsList()
}
}
onCheckContactsPermission의 코드입니다.
onCheckContactsPermission에서는 checkSelfPermission을 통하여 READ_CONTACTS, WRITE_CONTACTS의 Permission을 확인하여 권한이 없다면 btnPermission과 txtDescription을 보여주고 txtDescription의 text를 설정하며, 반대로 권한이 있다면 btnAddContacts와 contactsList를 보여주고 getContactsList() 메서드를 실행하게 됩니다.
권한이 없는 상태에서 btnPermission 버튼을 클릭하게 되면 requestPermission() 메서드를 실행합니다.
private fun requestPermission() {
requestPermissions(arrayOf(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS), PERMISSION_REQUEST_CODE)
}
이 메서드에서는 requestPermissions(permissions, requestCode)를 통해 사용자에게 아래와 같이 권한을 요청합니다.
이렇게 사용자에게 요청하고 나면 허용을 했는지 거절했는지 알아야 합니다. 권한을 설정했을 때 실행되는 콜백이 있습니다.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (checkSelfPermission(permissions[0]) == PackageManager.PERMISSION_GRANTED) {
onCheckContactsPermission()
} else {
if (shouldShowRequestPermissionRationale(permissions[0])) {
binding.txtDescription.text = "권한이 거절되었습니다."
} else {
AlertDialog.Builder(this).apply {
setTitle("권한")
setMessage("권한을 허용하기 위해서 설정으로 이동합니다.")
setPositiveButton("확인") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
setNegativeButton("거절") { dialog, _ ->
dialog.dismiss()
}
show()
}
}
}
}
권한을 허용/거절하게 되면 이 onRequestPermissionsResult에 콜백이 들어오게 됩니다. 여기서 checkSelfPermission을 통해 권한이 허용되었는지 확인을 하여 권한이 허용이 되었다면 onCheckContactsPermission()을 통해 View들을 재설정해주고 getContacts()를 실행하게 됩니다. 하지만 거절하였다면 souldShowRequestPermissionRationable()을 통해 if문을 분기해 줬는데 이 함수는 사용자에게 권한을 허용하도록 나타낼 수 있는지 여부를 확인하는 것입니다. 이게 무엇이냐면 권한을 거절하게 되면 다시 묻지 않음을 선택할 수 있는 화면이 아래와 같이 나타나게 되는데 이때 다시 묻지 않음을 선택하게 된다면 requestPermissions를 실행한다고 하여도 권한 획득 팝업이 나타나지 않게 됩니다.
그래서 shouldShowRequestPermissionRationable을 이용하여 사용자가 다시 묻지 않음을 체크/선택하였을 경우 AlertPopup을 띄워주고 그냥 거절만 하였을 경우에는 txtDescription에 "권한을 거절되었습니다."를 나타내도록 만들었습니다. AlertPopup에서 확인 버튼을 누르게 되면 내 앱 패키지의 설정으로 갈 수 있도록 intent를 설정해주고 startActivity를 통해 아래와 같이 설정으로 이동합니다. 사용자가 설정에서 돌아왔을 때 onResume에서 onCheckedContactsPermission()을 실행하여 권한을 다시 체크합니다.
위와 같은 로직을 통하여 Permission이 설정되었을 경우에는 getContactsList()를 실행하게 됩니다.
private fun getContactsList() {
val contacts = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)
val list = ArrayList<ContactsData>()
contacts?.let {
while (it.moveToNext()) {
val contactsId = contacts.getInt(contacts.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID))
val name = contacts.getString(contacts.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number = contacts.getString(contacts.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
list.add(ContactsData(contactsId, name, number))
}
}
list.sortBy { it.name }
contacts?.close()
if (contactsList != list) {
contactsList = list
setContacts()
}
}
getContactsList는 사용자의 휴대폰에 있는 연락처를 모두 가져옵니다. 이때 사용자 연락처 정보를 담기 위해 Parcelize Data Class인 ContactsData를 생성해야 합니다. Parcelize사용법은 아래의 게시글에서 확인할 수 있습니다.
[Android] Parcelable 생성 Parcelize로 편하게 생성하기(with Serializable)
안녕하세요. 이번에는 Parcelable에 대해 알아보고 Parcelize를 사용하는 방법을 알아볼까 합니다. 오늘 알아볼 Serializable / Parcelable은 데이터들을 직렬화하여 관리하는 인터페이스입니다. Serializable
jdroid.tistory.com
@Parcelize
data class ContactsData(
val contactsId: Int,
val name: String,
val number: String
) : Parcelable
그러면 다시 MainActivity로 돌아와서 contentResolver.query(ContactsContracts.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)을 실행하여 CONTENT_URI의 모든 데이터를 받아오게 됩니다. 그러고 나서 while문을 통하여 모든 데이터에서 ContactId, Name, PhoneNumber를 받아와 list에 저장합니다. 그러고 나서 전역 변수인 contactsList에 받아온 list를 넣어주고 setContacts() 메서드를 실행합니다.
private fun setContacts() {
contactsAdapter = ContactsAdapter(contactsList)
binding.contactsList.adapter = contactsAdapter
}
setContacts 메서드는 불러온 연락처 리스트를 통해 RecyclerView의 어댑터를 만들고, 설정하여 줍니다.
ContactsAdapter.kt
ContactsAdapter는 연락처의 정보를 나타낼 RecyclerView의 어댑터입니다.
class ContactsAdapter(private val contactsList: ArrayList<ContactsData>) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(ViewContactsListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind()
}
override fun getItemCount() = contactsList.size
inner class ViewHolder(private val binding: ViewContactsListBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind() {
binding.txtName.text = contactsList[adapterPosition].name
binding.txtPhoneNumber.text = PhoneNumberUtils.formatNumber(contactsList[adapterPosition].number, Locale.getDefault().country)
}
}
}
넘겨준 데이터를 통해 name과 phoneNumber를 textView에 설정해줍니다.
지금까지 만든 내용의 결과를 보겠습니다.
이번에는 연락처의 권한 획득과 연락처를 불러와서 나타내는 것까지 해보았습니다. 다음에는 연락처를 삭제하고 연락처를 수정하고 추가하는 것을 구현해 보도록 하겠습니다. 감사합니다!!
문제가 있거나 이해가 되지 않는 부분이 있으시면 댓글을 남겨주시면 답변드리겠습니다!
'Android > Libraries' 카테고리의 다른 글
[Android] 안드로이드 Permission(권한) 예제 - 연락처/주소록 만들기 (연락처 삭제) (0) | 2022.08.08 |
---|---|
[Android] 안드로이드 Permission(권한) 예제 - 연락처/주소록 만들기 (연락처 추가/수정) (0) | 2022.08.07 |
[Android] Parcelable 생성 Parcelize로 편하게 생성하기(with Serializable) (0) | 2022.07.24 |
[Android] 안드로이드 Permission(권한) 종류 및 권한 획득 예제(지문인증 / 생체 인증) (0) | 2022.07.11 |
[Android] 안드로이드12 대응 SplashScreen(스플래시 스크린) 만들기 (1) | 2022.07.09 |