[깡쌤의 안드로이드 프로그래밍 with 코틀린][5. 컴포넌트 이해하기] 16. 콘텐츠 프로바이더 컴포넌트
1. 콘텐츠 프로바이더 이해하기
콘텐츠 프로바이더는 앱끼리 데이터를 연동하는 컴포넌트입니다. 예를 들어 앱을 개발하면서 다른 앱의 데이터를 사용할 때 콘텐츠 프로바이더를 이용합니다. 이때 데이터는 대상 앱의 데이터베이스나 파일 또는 앱에 할당된 메모리에 있습니다. 콘텐츠 프로바이더를 이용하면 이런 저장소에 있는 데이터를 가져오거나 수정할 수 있습니다.
앱의 데이터는 그 앱의 구성 요소에서 이용할 때는 문제가 없지만 외부 앱에서는 기본적으로 접근하지 못합니다. 외부 앱에서 마음대로 접근하면 보안 문제가 발생하기 때문입니다. 그렇지만 앱을 만들다 보면 공유해야 하는 데이터도 있기 마련입니다. 예를 들어 휴대폰에 저장된 주소록은 주소록 앱의 데이터고, 카메라로 촬영한 사진은 갤러리 앱에 저장돼 있지만 모두 다른 앱에서 이용할 수 있습니다.
이처럼 어떤 앱의 데이터를 다른 앱에서 이용할 수 있게 하려면 콘텐츠 프로바이더를 이용해야 합니다. 예를 들어 내가 만든 앱의 데이터를 외부에 공개하려면 내 앱에 콘텐츠 프로바이더를 만들고 접근하는 방법을 제공해야 합니다. 그러면 외부 앱에서 콘텐츠 프로바이더를 이용해 공개한 데이터에 접근할 수 있습니다.
콘텐츠 프로바이더 작성하기
콘텐츠 프로바이더는 ContentProvider 클래스를 상속받아서 작성합니다.
class MyContentProvider : ContentProvider() {
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun onCreate(): Boolean {
return false
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
return null
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
return 0
}
}
onCreate 함수는 콘텐츠 프로바이더의 생명주기 함수이며 시스템이 콘텐츠 프로바이더 객체를 생성할 때 자동으로 호출됩니다. 그리고 외부 앱에서 query(), insert(), update(), delete() 함수 등을 호출해 데이터를 조작합니다.
콘텐츠 프로바이더도 안드로이드 컴포넌트이므로 매니페스트에 등록해야 합니다. 그런데 콘텐츠 프로바이더는 다른 컴포넌트와 달리 name 속성뿐만 아니라 authorities 속성도 반드시 선언해야 합니다.
<provider
android:name=".MyContentProvider"
android:authorities="com.example.test_provider"
android:enabled="true"
android:exported="true"></provider>
authorities 속성은 외부에서 이 콘텐츠 프로바이더를 이용할 때 식별값으로 사용되는 문자열입니다. 따라서 authorities 속성값은 개발자가 지정하는 고유한 값이어야 합니다.
콘텐츠 프로바이더 이용하기
콘텐츠 프로바이더는 인텐트와 상관이 없습니다. 콘텐츠 프로바이더는 필요한 순간에 시스템에서 자동으로 생성해 주므로 query(), insert(), update(), delete() 함수만 호출해 주면 됩니다.
외부 앱에서 콘텐츠 프로바이더를 사용하려면 먼저 매니페스트에 해당 앱에 관한 패키지 공개 설정을 해줘야 합니다.
<queries>
<!-- 둘 중 하나만 선언하면 됩니다. -->
<!-- <provider android:authorities="com.example.test_provider" /> -->
<package android:name="com.example.test_outter" />
</queries>
그리고 시스템에 등록된 콘텐츠 프로바이더를 사용할 때는 ContentResolver 객체를 이용합니다.
contentResolver.query(
Uri.parse("content://com.example.test_provider"),
null, null, null, null)
contentResolver 속성으로 ContentResolver 객체를 얻은 후에 데이터를 조작하는 다음과 같은 함수를 호출하면 됩니다.
- public final int delete(Uri uri, String where, String[] selectionArgs)
- public final Uri insert(Uri uri, ContentValues values)
- public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
- public final int update(Uri uri, ContentValues values, String where, String[] selectionArgs)
이 함수들의 첫 번째 매개변수는 대상 콘텐츠 프로바이더를 식별하는 Uri 객체입니다. Uri 객체의 URL 문자열은 프로토콜명과 콘텐츠 프로바이더의 식별자로 등록된 authorities 값이어야 합니다. 호스트에 지정한 문자열로 식별되는 콘텐츠 프로바이더의 query()나 insert(), update(), delete() 함수를 호출합니다.
// 프로토콜(scheme)://호스트(host)
content://com.example.test_provider
콘텐츠 프로바이더를 이용할 때 URL 문자열에서 호스트 뒤에 다음처럼 경로(Path)를 설정할 수도 있습니다. 경로는 선택 사항이지만 이를 이용해 조건을 명시할 수 있습니다. 경로에는 단어나 숫자를 사용하는데 단어로 끝나면 그 단어에 해당하는 모든 데이터를 의미하고 숫자로 끝나면 그 숫자로 식별되는 데이터를 의미하는 조건으로 주로 사용됩니다.
// 프로토콜(scheme)://호스트(host)/경로(path)
content://com.example.test_provider/user/1
물론 콘텐츠 프로바이더를 사용하는 곳에서 경로에 조건을 명시했더라도 실제 이용되는 콘텐츠 프로바이더가 이 경로를 활용하지 않는다면 의미는 없습니다.
update(), insert() 함수의 매개변수로 지정되는 ContentValues는 Map 형태의 집합 객체입니다. 즉, 키-값으로 여러 건의 데이터를 ContentValues에 지정하고 이 객체를 insert(), update() 함수의 매개변수로 넘겨서 데이터를 저장하거나 수정합니다. 그리고 query() 함수의 반환 타입인 Cursor도 가져올 데이터의 Map 객체입니다.
2. 안드로이드 기본 앱과 연동하기
주소록 앱 연동하기
주소록은 가장 많이 이용하는 안드로이드의 기본 앱입니다. 우선 주소록 앱에서 데이터를 가져오려면 다음과 같은 퍼미션을 설정해야 합니다.
<uses-permission android:name="android.permission.READ_CONTACTS" />
그리고 주소록의 목록 화면을 띄우는 코드를 작성합니다.
val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
startActivityForResult(intent, 20)
주소록 목록 화면은 인텐트를 이용해 실행하는데 이 때 액션 문자열은 Intent.ACTION_PICK으로 하고 데이터는 ContractsContract.CommonDataKinds.Phone.CONTENT_URI로 설정합니다. 데이터 정보에 설정한 Uri 객체는 주소록 앱과 연동할 때 이용하는 상수입니다. Uri 객체는 Uri.parse() 함수로 직접 지정해도 되지만 다음과 같은 상수를 이용해도 됩니다.
- ContractsContract.Contacts.CONTENT_URI : 모든 사람의 데이터
- ContractsContract.CommonDataKinds.Phone.CONTENT_URI : 전화번호가 있는 사람
- ContractsContract.CommonDataKinds.Email.CONTENT_URI : 이메일 정보가 있는 사람
이렇게 정의한 인텐트를 startActivityForResult() 함수로 시스템에 전달하면 주소록의 목록 화면이 출력되고, 사용자가 이 목록에서 한 사람을 선택하여 되돌아오면 onActivityResult() 함수가 자동으로 실행됩니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode==20 && resultCode == Activity.RESULT_OK) {
val result: String? = data?.getDataString()
Log.d("kkang", "result : $result")
}
}
onActivityResult() 함수에서 세 번째 매개변수인 Intent 객체의 getDataString() 함수로 주소록 앱에서 전달한 결과 데이터를 가져올 수 있습니다. 이 데이터를 출력해보면 주소록 앱이 전달한 문자열을 확인할 수 있습니다.
content://com.android.contacts/contacts/lookup/0r82-270667DB186FDB1C89D9.3114i12251c758ec39928/1144
주소록에서 전달한 데이터는 URL 문자열 형태이며 URL의 맨 마지막 단어(예에서는 1144) 는 사용자가 선택한 사람의 식별값입니다. 만약 사용자가 선택한 사람의 이름, 전화번호 등을 가져와야 한다면 위의 식별값을 조건으로 주소록 앱에 필요한 데이터를 구체적으로 다시 요청해야 합니다. 이 때 주소록 앱의 콘텐츠 프로바이더를 이용합니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// 주소록
if (requestCode == 20 && resultCode == Activity.RESULT_OK) {
val cursor = contentResolver.query(
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)
...
}
}
}
주소록 앱의 콘텐츠 프로바이더를 이용할 때 가져올 데이터를 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME과 ContactsContract.CommonDataKinds.Phone.NUMBER로 지정했습니다. 이 두 코드는 각각 이름과 전화번호를 의미합니다.
갤러리 앱 연동하기
갤러리 앱을 연동해 사진 이미지를 가져오는 방법도 살펴보겠습니다. 갤러리 앱 연동은 인텐트로 갤러리 앱의 목록 화면을 띄우거나 갤러리 앱의 콘텐츠 프로바이더로 데이터를 가져오는 작업입니다.
이미지 작업 시 고려 사항
- 안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현합니다.
- Bitmap 객체는 BitmapFactory로 생성합니다.
- BitmapFactory로 이미지를 생성할 때는 OOM 오류를 고려해야 합니다.
- Glide나 Picasso 같은 이미지 처리 라이브러리를 이용하는 것이 효율적일 수 있습니다.
안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현합니다. Drawable은 주로 리소스 이미지를 표현할 때, Bitmap 은 파일에서 읽은 이미지나 네트워크에서 내려받은 이미지를 표현할 때 사용합니다. Bitmap과 Drawable은 서로 호환하므로 Drawable 타입의 이미지를 Bitmap 타입으로 또는 그 반대로 바꿀 수 있습니다.
Bitmap 이미지는 BitmapFactory 클래스의 'decode'로 시작하는 다음과 같은 함수로 생성합니다.
- BitmapFactory.decodeByteArray() : byte[] 배열의 데이터로 비트맵 생성
- BitmapFactory.decodeFile() : 파일 경로를 매개변수로 지정하면 그 파일에서 데이터를 읽을 수 있는 FileInputStream을 만들어 decodeStream() 함수 이용
- BitmapFactory.decodeResource() : 리소스 이미지로 비트맵 생성
- BitmapFactory.decodeStream() : InputStream으로 읽은 데이터로 비트맵 생성
BitmapFactory를 이용하면 비트맵을 만들 때 아이콘처럼 작은 이미지를 불러오는 데는 문제가 없지만 크기가 큰 이미지를 불러올 때는 OOM(Out Of Memory) 오류가 발생할 수 있습니다. OOM이란 앱의 메모리가 부족해서 발생하는 오류입니다. 이 오류는 주로 용량이 큰 이미지를 불러올 때 발생하므로 이미지의 크기를 줄이면 해결됩니다.
val option = BitmapFactory.Options()
option.inSampleSize = 4
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
이미지 크기를 줄일 때는 BitmapFactory.Option 객체의 inSampleSize 속성을 이용합니다. Option 객체의 inSampleSize에 값을 적용하면 이 값만큼의 비율로 데이터를 줄여서 불러옵니다.
갤러리 앱 연동 방법
인텐트로 갤러리 앱의 사진 목록을 띄우고 사용자가 선택한 사진을 읽어 화면에 출력하는 방법입니다.
먼저 인텐트로 갤러리 앱의 사진 목록을 출력하는 코드를 다음과 같이 작성합니다. 인텐트의 액션 문자열은 Intent.ACTION_PICK으로, 데이터는 MediaStore.Images.Media.EXTERNAL_CONTENT_URI로, 그리고 타입을 image/* 로 지정한 인텐트를 전달하면 갤러리 앱의 목록 화면이 실행됩니다.
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
startActivityForResult(intent, 30)
다음으로 갤러리 앱의 사진 목록에서 사용자가 선택한 사진을 액티비티 화면에 출력할 차례입니다. 이때 앞에서 살펴본 OOM 문제가 발생할 수 있으므로 BitmapFactory.Option 객체의 inSampleSize 값을 지정해 이미지 크기를 줄여서 불러와야 합니다.
다음 코드에 작성한 함수는 inSampleSize에 지정할 값을 계산하는 예를 보여줍니다.
private fun calculateInSampleSize(fileUri: Uri, reqWidth: Int, reqHeight: Int): Int {
val options = BitmapFactory.Options()
// 옵션만 설정하고자 true로 지정합니다.
options.inJustDecodeBounds = 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
}
위 코드에서는 decoreStream() 함수로 이미지를 메모리에 불러오는 것처럼 보이지만, 세 번째 매개변수에 지정한 Option 객체의 inJustDecodeBounds 값을 true로 설정했으므로 실제로 Bitmap 객체가 만들어지지 않습니다. 그 대신 읽은 이미지의 각종 정보가 option 객체에 설정됩니다.
이제 갤러리 앱의 목록에서 사용자가 사진을 하나 선택해서 되돌아왔을 때 위의 calculateInSampleSize() 함수를 이용해 이미지를 불러오는 코드를 살펴보겠습니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode === 30 && resultCode === Activity.RESULT_OK) {
try {
// inSampleSize 비율 계산, 지정
val calRatio = calculateInSampleSize(data!!.data!!,
resources.getDimensionPixelSize(R.dimen.imgSize),
resources.getDimensionPixelSize(R.dimen.imgSize))
val option = BitmapFactory.Options()
option.inSampleSize = calRatio
// 이미지 불러오기
var inputStream = contentResolver.openInputStream(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()
}
}
}
카메라 앱 연동하기
이번에는 앱에서 카메라 앱을 실행해 사진을 촬영하고 사진 데이터를 가져오는 방법을 살펴보겠습니다. 카메라 앱을 연동하여 사진을 촬영하고 그 결과를 돌려받는 방법은 다음 2가지 입니다.
- 사진 데이터를 가져오는 방법
- 사진 파일을 공유하는 방법
먼저 사진 데이터를 가져오는 방법은 카메라 앱으로 사진을 촬영한 후 파일로 저장하지 않고 데이터만 넘겨주는 방식입니다. 이 방식은 사진을 파일로 저장하지 않으므로 쉽게 구현할 수 있지만 넘어오는 사진 데이터의 크기가 작다는 단점이 있습니다.
사진 파일을 공유하는 방법은 카메라 앱에서 촬영한 사진을 파일에 저장한 후 성공인지 실패인지만 넘겨주는 방식입니다. 이 방식을 이용하면 휴대폰의 카메라 성능만큼 큰 크기의 사진을 촬영하고 앱에서 이용할 수 있지만, 카메라 앱이 파일 정보를 공유하는 것이므로 몇 가지 준비 작업을 해야 합니다.
사진 데이터를 가져오는 방법
인텐트로 카메라 앱의 사진 촬영 액티비티를 실행합니다.
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, 40)
인텐트에 액션 문자열을 MediaStore.ACTION_IMAGE_CAPTURE 로 지정하여 시스템에 전달하면 카메라 앱이 실행됩니다. 카메라 앱에서 사용자가 사진을 촬영하고 <확인>을 누르면 다시 내가 만든 앱으로 되돌아옵니다. 그러면 카메라 앱에서 넘어온 사진 데이터는 onActivityResult() 함수에서 다음과 같은 코드로 가져오면 됩니다.
val bitmap = data?.getExtras()?.get("data") as Bitmap
사진 파일을 공유하는 방법
카메라 앱으로 촬영한 사진 파일을 공유하는 방법은 다음 절차를 따릅니다.
- 앱에서 사진을 저장할 파일을 만듭니다.
- 사진 파일 정보를 포함한 인텐트를 전달해 카메라 앱을 실행합니다.
- 카메라 앱으로 사진을 촬영하여 공유된 파일에 저장합니다.
- 카메라 앱을 종료하면서 성공 또는 실패를 반환합니다.
- 카메라 앱이 저장한 사진 파일을 앱에서 이용합니다.
사진 파일을 공유하는 방법을 이용하려면 먼저 앱에서 외장 메모리에 파일을 만들어 줘야 합니다. 그런데 파일을 만들 때 getExternalStoragePublicDirectory() 또는 getExternalFilesDir() 함수를 이용할 수도 있습니다. 전자는 모든 앱에서 이용할 수 있는 파일을 만든다면, 후자는 이 앱에서만 이용할 수 있는 파일을 만듭니다.
getExternalStoragePublicDirectory() 함수를 이용해 외장 메모리에 파일을 만들려면 다음과 같은 퍼미션을 설정해 줘야 합니다. 원래는 getExternalFilesDir() 함수로 파일을 만들때에도 퍼미션을 설정해줘야했지만 API 레벨 19 버전부터는 이 작업을 할 필요가 없어졌습니다.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
그런데 API 레벨 24 버전부터는 file:// 프로토콜로 구성된 URI를 외부에 노출하지 못하도록 하는 엄격 모드(strict mode)가 적용되었습니다. 따라서 앱끼리 파일을 공유하려면 content:// 프로토콜을 이용하고 이 URI에 임시로 접근할 수 있는 권한을 부여해야 합니다. 이떄 FileProvider 클래스를 이용합니다. FileProvider 클래스는 androidx 라이브러리에서 제공하며 XML 설정을 기반으로 해서 content:// 프로토콜로 구성된 URI를 생성해 줍니다.
결국 FileProvider를 이용하려면 공유할 파일의 URI값을 만들어야 한다는 의미입니다. 그러려면 프로젝트의 res/xml 디렉터리에 파일 프로바이더용 XML 파일을 만들어 줘야 합니다.
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="myfiles" path="Android/data/com.example.test16/files/Pictures" />
</paths>
파일 프로바이더용 XML 파일은 path 속성에 지정한 경로의 파일 권한을 설정합니다. 이곳에 지정한 경로는 getExternalFilesDir() 함수로 파일을 만들었을 때 파일이 저장되는 위치입니다.
이렇게 작성한 파일 프로바이더용 XML 파일을 매니페스트 파일에 다음처럼 등록합니다.
<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>
매니페스트에 androidx 라이브러리에서 제공하는 프로바이더를 등록했습니다. <provider> 태그 아래에 <meta-data> 태그를 작성하고 resource 속성으로 res/xml에 만들어 놓은 XML 파일을 지정합니다.
이제 코드에서 카메라 앱을 연동해 보겠습니다.
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
getExternalFilesDir() 함수로 파일을 만들면서 매개변수에 Environment.DIRECTORY_PICTURES라고 지정했으므로 Android/data/com.example.test16/files/Pictures 에 저장됩니다. 파일명이 중복되지 않도록 날짜와 시각을 이용했으며 나중에 파일 내용을 읽을 때 사용하려고 파일 경로를 filePath에 저장했습니다.
val photoURI: Uri = FileProvider.getUriForFile(
this,
"com.example.test16.fileprovider", file
)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(intent, 40)
앞에서 설정한 정보를 바탕으로 FileProvider를 이용해 Uri 객체를 만들고 이를 카메라 앱을 실행하는 인텐트의 엑스트라 데이터로 설정했습니다. 이렇게 하면 파일 정보를 가지고 카메라 앱의 액티비티가 실행됩니다.
다음은 카메라 앱에서 사진을 촬영한 후 다시 앱으로 돌아왔을 때 실행되는 코드 입니다.
val option = BitmapFactory.Options()
option.inSampleSize = 10
val bitmap = BitmapFactory.decodeFile(filePath, option)
bitmap?.let {
binding.cameraResult.setImageBitmap(bitmap)
}
앞에서 저장해 뒀던 파일 경로를 decodeFile() 함수에 전달해서 비트맵 객체를 얻습니다. 그리고 OOM 오류를 막으려고 BitmapFactory.Options의 inSampleSize 값을 지정했습니다.
지도 앱 연동하기
앱이 위도와 경도의 값을 가지고 있다면 지도 앱을 연동해 위치를 보여 줄 수 있습니다. 지도 앱을 연동할 때는 인텐트의 액션 문자열을 Intent.ACTION_VIEW로 지정합니다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo: 37.5662952, 126.9779451"))
startActivity(intent)
지도 가운데 특정 위치를 보이게 하려면 위도와 경도를 넘겨줘야 하는데 이 정보를 인텐트의 데이터 정보로 전달합니다. 이때 URL은 반드시 geo:로 시작해야 하고 이어서 위도와 경도를 쉼표로 구분해서 지정해야 합니다.
전화 앱 연동하기
전화 또한 안드로이드의 기본 앱으로 많이 이용합니다. 앱에 전화번호가 있다면 전화를 걸수 있도록 해주는 것이 기본입니다. 그런데 전화를 거는 기능은 전화 앱의 액티비티에 이미 구현되어 있으므로 전화 앱과 연동해서 전화번호 데이터만 넘겨주면 됩니다.
전화 앱과 연동해 전화를 거는 기능은 다음처럼 퍼미션을 설정해야 합니다.
<uses-permission android:name="android.permission.CALL_PHONE" />
그리고 다음과 같은 인텐트를 선언해서 시스템에 전달해야 합니다.
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:02-120"))
startActivity(intent)
전화 앱과 연동하려면 인텐트의 액션 문자열을 Intent.ACTION_CALL 로 지정해야 합니다. 그리고 데이터 정보는 tel: 로 시작해야 하고 이어서 전화번호를 명시해야 합니다. 그러면 자동으로 해당 전화번호로 전화를 겁니다.
3. 카메라, 갤러리 앱과 연동하는 앱 만들기
1. 그레들 파일 수정
android {
...
buildFeatures {
viewBinding = true
}
}
2. 리소스(res/drawable) 준비
3. 매니페스트 파일 수정
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyFirstApplication">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapplication16.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
4. 메인 액티비티 수정
package com.example.myapplication16
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.example.myapplication16.databinding.ActivityMainBinding
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var filePath: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//gallery request launcher..................
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.userImageView.setImageBitmap(bitmap)
} ?: let {
Log.d("kkang", "bitmap null")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
binding.galleryButton.setOnClickListener {
//gallery app........................
val intent = Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
requestGalleryLauncher.launch(intent)
}
//camera request launcher.................
val requestCameraFileLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult())
{
// 카메라 앱
val calRatio = calculateInSampleSize(
Uri.fromFile(File(filePath)),
resources.getDimensionPixelSize(R.dimen.imgSize),
resources.getDimensionPixelSize(R.dimen.imgSize)
)
val option = BitmapFactory.Options()
option.inSampleSize = calRatio
val bitmap = BitmapFactory.decodeFile(filePath, option)
bitmap?.let {
binding.userImageView.setImageBitmap(bitmap)
}
}
binding.cameraButton.setOnClickListener {
//camera app......................
//파일 준비...............
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.myapplication16.fileprovider", file
)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
requestCameraFileLauncher.launch(intent)
}
}
private fun calculateInSampleSize(fileUri: Uri, reqWidth: Int, reqHeight: Int): Int {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
try {
var inputStream = contentResolver.openInputStream(fileUri)
//inJustDecodeBounds 값을 true 로 설정한 상태에서 decodeXXX() 를 호출.
//로딩 하고자 하는 이미지의 각종 정보가 options 에 설정 된다.
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
}
}
5. 메인 레이아웃 수정
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="프로필 사진 교체"
android:textStyle="bold"
android:layout_marginBottom="24dp"/>
<androidx.cardview.widget.CardView
android:layout_width="150dp"
android:layout_height="150dp"
app:cardCornerRadius="75dp"
app:cardElevation="0dp"
>
<ImageView
android:id="@+id/userImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/user_basic"
android:scaleType="centerCrop"
/>
</androidx.cardview.widget.CardView>
<Button
android:id="@+id/galleryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gallery App"
android:layout_marginTop="24dp"/>
<Button
android:id="@+id/cameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Camera App"
android:layout_marginTop="24dp"/>
</LinearLayout>
6. res/values/dimens.xml 추가
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="imgSize">150dp</dimen>
</resources>
7. res/xml/file_paths.xml 추가
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="myfiles"
path="Android/data/com.example.myapplication16/files/Pictures" />
</paths>
8. 앱 실행 및 테스트
카메라로 사진을 찍으면 옆으로 90도 돌아간 사진이 세팅되는데 이 문제는 나중에 한 번 알아봐야겠다.