Android 콘텐츠 프로바이더

timothy jeong·2021년 11월 17일
0

Android with Kotlin

목록 보기
47/69

앱의 데이터는 그 앱의 구성 요소에서 이용할 때는 문제가 없지만 외부 앱에서는 보안상의 이유로 접근하지 못한다. 그렇지만 앱을 만들다보면 공유해야하는 데이터도 있기 마련이다. 대표적인게 휴대폰에 저장된 주소록 데이터이고, 카메라로 촬영한 사진등이 있다.

이런 앱 사이에 데이터 연동 문제를 해결해주는 컴포넌트가 콘텐츠 프로바이더이다. 내가 만든 앱의 데이터를 공개하려고 할때도, 다른 앱이 공개해놓은 데이터에 접근하려고할때에도 모두 콘텐츠 프로아비더를 이용해야한다.

콘텐츠 프로바이더 작성하기

콘텐츠 프로바이더는 ContentProvider 클래스를 상속받아서 작성한다.

class MyContentProvier : ContentProvier() {

     // 콘텐츠 프로바이더의 생명주기 함수
    override fun onCreate(): Boolean {
        TODO("Not yet implemented")
    }
    
    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")
    }

    // 이하는 외부앱에서 호출하여 데이터를 조작하는 함수
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Not yet implemented")
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        TODO("Not yet implemented")
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        TODO("Not yet implemented")
    }
}

이 코드에서 delete 와 update 가 있으므로, 단순히 데이터 조회 그 이상의 것을 해야만하는 건가? 생각할 수 있다. 하지만 해당 함수의 기능은 개발자가 마음대로 만들 수 있으므로, 이는 개발자의 재량에 달려있다.

콘텐츠 프로바이더로 컴포넌트이므로 메니페스트에 등록해야한다. 다른 컴포넌트와는 달리 authorities 속성을 반드시 선언해줘야한다. 이 속성은 외부에서 이 콘텐츠 프로바이더를 이용할 때 식별값으로 사용되는 문자열이다. 따라서 authorities 는 개발자가 지정하는 고유값이어야 한다.

        <provider
            android:authorities="com.example.test_provider"
            android:name=".MyContentProvider"
            android:enabled="true"
            android:exported="true"/>

콘텐츠 프로바이더 이용하기

콘텐츠 프로바이더는 인텐트와 상관이 없다. 콘텐츠 프로바이더는 필요한 순간에 시스템에서 자동으로 생성해 주므로 query(), insert(), update(), delete() 함수만 호출해 주면된다.

외부 앱에서 콘텐츠 프로바이더를 사용하려면 먼저 메니페스트에 해당 앱에 관한 패키지 공개 설정을 해줘야한다. 아래의 예시에서 나온대로 authorities 를 명시하거나, 패키지를 명시하는 방법 둘 중 하나만 하면 된다.

<!-- 둘중 하나만 해도 됨 -->
<queries>
  <provider android:authorities="com.example.test_provider" />
  <package android:name="com.example.test_provider"/>
</queries>

위와 같이 메니페스트에 등록함으로써 시스템에 콘텐츠 프로바이더가 등록이 됐다. 그리고 시스템에 등록된 콘텐츠 프로바이더를 사용할 때는 ContentResolver 객체를 이용한다.

contentResolver.query(
    Uri.parse("content://com.example.test_provider"),
    null, null, null, null)

그리고 delete, insert, query, update 함수들을 이용할 수 있는데, 이 함수들의 첫번째 매개변수는 대상 콘텐츠 프로바이더를 식별하는 Uri 객체이다. Uri 객체의 URL 문자열은 프로토콜명과 콘텐츠 프로바이더의 식별자로 등록된 authorities 값이어야 한다. 호스트에 지정한 문자열로 식별되는 콘텐츠 프로바디어의 query() 나 insert(), update(), delete() 함수를 호출한다.

content://com.example.test_provider 여기서 content:// 부분이 프로토콜이며, 이하가 호스트에 해당하는 authorities 값이다. 마치 web 의 restAPI 를 이용하는 것과 같다.

똑같이content://com.example.test_provider/user/1 이런식으로 path varibale을 넘겨줄 수 있는데, 실제 프로바이더 코드에서 이러한 값들을 처리하지 않는다면 의미가 없을 것이다.

update(), insert() 함수의 매개변수로 지정되는 ContentValues 는 Map 형태의 집합 객체이다. 이 객체를 매개변수로 넘겨주고 update, insert 를 시행하면 데이터를 저장하거나 수정한다. query() 함수의 반환 타입인 Cursor 도 가져올 데이터의 Map 객체이다.

안드로이드 연락처 앱과 연동하기

직접 안드로이드 주소록 앱과 연동해서 콘텐츠 프로바이더를 이용해보자, 우선 메니페스트에서 퍼미션을 등록해야한다.

<uses-permission android:name="android.permission.READ_CONTACTS"/>

주소록의 목록 화면을 띄우는 코드를 작성하자

class MainActivity : AppCompatActivity() {

    lateinit var resultLauncher : ActivityResultLauncher<Intent>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        resultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()) {
            Log.d("INFO", "RESULT CODE ${it.resultCode}")
            if (it.resultCode == RESULT_OK) {
                val result = it.data?.dataString
                result.let {
                    Log.d("INFO", "DATA : $result")
                }
            }
        }
        
        // 주소록 목록을 띄우도록 한다.
        val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
        resultLauncher.launch(intent)
    }
}

이때 intent 를 만들기 위해 사용된 함수는 public Intent(String action, Uri uri) 이다. action에 해당하는 String 과 가져올 자원 (Uri) 를 지정하여 해당 자원을 가져온다. Uri 객체는 위에서 한것 처럼 Uri.parse() 함수로 직접 지정해도 되지만, 상수를 이용해도 된다.

이 앱을 실행하면 주소록 화면으로 넘어가고, 주소록 중 하나를 클릭하면 그 사항을 로그로 남긴다.

D/INFO: RESULT CODE -1
    DATA : content://com.android.contacts/data/7

주소록에서 전달한 데이터는 URL 문자열 형태이며, URL의 맨 마지막 단어는 사용자가 선택한 사람의 식별자 값이다. 만약 사용자가 선택한 사람의 이름, 전화번호 등을 가져와야 한다면 위의 식별자 값을 조건으로 주소록 앱에 필요한 데이터를 구체적으로 요청하면 된다. 이때 주소록 앱의 콘텐츠 프로바이더를 이용한다.

     resultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()) {
            Log.d("INFO", "RESULT CODE ${it.resultCode}")
            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("INFO","Cursor size : ${cursor?.count}")
                if (cursor!!.moveToFirst()) {
                    val name = cursor.getString(0)
                    val phone = cursor.getString(1)
                    Log.d("INFO", "name : $name \nnumber : $phone")
                }
            }
        }

안드로이드 갤러리 앱과 연동하기

갤러리 앱 연동은 인텐트로 갤러리 앱의 목록 화면을 띄우거나 갤러리 앱의 콘텐츠 프로바이더로 데이터를 가져오는 작업이다.

이미지 작업 시 고려 사항

  • 안드로이드에서 이미지는 Drawable 이나 Bitmap 객체로 표현한다.
  • Bitmap 객체는 BitmapFactory로 생성한다.
  • BitmapFactory로 이미지를 생성할 때는 OOM 오류를 고려해야 한다.
  • Glide 나 Pocasso 같은 이미지 처리 라이브러리를 이용하는 것이 효율적일 수 있다.

Drawable 은 주로 리소스 이미지를 표현할 때, Bitmap은 파일에서 읽은 이미지나 네트워크에서 내려받은 이미지를 표현할 때 사용한다. Bitmap 과 Drawable 은 서로 호환하므로 Drawable 타입의 이미지를 Bitmap 타입으로, 또는 그 반대로 바꿀 수 있다.

Bitmap 이미지는 Bitmapfactory 클래스의 decode 로 시작하는 함수들로 생성한다.

  • BitmapFactory.decodeByteArray() : byte[] 배열의 데이터로 비트맨 생성
  • BitmapFactory.decodeFile() : 파일 경로를 매개변수로 지정하면 그 파일에서 데이터를 읽을 수 있는 FileInputStream 을 만들어 decodeStream() 함수 이용
  • BitmapFactory.decodeResource() : 리소스 이미지로 비트맵 생성
  • BitmapFactory.decodeStream() : InputStream으로 읽은 데이터로 비트맵 생성

OOM(out of memory) 오류

BitmapFactory 를 이용하여 서버에서 내려받거나 카메라로 찍은 사진 같이 크기가 큰 이미지를 불러올때 OOM 오류가 발생할 수 있다. 앱의 메모리가 부족하다는 것인데, 이미지의 크기를 줄여주면 해결된다.


// 옵션을 지정하지 않고 비트맵 생성, OOM 오류 발생 가능
val bitmap = BitmapFactory.decodeStream(inputStream)

// 옵션을 지정
val option = BitmapFactory.Options()
option.sinSampleSize = 4
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)

sinSampleSize 에 값을 적용하면 값만큼의 비율로 데이터를 줄여서 불러온다. 2048x1536(12MB) 이미지를 불러오면 512x384(0.75MB)의 비트맵으로 생성해준다.

갤러리 앱 연동 방법

인텐트로 갤러리 앱의 사진 목록을 띄우고 사용자가 선택한 사진을 읽어 화면에 출력하는 방법을 알아보자.

먼저 인텐트로 갤러리 앱의 사진 목록을 출력하는 코드를 다음과 같이 작성하자.
AVD 에서 구글에서 아무 이미지나 다운받고, 이 앱을 실행해보면 갤러리에서 사진 목록을 확인할 수 있다.

class MainActivity : AppCompatActivity() {

    lateinit var resultLauncher : ActivityResultLauncher<Intent>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        resultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()) {
            Log.d("INFO", "RESULT CODE ${it.resultCode}")
            
        }

        val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        intent.type = "image/*"
        resultLauncher.launch(intent)
    }
}

이제 이 갤러리 앱에서 유저가 클릭한 사진을 우리의 액티비티에 출력해보자.
우선, OOM 오류를 막기 위한 장치를 만들자, Bitmap 이미지의 크기를 최적화 시켜주는 함수가 있으면 좋지 않알까?

    private fun optimizeInSampleSize(fileUri : Uri, reqWidth: Int, reqHeight: Int) : Int {
    // (1) 요청한 이미지의 크기를 확인한다.
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true  // 실제 이미지 객체가 만들어지지는 않고, 설정값만 불러오도록 한다.
        try {
            var inputStream = contentResolver.openInputStream(fileUri)
            BitmapFactory.decodeStream(inputStream, null, options) // 각종 이미지 정보가 옵션에 설정된다.
            inputStream!!.close()
            inputStream = null
        } catch (e : Exception) {
            e.printStackTrace()
        }
    // (2) 요구되는 이미지 크기 (reqWidth, reqHeight) 에 부합할때까지 inSampleSize 를 조정한다.
        val (height: Int, width: Int) = options.run { outHeight to outWidth } // outHeight, outWidth 는 options 객체에 설정된 Bitmap 이미지의 높이와 너비
        var inSampleSize = 1

        while (height / inSampleSize > reqHeight ||
            width / inSampleSize > reqWidth) {
            inSampleSize++
        }
        return inSampleSize
    }

이제 갤러리 앱에 이미지를 요청하도록 하면 된다.

        resultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == RESULT_OK) {
                try {
                    val calRatio = optimizeInSampleSize(it.data!!.data!!,
                    resources.getDimensionPixelSize(R.dimen.imgSize),
                    resources.getDimensionPixelSize(R.dimen.imgSize))
                    val option = BitmapFactory.Options()
                    option.inSampleSize  = calRatio

                    var inputStream = contentResolver.openInputStream(it.data!!.data!!)
                    val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
                    inputStream!!.close()
                    inputStream = null
                    bitmap?.let {
                        binding.galleryResult.setImageBitmap(bitmap)
                    } ?: let{
                        Log.d("INFO", "bitmap null")
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }

지도앱 연동하기

앱이 위도와 경도의 값을 가지고 있다면 지도 앱을 연동해 위치를 보여줄 수 있다. 지도 앱을 연동할 때는 인텐트의 액션 문자열을 Intent.ACTION_VIEW 로 지정한다.

val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.560648,127.038893"))
startActivity(intent)
profile
개발자

0개의 댓글