안드로이드 content provider

이영준·2023년 4월 11일
0
post-thumbnail

📌 Content Provider

다른 앱으로 데이터를 제공하고자 할 경우 사용한다.

  • 요청은 Content Resolver 클래스에 의해서 처리됨
  • 다양한 방법으로 데이터를 저장
    • 데이터 베이스, 파일 또는 네트워크를 통해 저장 가능

ContentProvider 만드는 이유

  • 데이터 베이스 위헤 추상화 단계를 한 단계 둠
    • ContentProvider을 접근하는 다른 앱들의 코드 변경없이 개발자가 데이터 저장소에 변경을 줄 수 있음
  • CursorLoader나, CursorAdapter 같은 클래스들이 ContentProvider 사용
  • 개발자들이 접근하고, 사용하고, 수정할수 있게 데이터 저장소를 열어 두기 위함

ContentResolver

  • ContentResolver로 ContentProvider가 어떤 앱과 통신하는지 알고 데이터의 동기화를 유지.
    query, insert, update, delete 의 메서드를 호출하면 contentProvier에게 해당 기능을 수행하라고 알려줌

📌 ContentProvider 사용방법

🔑 query 호출

ContentResolver를 받아오면 해당 레퍼런스를 이용해 query 메서드 호출


URI : 데이터의 위치를 표시하고 데이터를 가져오기 위해 사용
다른 Query Prameter

  • projection : 칼럼 필터링 (select 할 칼럼)
  • selection : 행 필터링 방법 설명 (where 절 -> id=? and passwd=?)
  • selection arguments : 필터링 대상
  • sort order : 데이터 정렬 순서

🔑 종류

  • 나머지 외부 사진,음악, 영상,
  • 통화 이력, 연락처
  • 사용자 정보
  • 북마크
  • 검색 문자열
    등은 권한이 필요하다.

🔑 ContentResolver 구현

//        var uri = "content://com.android.contacts/contacts/1"
        var uri = ContactsContract.Contacts.CONTENT_URI.toString() + "1" //1번째 연락처를 가져오기 위해 uri 주소를 만들어줌
        var uri2 = Uri.parse(uri) // string 주소값을 실제 uri 로 바꿈
    
        contentResolver.query(uri2 , null, null, null, null)

URI는 안드로이드 시스템에서 상수로 매핑되어있기 때문에 주석 처리된 문을 사용하나, 아래의 문을 사용하나 같은 의미이다.

쿼리 string을 작성하여 구체적인 데이터 값을 가져와보자

첫번째 데이터를 가져와 이 행 레이블들을 출력해주면
레이블들을 확인해 볼 수 있다.

🔑 구체적인 컬럼명의 값을 가져오는 법

var name = it.getString(it.getColumnIndexOrThrow("display_name"))

getColumnIndexOrThrow 는 해당 열이 없으면 오류를 출력한다.

🔑 content resolver로 받아온 값을 adapter에 넣기

		val URI = ContactsContract.Contacts.CONTENT_URI // 전체 주소록
        val cursor = contentResolver.query(URI, null, null, null, null)
        val from = arrayOf( "_id", "display_name")
        val to = intArrayOf( R.id.id_item, R.id.name_item)

        val adapter1 = SimpleCursorAdapter(
            this,
            R.layout.list_item,
            cursor!!,
            from,
            to,
            FLAG_REGISTER_CONTENT_OBSERVER
        )
        listview.adapter = adapter1

contentResolver로 전체 주소록의 값을 가져왔다. 이는 Cursor 객체로 저장되는데,

CursorAdapter를 사용하면 (예시에서는 cursorAdapter을 상속받는 simpleCursorAdapter 사용) 아이템을 넣을 레이아웃, 커서, 넣을 컬럼명, 넣어질 레이아웃 안의 뷰 값, observer 값을 넣어주어서 adapter를 만들 수 있다.
FLAG_REGISTER_CONTENT_OBSERVER
콘텐트 프로바이더의 내용이 바뀔 때마다(데이터 변경이 있을 때마다) 콜백 메서드로 onContentChanged() 를 호출한다.

🔑 바뀐 커서 테이터에 대해서 adapter에 적용

simpleCursorAdapter 대신에 이를 상속받는 MySimpleCursorAdapter를 만들었다.

    //상속해서 구현하면 주소록 변경시 onContentChanged 콜백됨.
    inner class MySimpleCursorAdapter(
        context: Context, layout:Int, cursor:Cursor, from:Array<String>, to:IntArray, flag:Int
    ) : SimpleCursorAdapter(context, layout, cursor,from, to, flag){
        override fun onContentChanged() {
            super.onContentChanged()
            Log.d(TAG, "onContentChanged: ")
//            cursor.requery()

            val newCursor = contentResolver.query(URI, null, null, null, null)
            swapCursor(newCursor)
        }
    }

onContentChanged를 오버라이딩 하면 FLAG_REGISTER_CONTENT_OBSERVER 로 설정한 adapter가 content provider를 통해 받아오는 주소값이 변경되었을 때 onContentChanged 를 불러 오버라이딩 한 대로 adapter를 재구성 해준다.

🔑 query에 정렬과 건수제한 등의 처리

        //android version 대응. 10이상.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //정렬 & 건수제한

            //bundle은 쿼리의 개수 제한, 순서 등등의 queryArgs 를 담아준다
            val bundle = Bundle().apply {
                putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS,  arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN))
                putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
                putInt(ContentResolver.QUERY_ARG_OFFSET, 0)
                putInt(ContentResolver.QUERY_ARG_LIMIT, 3)


            }
            return resolver.query(queryUri, what, bundle, null)

10 버전 이상부터는 bundle에 속성을 넣은 다음 쿼리 문에 담는 식으로 처리한다.

또한 커서에서 행을 하나하나 탐색할 때는 일반적으로 가장 첫번째 인덱스로 간다음 계속 인덱스가 있을 때까지 moveNext로 다음 인덱스로 접근한다. 이를 위해 do-while 패턴을 주로 사용한다.

                do {
                    //열 인덱스로 데이터 구하기.
                    val id = cursor.getLong(idColNum)
                    val title = cursor.getString(titleColNum)
                    val dateTaken = cursor.getLong(dateTakenColNum)
                    val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
                    Log.d(TAG, "imageUri: ${imageUri}") //  content://media/external/images/media/31
                    Log.d(TAG, "ISO: ${cursor.getString(4)}")
                    val text = DateFormat.format("yyyy/MM/dd (E) kk:mm:ss", Date(dateTaken)).toString()

                    //View에 데이터 세팅
                    var textId = getResources().getIdentifier("textView_" + (++index), "id", "${packageName}");
                    var textView = findViewById<TextView>(textId);

                    var imgId = getResources().getIdentifier("imageView_" + index, "id", "${packageName}");
                    var imgView = findViewById<ImageView>(imgId);

                    textView.text = "촬영일시: $text"
                    imgView.setImageURI(imageUri)
                } while (cursor.moveToNext())

📌 UriMatcher

Contentprovider에서 쿼리를 호출할 때 URI를 구성한다. 이 때 마지막 분이 /notes notes/3 일 때 실행문이 다를 것이다.

/notes: 전체 조회
notes/3: 단건 조회

이에 대하여 uri의 생김새로 분기를 태워주도록 하는 것이 uriMatcher이다.

    private var sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply{
        addURI(Notes.AUTHORITY, "notes", NOTES)
        addURI(Notes.AUTHORITY, "notes/#", NOTE_ID)
    }
override fun getType(uri: Uri): String? {
        return when (sUriMatcher.match(uri)) {
            NOTES -> CONTENT_TYPE
            NOTE_ID -> CONTENT_ITEM_TYPE
            else -> throw IllegalArgumentException("Unknown URI $uri")
        }

    }
profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글