[kotlin] 콘텐츠 프로바이더 컴포넌트

ywown·2021년 11월 9일
0

코틀린 개념

목록 보기
4/8

『Do it! 깡샘의 안드로이드 앱 프로그래밍 with 코틀린』 교재를 바탕으로 정리한 내용입니다.

1. 콘텐츠 프로바이더

콘텐츠 프로바이더란?

앱끼리 데이터를 연동하는 컴포넌트

(어떤 앱의 데이터를 다른 앱에서 이용할 수 있게 하려면 콘텐츠 프로바이더를 이용)

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

  • ContentProvider 클래스 상속 받아 작성
  • onCreate(), getType, query(), insert(), update(), delete() 함수 재정의해 사용 → 외부 요청으로부터 허용하지 않으려면 함수 내부를 구현하지 않으면 됨 ❗
    		
    class MyContentProvider : ContentProvider() {
    	override fun onCreate(): Boolean {
    		return false
    	}
    
    	override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
    		return 0
    	}
    
    	override fun getType(uri: Uri): String? {
    		return null
    	}
    
    	override fun insert(uri: Uri, values: ContentValues?): Uri? {
    		return null
    	}
    
    	override fun query(
    		uri: Uri,
    		projection: Array<out String>?,
    		selection: String?,
    		selectionArgs: Array<out String>?,
    		sortOrder: String?
    	): Cursor? {
    			return null
    	}
    
    	override fun update(
    		uri: Uri,
    		values: ContentValues?,
    		selection: String?,
    		selectionArgs: Array<out String>?
    	): Int {
    			return 0
    	}
    }
  • 콘텐츠 프로바이더는 안드로이드 컴포넌트이기 때문에 메니페스트에 등록해줘야 함
  • name 속성뿐만 아니라 authorities 속성도 반드시 선언! → authorities 속성은 외부에서 해당 콘텐츠 프로바이더를 이용할 때의 식별값을 의미
    <provider
    	android:authorities="com.example.test_provider"
    	android:name=".MyContentProvider"
    	android:enabled="true"
    	android:exported="true"></provider>

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

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

  • 외부 앱에서 사용하기 위해 메니페스트에 해당 앱에 대한 패키지 공개 설정

    <queries>
    	<!--  둘 중 하나만 선언. -->
    	<!--  <provider android:authorities="com.example.provider" />. -->
    	<package android:name="com.example.test_outter" />
    </queries>
  • 등록된 콘텐츠 프로바이더를 사용할 때에는 ContentResolver 객체 이용
    contentResolver.query(
    	Uri.parse("content://com.example.test_provider"), null, null, null, null
    )
  • 데이터 조작 함수
    • public final int delete(Uri url, String where, String[] selectionArgs)

    • public final Uri insert(Uri url, ContentValues values)

    • public final Cursor query(Uri url, String[] projection, String selection)

    • public final int update(Uri url, ContentValues values, String where, String[] selectionArgs)

      → 첫 번째 매개변수 Uri 객체 : 프로토콜명과 콘텐츠 프로바이더 식별자로 등록된 authorities 값이어야 함

      → inset()함수와 upddate() 함수의 매개변수인 ContentValues는 Map 형태의 집합 객체

  • URL 구성
    예) content://com.example.test_provider/user/1 프로토콜(content://) + 호스트(com.example.test_provider) + 경로 (/user/1) → user 데이터에서 1번으로 식별되는 데이터를 가져오겠다는 의미
    • 경로는 생략 가능
    • 경로는 단어로 끝나면 그 단어에 해당하는 모든 데이터를 의미하고, 숫자로 끝나면 그 숫자로 식별되는 데이터를 의미하는 조건으로 사용함

2. 안드로이드 기본 앱과의 연동

주소록 앱 연동

  • 퍼미션 설정
    <uses-permission android:name="android.permission.READ_CONTACTS" />
  • 주소록 목록 출력
    val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
    getResult.launch(intent)

→ Uri 객체는 Uri.parse() 함수로 직접 지정하거나 아래와 같은 상수 이용

- ContactsContract.Contacts.CONTENT_URI : 모든 사람의 데이터
- ContactsContract.CommonDataKinds.Phone.CONTENT_URI : 전화번호가 있는 사람
- ContactsContract.CommonDataKinds.Email.CONTENT_URI : 이메일 정보가 있는 사람

  • 사용자가 한 사람 선택했을 때 실행될 함수
    • onActivityResult() 함수에서 세 번째 매개변수인 Intent 객체의 getDataString() 함수로 주소록 앱에서 전달한 결과 데이터 가져옴
    • 콘텐츠 프로바이더로 필요한 데이터 요청
    private val getResult = 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("kkang", "cursor size....${cursor?.count}")
    		if (cursor!!.moveToFirst()) {
    			val name = cursor?.getString(0)
    			val phone = cursor?.getString(1)
    		}
    	}
    }

갤러리 앱 연동

  • 이미지 작업 시 고려 사항
    1. 안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현 (서로 호환됨)
    2. Bitmap 객체는 BitmapFactory로 생성
      • 'decode' 로 시작하는 함수로 생성 BitmapFactory.decodeByteArray() : byte[] 배열의 데이터로 비트맵 생성 BitmapFactory.decodeFile() : 파일 경로를 매개변수로 지정하면 그 파일에서 데이터를 읽을 수 있는 FileInputStream을 만들어 decodeStream() 함수 이용 BitmapFactory.decodeResource() : 리소스 이미지로 비트맵 생성 BitmapFactory.decodeStream() : InputStream으로 읽은 데이터로 비트맵 생성
    3. BitmapFactory로 이미지 생성 시 OOM(Out Of Memory) 오류 고려
      • 크기가 큰 이미지 불러오는 경우 OOM 오류 발생 가능 → BitmapFactory.Option 객체의 inSampleSize 속성 이용해 이미지 크기를 줄여 해결!
        //옵션 지정하지 않고 비트맵 생성
        val bitmap = BitmapFactory.decodeStream(inputStream)
        
        //옵션 지정해 비트맵 생성
        val option = BitmapFactory.Options()
        option.inSampleSize = 4
        val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
  1. Glide나 Picasso 같은 이미지 처리 라이브러리 이용하는 것이 효율적일 수 있음
  • 인텐트로 갤러리 앱의 사진 목록 출력
    val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
    intent.type = "image/*"
    getResult.launch(intent)   //getResult -> 인텐트 콜백 함수명
  • 사용자가 선택한 사진 이미지 크기 줄이기
    • OOM문제 발생하지 않도록 이미지 크기 줄여서 불러와야 함
    • inSampleSize 값 지정 시, 원본 데이터의 크기와 화면에 출력되는 이미지의 크기를 비교해 적절한 비율로 지정하면 더 효율적
    private fun calculateInSampleSize(fileUri: Uri, reqWidth: Int, reqHeight: Int): Int {
    	val options = BitmapFactory.Options()
    	options.inJustDecodeBounds = true   //옵션만 설정하기 위해 true로 지정
    	try {
    		var inputStream = contentResolver.openInputStream(fileUri)
    		BitmapFactory.decodeStream(inputStream, null, options)
    		inputStream!!.close()
    		inputStream = null
    	} catch (e: Exception) {
    		e.printStackTrace()
    	}
    	val(height: Int, width: Int) = options.run { outHeight to outWidth }
    	var inSampleSize = 1
    
    	//inSampleSize 비율 계산
    	if (height > reqHeight || width > reqWidth) {
    		val halfHeight: Int = height / 2
    		val halfWidth: Int = width / 2
    		while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
    			inSampleSize *= 2
    		}
    	}
    	return inSampleSize
    }
  • 이미지 불러오기
    private val getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    	if(it.resultCode == RESULT_OK) {
    		try {
    			//inSampleSize 비율 계산
    			val calRatio = calculateInSampleSize(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("kkang", "bitmap null")
    			}
    		} catch (e: Exception) {
    			e.printStackTrace()
    		}
    	}
    }

카메라 앱 연동

  1. 사진 데이터 가져오는 방법
  • 인텐트로 카메라 앱 촬영 액티비티 실행
    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    getResult.launch(intent)   //getResult -> 인텐트 콜백 함수명
  • 사진 데이터 가져오기
    val bitmap = data?.getExtras()?.get("data") as Bitmap
  1. 사진 파일을 공유하는 방법

    1. 앱에서 사진을 저장할 파일 생성
    2. 사진 파일 정보 포함한 인텐트 전달해 카메라 앱 실행
    3. 카메라 앱으로 사진 촬영 후 공유된 파일에 저장
    4. 카메라 앱 종료하면서 성공 또는 실패 반환
    5. 카메라 앱이 저장한 사진 파일을 앱에서 이용
    • 앱에서 사진을 저장할 파일 생성
      • getExternalStoragePublicDirectory() : 모든 앱에서 이용할 수 있는 파일 생성 → 퍼미션 설정!
      • getExternalFilesDir() : 해당 앱에서만 이용할 수 있는 파일 생성
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

→ API 레벨 24 버전부터는 file:// 프로토콜로 구성된 URI를 외부에 노출하지 못하도록 하는 엄격 모드가 적용됨. 따라서, 앱끼리 파일을 공유하려면 content:// 프로토콜을 이용하고 이 URI에 임시로 접근할 수 있는 권한을 부여해야 함.

→ 이 때, FileProvider 클래스 이용해 XML 설정을 기반으로 content:// 프로토콜 URI를 생성해 줌

  • 파일 프로바이더용 XML 파일 생성 (res/xml)
            <paths xmlns:android="http://schemas.android.com/apk/res/android">
            	<external-path
            		name="myfiles"
            		path="Android/data/com.example.chap16/files/Pictures" />
            </paths>
  • 매니페스트 파일에 파일 프로바이더용 XML 파일 등록
            <provider
            	android:authorities="com.example.chap16.fileprovider"
            	android:name="androidx.core.content.FileProvider"
            	android:exported="false"
            	android:grantUriPermissions="true">
            		<meta-data
            			android:name="android.support.FILE_PROVIDER_PATHS"
            			android:resource="@xml/file_paths"></meta-data>
            </provider>
  • getExternalFilesDir()로 파일 생성
            val timestamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
            val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
            val file = File.createTempFile(
            	"JPEG_${timestamp}_",
            	".jpg",
            	storageDir
            )
            filePath = file.absolutePath
  • 사진 파일 정보 포함한 인텐트 전달해 카메라 앱 실행
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
        getResult.launch(intent)   //getResult -> 인텐트 콜백 함수명
  • 카메라 앱에서 사진 촬영 후 다시 앱으로 돌아왔을 때 실행되는 코드
    • 비트맵 이미지 생성
        val option = BitmapFactory.Options()
        option.inSampleSize = 10
        val bitmap = BitmapFactory.decodeFile(filePath, option)
        bitmap?.let {
        	binding.cameraResult.setImageBitmap(bitmap)
        }

지도 앱 연동

  • 인텐트로 지도 앱 실행
  • URL은 반드시 geo: 로 시작해야 하고, 위도와 경도는 쉼표로 구분해 지정
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.5662952,126.9779451"))
    getResult.launch(intent)  //getResult -> 인텐트 콜백 함수명

전화 앱 연동

  • 인텐트로 전화 앱 실행
  • 전화 거는 기능은 퍼미션 설정해야 함
  • 데이터 정보는 tel: 로 시작해야 하고, 전화번호(수신자) 명시
    //퍼미션 설정
    <uses-permission android:name="android.permission.CALL_PHONE" />
    val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:02-120"))
    getResult.launch(intent)  //getResult -> 인텐트 콜백 함수명
profile
기록

0개의 댓글