본문 바로가기
Android/[Kotlin]

[Kotlin] 카메라 예제

by PhoB 2023. 4. 1.

과거에 작성했던 카메라 예제의 코틀린 버전이다.

 

[Android][JAVA] 카메라

앱에서 카메라를 사용하여 사진을 읽어오고자 한다. 참고로 찍은사진을 이미지뷰에 불러오기만 할뿐 저장하는 코드는 아니다. 우선 카메라 기능을 사용하기 위해서는 권한 허가가 필요하다. And

psh0036.tistory.com

이때는 카메라를 이용하는것과 촬영한 사진을 프리뷰에 띄워주는 부분까지만 진행했었지만 이번 포스팅에서는 촬영한 이미지를 디바이스의 갤러리에 저장하는 부분까지 해볼 예정이다.

 

1. 권한 설정

먼저 안드로이드에서 카메라와 저장소에 관련된 기능을 사용하기 위해서는 권한이 필요하다.

안드로이드 공식문서를 보면 카메라와 캘린더 등의 디바이스의 기능에 접근하는 권한들을 위험한 권한으로 분류해놓은것을 볼 수 있다.

(위험한 권한이란? , 앱이 사용자의 개인정보를 포함하거나 사용자의 저장된 데이터나 다른 앱의 작업에 영향을 미칠 수 있는 데이터나 리소스를 필요로 하는 영역)

권한을 획득하는 부분까지는 코틀린과 자바 이전의 영역이기 때문에 서로 크게 다를것이 없다.

AndroidManifest.xml파일에 권한을 설정해 주도록 하자

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.hardware.camera"  android:required="true"/>

위 3줄 같은 경우에는 카메라 사용권한 , 외부 저장소 입/출력 권한 이라고 생각하시면 됩니다

마지막 4번째 줄의 uses-feature은 앱을 설치하는데 필요한 요구사항 이라고 생각하시면 됩니다 해당앱을 설치하기 위해서는 카메라 기능이 요구된다는 소리입니다.

( + 외부저장소는 SD카드 , 디바이스의 저장소 등 로컬저장소가 아닌 저장소를 말하는 것 입니다. 반대로 로컬 저장소는 앱 내부의 저장소를 말합니다)

2.Provider설정

안드로이드의 보안수칙에 따라서 앱 외부의 콘텐츠(주소록, 저장소 등등)에 접근하기 위해서는 무조건 콘텐츠 제공자(Content Provider)를 사용해야합니다. 이중에서도 <provider>는 콘텐츠 제공자의 구성요소를 선언하는 부분으로 Content Provider의 서브클래스 입니다. <application>태그 사이에 작성해주도록 합시다.

<provider
    <!--구성요소의 이름-->
    android:name="androidx.core.content.FileProvider" 
    <!--구성요소의 권한-->
    android:authorities="[패키지명].fileprovider"
    <!--구성요소가 생성한 URI에 대한 권한을 다른 앱에 부여할지 여부-->
    android:grantUriPermissions="true"
    <!--구성요소가 다른 앱에서 접근 가능한지 여부-->
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        <!--요소에서 참조하는 XML 리소스-->
        android:resource="@xml/filepaths" />
</provider>

 

3. filepaths.xml작성

filepaths.xml파일은 위 provider를 작성할때 resource에서 설정한 파일명과 일치시켜 주어야합니다.

provider가 해당 파일에서 경로를 가져와서 URI를 생성하도록 하는 역할을 합니다.

 

1.파일 만들기

res -> xml 디렉토리에 filepaths.xml파일을 새로 만들어 줍니다.

 

2, 코드 작성

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--Context.getCacheDir() 내부 저장소-->
    <cache-path
        name="cache"
        path="." />

    <!--Context.getFilesDir() 내부 저장소-->
    <files-path
        name="Capture"
        path="." />

    <!--  Environment.getExternalStorageDirectory() 외부 저장소-->
    <external-path
        name="external"
        path="Android/data/[패키지명]/files/Pictures" />

    <!--  Context.getExternalCacheDir() 외부 저장소-->
    <external-cache-path
        name="external-cache"
        path="." />

    <!--  Context.getExternalFilesDir() 외부 저장소-->
    <external-files-path
        name="external-files"
        path="." />
</paths>

설정해야하는 저장소 태그에 경로를 설정해줍시다.

 

4. 클래스파일 작성

이제 클래스파일 작성입니다.

val REQUEST_IMAGE_CAPTURE = 1 //카메라 요청코드
lateinit var curPhothPath: String //문자열 형태의 사진 저장 경로
lateinit var binding : ActivityMainBinding // ViewBinding

먼저 전역변수들을 설정해 줍니다.

카메라 요청코드 같은 경우는 따로 존재하는 변수명이 아니라 카메라 기능호출을 요청했다. 라는것을 알려줘야하는데 그때 사용합니다.

그리고 뷰 바인딩을 사용해서 레이아웃과 연결하였습니다.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
    setPermission()
    binding.camera.setOnClickListener {
        takeCapture()//사진 촬영
    }
}

setPermission() : 권한에 대해서 후술할 사항이 있는데 그때 다시 설명하겠습니다, 권한과 관련된 메소드입니다.

버튼을 눌러 takeCapture()메소드를 실행합니다.

 

//카메라 촬영
private fun takeCapture() {
    //기본카메라 앱 실행
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).also{takePictureIntent->
        takePictureIntent.resolveActivity(packageManager)?.also{
            val photoFile: File? = try{
                 createImageFile()
            }catch(ex: IOException){
                null
            }
            photoFile?.also{
                val photoURI: Uri = FileProvider.getUriForFile(
                    this,
                    "[패키지명.fileprovider",
                    it
                )
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,photoURI)
                startActivityForResult(takePictureIntent,REQUEST_IMAGE_CAPTURE)
            }
        }
    }
}

 

 

인텐트를 통하여 기본카메라를 작동시키고 찍은 결과물의 저장경로를 설정하는 메소드입니다.

다만 앱내부의 로컬저장소에만 저장할 뿐 원래 계획한 외부저장소, 즉 갤러리로의 저장은 수행하지 않습니다.

 

//이미지 파일 생성
private fun createImageFile(): File? {
    val timestamp : String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val storageDir : File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile("JPEG_${timestamp}_",".jpg",storageDir)
        .apply{curPhothPath = absolutePath}
}

 촬영한 이미지 파일을 생성하며 이미지의 이름과 형식을 설정해주는 메소드입니다.

여기서는 촬영시각을 파일명으로 설정하였습니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    //startActivity를 통해서 기본 카메라 앱으로 받아온 사진의 결과값
    super.onActivityResult(requestCode, resultCode, data)

    //이미지를 성공적으로 가져왔다면
    if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode== RESULT_OK) {
        val bitmap: Bitmap
        val file = File(curPhothPath)


        if(Build.VERSION.SDK_INT>=29){
            val decode = ImageDecoder.createSource(
                this.contentResolver,
                Uri.fromFile(file)
            )
            bitmap = ImageDecoder.decodeBitmap(decode)
            binding.imageView.setImageBitmap(bitmap)
            savePhoto(bitmap)
        }
    }
}

촬영한 이미지를 ImageView에 띄워 프리퓨기능을 제공해주는 메소드입니다. 이후 로컬저장소가 아니라 외부저장소에 사진을 저장하기 위해서 savePhoto()메소드를 실행해줍니다.

 

//갤러리에 저장
private fun savePhoto(bitmap: Bitmap) {
    val timestamp : String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val fileName = "${timestamp}.jpeg"

    // 외부 저장소에 사진을 저장할 수 있는 URI 생성
    val imagesCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    val imageDetails = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    }
    val uri = contentResolver.insert(imagesCollection, imageDetails)

    // 이미지 파일을 OutputStream으로 저장
    uri?.let {
        contentResolver.openOutputStream(it)?.use { outputStream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
            Toast.makeText(this,"저장 완료",Toast.LENGTH_SHORT).show()
        }
    }
}

savePhoto()뿐만 아니라 takeCapture()에서도 MediaStore가 사용된 것을 볼 수 있는데 그 이유는 안드로이드의 보안정책 때문이다. 안드로이드 운영체제에서는 앱이 파일시스템의 모든 경로에 접근하는 것을 허용하지 않는다고 합니다. 외부저장소에 대해서는 앱에서 저장한 파일만 접근할 수 있다고 하는데 이는 안드로이드에서 개인정보 보호를 위해서 강제하는 규칙입니다.

그렇기 때문에 외부저장소에 접근하기 위한방법으로 MediaStore이라는 안드로이드 API를 사용해야만 합니다.

(다른방법으로  getExternalStorageDirectory()메소드를 사용하는 방법도 있지만 API29이상부터는 허용되지 않으며 보안도 MediaStore에 비해 취약하다고 합니다.)

 

4. TedPermission

안드로이드 6.0으로 업데이트 한 이후에는 메니페스트 파일에 권한설정을 해주어야할 뿐만 아니라 런타임시에도 설정을 해주어야합니다 특히 카메라나 외부 저장소같은 위험한 권한을 사용하고자 할때는 무조건입니다.

checkSelfPermission(), requestPermissions(), onRequestPermissionsResult()같은 메소드들을 사용하여 다이얼로그 띄우기, 권한이 거절되었을때 띄우는 메세지등 다양한 설정을 할 수 있지만 그 사용법이 복잡하고 어렵습니다.

그럴때 TedPermission라이브러리를 사용하여 그 과정을 간소화시킬 수 있습니다.

먼저 build.Gradle(Module)에 의존성을 추가해 줍니다. 저의 경우에는 최신버전인 3.3.0버전을 사용했습니다.

implementation 'io.github.ParkSangGwon:tedpermission-normal:3.3.0'

이후 TedPermission을 호출하고 사용할 메소드를 작성해주었습니다.

private fun setPermission() {
    val permission = object : PermissionListener {
        override fun onPermissionGranted() { //설정해놓은 위험권한들이 허용되었을경우 해당 메소드를 사용
           Toast.makeText(this@MainActivity,"권한이 허용되었습니다",Toast.LENGTH_SHORT).show()
        }
        override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
            //거부한경우에은 해당 메소드
            Toast.makeText(this@MainActivity,"권한이 거부되었습니다",Toast.LENGTH_SHORT).show()
        }
    }
    TedPermission.create()
        .setPermissionListener(permission)
        .setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
        .setPermissions(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.CAMERA)
        .check();
}
 

GitHub - ParkSangGwon/TedPermission: Easy check permission library for Android Marshmallow

Easy check permission library for Android Marshmallow - GitHub - ParkSangGwon/TedPermission: Easy check permission library for Android Marshmallow

github.com