본문 바로가기

안드로이드/안드로이드 개인공부

[안드로이드 개인공부] ContentProvider로 이미지 가져오기 구현하기

ContentProvider앱 사이에서 각종 데이터를 공유할 수 있게 해주는 컴포넌트이다.

안드로이드 표준 시스템에서는 연락처인 Contacts나 이미지나 동영상 등의 데이터를 보관하는 MediaStore등이 있다.

 

데이터를 검색, 추가, 갱신, 삭제할 수 있으며, 주로 SQLITE 등의 관계형 데이터 베이스 이용을 염두에 두고 설계되었다.

ContentProvider로 부터 데이터를 읽어오자.

ContentProvider로 부터 데이터를 읽어오려면 해당 ContentProvider가 어디에 있는지 알아야한다.

경로는 'content://스키마'를 가진 URI(Universal Resource Indentifier)로 지정되고, 일반적으로 접근할 대상 앱에서 정의가 된다.

 

이 URI는 authority로 불리고, ContentProvider를 직접 만들 때는 AndroidManifest.xml에 기술하게 된다.

ContentResolver를 통해서 데이터를 읽는다.

ContentProvider가 제공하는 데이터에는 ContentResolver를 통해서 접근하도록 설계되어 있다.

ContentResolver의 인스턴스는 getContentResolver() 메소드로 가져오게 된다.

 

val cr: ContentResolver = contentResolver // getContentResolver()

 

ContentResolver에 URI를 전달함으로써 ContentProvider의 데이터에 접근할 수가 있다.

ContentResolver.query()를 통해 가져오게 된다.

 

public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return query(uri, projection, selection, selectionArgs, sortOrder, null);
    }

 

query 메서드의 인수는 조금 복잡하므로 간단하게 어떤 역할을 하는지 설명하겠다.

1. uri :  ContentProvider가 관리하는 uri

2. projection : select에 해당된다. (가져오고 싶은 칼럼명)

3. selection : where에 해당된다.(필터링할 칼럼명르 지정)

4. selectionArgs : 프리페어드 스테이트먼트에 해당된다. (selection으로 지정한 칼럼명의 조건을 설정)

5. sortOrder: order by에 해당된다. (정렬하고 싶은 칼럼명을 지정) 

 

query()의 반환값인 Cursor()는 무엇일까?

 

Cursor데이터에 접근하는 포인터이다.

표에서 어떤 행을 가리키는지 나타내는 것이 Cursor이다.

Cursor의 설명

MediaStore에서 이미지를 가져오기

먼저 안드로이드 M 버전(6 이상부터)는 동적으로 권한을 체크해줘야한다.

 

1. 권한 체크 코드 부터 살펴보자.

private fun checkPermission() {
        if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 200)
        } else {
            initView()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            200 -> {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    initView()
                } else {
                    Toast.makeText(this, "스토리지에 접근 권한을 허가해주세요", Toast.LENGTH_SHORT).show()
                    requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 200)
                }
            }
        }
    }

 

이미지를 읽어오기 위해서는 Manifest.permission.READ_EXTERNAL_STORAGE 권한이 필요하다.

checkSelfPermission(권한)을 통해서 권한이 승인되었는지 아닌지 값을 가져오고

PackageManager.PERMISSION_GRANTED 값과 비교하여 승인을 체크한다.

 

만약 승인이 되어 있을 시 initView()를 실행시키고,

그렇지 않다면 requestPermissions(String[], Int)를 통해서 승인 요청 팝업을 띄우게 된다.

 

요청 팝업의 응답을 했을 경우 onRequestPermissionResult() 콜백 메소드가 실행되며

grantResult[]으로 승인 상태를 확인할 수 있다.

 

위의 코드는 승인 요청 팝업에서 거절을 누를시 토스트 메시지를 띄우고 다시 요청하게 구현되어 있다.

 

2. 커서를 가져오는 부분 구현하기

private fun getImage(): Cursor {
        val resolver = contentResolver
        var queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

        //가져올 컬럼명
        val what = arrayOf(
            MediaStore.Images.ImageColumns._ID,
            MediaStore.Images.ImageColumns.TITLE,
            MediaStore.Images.ImageColumns.DATE_TAKEN
        )

        //정렬
        val orderBy = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"

        //1건만 가져온다.
        queryUri = queryUri.buildUpon().appendQueryParameter("limit", "1").build()

        return resolver.query(queryUri, what, null, null, orderBy)!!
    }

 

데이터를 읽어오기 위해서는 ContentProvider의 경로가 필요하다고 했다.

queryUri를 통해서 이미지 ContentProvider의 경로를 설정한다.

 

그리고 contentResolver는 ContentProvider가 제공하는 데이터에 접근하여 가져올 수 있다.

우리는 contentResolver를 통해서 접근하기 위해서, 위에 설명과 같이 인수에 대해서 공부했었다.

(uri, projection, selection, selectionArgs, sortOrder)

 

우리는 uriprovider의 경로,

projection아이디, 파일 제목, 날짜를 가져오기위해서 컬럼명을 정하고,

orderBy로 날짜순으로 내림차순하여 1건만 가져오도록하자.

 

위의 코드의 요청 query는 아래와 같을 것이다.

(SELECT _id, title, datetaken FROM images ORDER BY datetaken DESC LIMIT 1)

 

3. 커서를 통해서 데이터를 뷰에 뿌려주기

private fun initView() {
        try {
            val cursor = getImage()
            if (cursor.moveToFirst()) {
                //1. 각 컬럼의 열 인덱스를 취득한다.
                val idColNum = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)
                val titleColNum = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.TITLE)
                val dateTakenColNum =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)

                //2. 인덱스를 바탕으로 데이터를 Cursor로부터 취득하기
                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)

                //3. 데이터를 View로 설정
                val calendar = Calendar.getInstance()
                calendar.timeInMillis = dateTaken
                val text = DateFormat.format("yyyy/MM/dd (E) kk:mm:ss", calendar).toString()

                textView.text = "촬용일시: $text"
                imageView.setImageURI(imageUri)
            }
            cursor.close()
        } catch (e: SecurityException) {
            Toast.makeText(this, "스토리지에 접근 권한을 허가해주세요", Toast.LENGTH_SHORT).show()
            finish()
        }
    }

 

getImage()를 통해서 resolver.query에서 반환된 cursor 객체를 가져온다.

그리고 cursor.moveToFirst()를 호출하여 커서를 맨 앞으로 이동한다.

true가 반환된 경우에만 cursor로 부터 데이터를 가져올수 있다. (false일 경우 데이터가 비어있다는 뜻)

 

여기서 2단계로 나뉠 수가 있는데..

1. 가져오고 싶은 컬럼의 인덱스를 얻는다.

2. 데이터를 가져온다.

 

가져오고 싶은 컬럼의 인덱스를 얻기 위해서는 cursor.getColumnIndexOrThrow(컬럼명)을 통해서 얻어올수 있다.

그리고 그렇게 가져온 인덱스를 cursor.getLong(인덱스) / cursor.getString(인덱스) 등을 통해서 데이터를 취득할 수 있다.

 

마지막으로 다 사용한 cursor는 close()메소드로 해제해주면 끝이난다.

 

MediaStore를 통해서 가져온 이미지

깃허브 전체 코드 보러가기