오늘은 콘텐츠 프로바이더를 이용해 안드로이드 기본 앱인 주소록, 갤러리, 카메라, 지도, 전화 앱과 연동을 해봤다.
<uses-permission android:name="android.permission.READ_CONTACTS"/>
// 주소록에서 한사람 선택후 돌아오면 실행되는 함수 정의
var requestContactsLancher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) Log.d("event", "${it.data?.data}")
}
// 주소록 목록 출력
val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
requestContactsLancher.launch(intent)
갤러리 앱 연동은 인텐트로 갤러리 앱의 목록 화면을 띄우거나 갤러리 앱의 콘텐츠 프로바이더로 데이터를 가져오는 작업이다.
안드로이드에서 이미지는 Drawable(주로 리소스 이미지)이나 Bitmap(파일이나 네트워크에서 읽은 이미지) 객체로 표현한다. 둘은 서로 호환된다.
Bitmap 객체는 BitmapFactory로 생성한다.
Glide나 Picasso 같은 이미지 처리 라이브러리를 이용하는 것이 효율적일 수 있다.
BitmapFactory 클래스의 decode로 시작하는 함수로 bitmap 이미지는 생성된다.
BitmapFactory.decodeByteArray(): byte[] 배열의 데이터로 비트맵 생성
BitmapFactory.decodeFile(): 파일 경로를 매개변수로 지정하면 그 파일에서 데이터를 읽을 수 있는 FileInputStream을 만들어
decodeStream() 함수 이용
BitmapFactory.decodeResource(): 리소스 이미지로 비트맵 생성
BitmapFactory.decodeStream(): InputStream으로 읽은 데이터로 비트맵 생성
BitmapFactory로 이미지를 생성할 때는 OOM 오류를 고려해야 한다.
이는 크기가 큰 이미지를 불러올 떄 앱의 메모리 부족 에러이다.
이를 방지하기 위해 옵션으로 이미지 크기를 줄이면 된다.
val option = BitmapFactory.Options()
option.inSampleSize = 4 // inSampleSize의 비율 만큼 용량 절감
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
requestGalleryLauncher. launch(intent)
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) // 이미지 정보들이 option에 설정
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
}
val requestGalleryLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult())
{
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()
}
}
}
카메라 앱으로 사진 촬영 후 파일로 저장하지 않고 데이터만 넘겨주는 방식.
쉽게 구현할 수 있지만 데이터의 크기가 작다는 단점이 있다.
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
requestCameraThumbnailLauncher. launch(intent)
requestCameraThumbnailLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()){
val bitmap = it?.data?.extras?.get("data") as Bitmap
}
카메라 앱에서 촬영한 사진을 저장한 후 성공, 실패만 넘겨주는 방식
카메라 성능만큼 큰 크기의 사진을 촬영해 앱에서 이용가능 하지만 몇가지 준비작업을 해야한다.
앱에서 외장 메모리에 파일을 만들려면 getExternalStoragePublicDirectory() 또는 getExternal FilesDir() 함수를 이용해야한다.
이때 API 레벨 19 하위버전에서는 아래의 퍼미션이 필요하다.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
API 24버전 부터는 file:// 프로토콜로 구성된 URI를 외부에 노출할 수 없다. 따라서 content:// 프로토콜 이용 후 여기에 임시로 접근할 수 있는 권한을 부여해야하는데, FileProvider 클래스를 이용한다.
FileProvider 클래스는 androidx 라이브러리에서 제공하며 XML 설정을 기반으로 해서 content:// 프로토콜로 구성된 URI를 생성한다.
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="myfiles" path="Android/data/com.example.test16/files/Pictures" />
</paths>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.test16.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
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 photoURI: Uri = FileProvider.getUriForFile(this, "com.example.test16.fileprovider", file)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
requestCameraFileLauncher.launch(intent)
requestCameraFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val option=BitmapFactory.Options()
option.inSampleSize = 10
val bitmap = BitmapFactory.decodeFile(filePath, option)
bitmap?.let {
binding.cameraFileResult.setImageBitmap(bitmap)
}
}
앱이 위도와 경도 값을 가지고 있다면 지도 앱을 연동해 위치를 보여줄 수 있다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo: 37.123456,126.977123"))
startActivity(intent)
전화를 거는 기능은 전화 앱의 액티비티에 이미 구현되어 있으므로 전화 앱과 연동해 전화번호 데이터만 넘겨주면 된다.
• 전화를 거는 퍼미션을 매니페스트에 설정
<uses-permission android:name="android.permission.CALL_PHONE" />
• 전화 앱을 실행하는 인텐트
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:02-123"))
startActivity(intent)