[Android] Camera And Storage

KIMGEUNTAE·2023년 12월 10일
1

Android

목록 보기
5/10

카메라 앱

카메라 앱을 실행하여 사진을 촬영하면 해당 사진이 외부 파일 앱에 저장이 되고 파일 앱을 여는 버튼을 누르면 해당 사진을 가져와서 이미지를 다이얼로그로 미리보기를 표시하는 것을 작업 하였고 해당 부분을 샘플로 해서 작성하였습니다.


Manifest 정의

카메라 및 저장소를 사용하기 위한 것

  <uses-feature android:name="android.hardware.camera.any" />
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
      android:maxSdkVersion="32" />
  <uses-permission
      android:name="android.permission.WRITE_EXTERNAL_STORAGE"
      android:maxSdkVersion="28" />
  • android.hardware.camera.any: 카메라 하드웨어(전면, 후면 등)를 사용할 수 있음을 나타냄
  • android.permission.CAMERA: 카메라에 접근하기 위한 권한을 요청
  • android.permission.READ_EXTERNAL_STORAGE:
    외부 저장소에서 데이터를 읽을 수 있는 권한을 요청
  • android.permission.WRITE_EXTERNAL_STORAGE:
    외부 저장소에 데이터를 쓸 수 있는 권한을 요청


변수 정의

카메라 사용 권한 및 저장소에 이미지를 선택하고 받기 위해 선언

  private lateinit var cameraLauncher: ActivityResultLauncher<Intent>
  private lateinit var permissionLauncher: ActivityResultLauncher<String>
  private lateinit var storageLauncher: ActivityResultLauncher<Intent>
  private lateinit var storageLauncher2: ActivityResultLauncher<Intent>
  private var currentPhotoUri: Uri? = null
  private var currentPhotoUri2: Uri? = null

  private var currentCameraState: CameraState = CameraState.CAMERA1

  private var isCameraAppClosed = false
  • cameraLauncher: 카메라 앱을 시작하고 결과를 받기 위한 변수

  • permissionLauncher: 카메라 사용 권한을 요청하고 결과를 받기 위한 변수

  • storageLauncher: 파일 저장소에서 이미지를 선택하고 결과를 받기 위한 변수

  • currentPhotoUri: 현재 선택된 또는 캡처된 사진의 URI를 저장

  • isCameraAppClosed: 카메라 앱이 종료되었는지 여부를 추적하는 변수

  • currentCameraState: 현재 사용 중인 카메라 상태를 추적



로직 설정

선언한 변수에는 ActivityResultLauncher를 사용하여 설정된 카메라 앱과 파일 저장소 앱으로부터 사용자 상호작용의 결과를 처리하기 위해 로직을 설정

private fun cameraResult() {
    cameraLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                isCameraAppClosed = false
                when (currentCameraState) {
                    CameraState.CAMERA1 -> {
                        currentPhotoUri?.let { uri ->
                            handleButtonUpload(
                                binding.buttonFileUploadEnd,
                                binding.buttonFileUploadDelete,
                                uri
                            )
                            isCameraAppClosed = true
                        }
                    }

                    CameraState.CAMERA2 -> {
                        currentPhotoUri2?.let {uri ->
                            handleButtonUpload(
                                binding.buttonFileUploadEnd2,
                                binding.buttonFileUploadDelete2,
                                uri
                            )
                            isCameraAppClosed = true
                        }
                    }
                }
            } else {
                isCameraAppClosed = true
            }
        }

    storageLauncher = registerLauncher {
        handleStorageResult(
            it,
            binding.buttonFileUploadEnd,
            binding.buttonFileUploadDelete,
            binding.error
        ) { uri -> currentPhotoUri = uri }
    }
    storageLauncher2 = registerLauncher {
        handleStorageResult(
            it,
            binding.buttonFileUploadEnd2,
            binding.buttonFileUploadDelete2,
            binding.error2
        ) { uri -> currentPhotoUri2 = uri }
    }

    permissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                launchCamera { uri -> currentPhotoUri = uri }
            }
        }
}
  1. cameraLauncher 설정:

  • 카메라 앱에서 사진을 찍은 후 결과를 처리
  • 결과 코드가 Activity.RESULT_OK일 때, 즉 사용자가 성공적으로 사진을 찍었을 경우:
    • isCameraAppClosed 변수를 false로 설정 (카메라 앱이 아직 닫히지 않았음을 나타냄)
    • currentCameraState에 따라 currentPhotoUri 또는 currentPhotoUri2를 사용하여 handleButtonUpload 함수를 호출, 이 함수는 UI에 촬영된 사진을 표시하고 관련 버튼을 설정
    • isCameraAppClosed를 true로 설정하여 카메라 앱이 닫혔음을 나타냄
  • 결과 코드가 Activity.RESULT_OK가 아닌 경우에도 isCameraAppClosedtrue로 설정
  1. storageLauncher 및 storageLauncher2 설정:

  • 파일 저장소에서 이미지를 선택한 후 결과를 처리
  • registerLauncher 함수를 호출하여 ActivityResultLauncher 인스턴스를 생성하고, 이를 storageLauncherstorageLauncher2에 할당
  • handleStorageResult 함수를 사용하여 선택된 이미지의 URI를 처리하고, 이를 currentPhotoUri 또는 currentPhotoUri2에 저장
  1. permissionLauncher 설정:

  • 카메라 사용 권한을 요청하고 결과를 처리
  • 사용자가 권한을 허용한 경우 launchCamera 함수를 호출하여 카메라 앱을 실행함 이 함수는 사진이 저장될 URI를 currentPhotoUri에 할당합니다.


Camera

  • 카메라 앱을 기동 할 수 있도록 하는 처리

requestCameraPermission

카메라 권한을 요청하는 과정을 처리

 private fun requestCameraPermission(callback: () -> Unit) {
        val permission = Manifest.permission.CAMERA
        when {
            ContextCompat.checkSelfPermission(
                requireContext(),
                permission
            ) == PackageManager.PERMISSION_GRANTED -> {
                callback()
            }

            // 카메라 권한 거절로 되어 있을 시
            shouldShowRequestPermissionRationale(permission) -> {
                AlertDialog.Builder(requireContext())
                    .setMessage("카메라 권한이 필요합니다.")
                    .setPositiveButton("확인") { _, _ ->
                        permissionLauncher.launch(permission)
                    }
                    .setNegativeButton("취소", null)
                    .show()
            }

            else -> {
                permissionLauncher.launch(permission)
            }
        }
    }
  1. 권한 변수 설정:

  • Manifest.permission.CAMERA를 사용하여 카메라 권한을 나타내는 문자열을 permission 변수에 할당
  1. 권한 확인:

  • ContextCompat.checkSelfPermission 메소드를 사용하여 현재 애플리케이션의 카메라 권한 상태를 확인
  • 권한이 이미 부여되어 있으면 (PackageManager.PERMISSION_GRANTED) callback 함수를 호출하고 콜백 함수는 권한이 있을 때 실행할 작업을 정의
  1. 권한 거절 시 대화 상자 표시:

  • shouldShowRequestPermissionRationale 메소드를 사용하여, 사용자가 이전에 권한 요청을 거절했는지 확인
  • 거절한 경우, 사용자에게 카메라 권한이 필요한 이유를 설명하는 대화 상자를 표시
  • 대화 상자에는 확인 버튼을 누를 경우 권한 요청을 다시 시도하는 로직(permissionLauncher.launch(permission))취소 버튼이 포함
  1. 권한 요청:

  • 위의 조건에 해당하지 않는 경우 (즉, 권한이 거절되지 않았거나 권한 요청을 한 번도 하지 않은 경우) permissionLauncher를 사용하여 카메라 권한을 요청

launchCamera

카메라를 사용하여 사진을 찍고, 그 사진을 애플리케이션에서 접근할 수 있는 위치에 저장하는 과정을 단순화 함

private fun launchCamera(updateUri: (Uri) -> Unit) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME,"JPEG_${System.currentTimeMillis()}_")
        put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg")        }
    val uri = requireContext().contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    )
    val cameraIntent =Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    uri?.let {
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT,uri)
        cameraLauncher.launch(cameraIntent)
        updateUri(uri)  
    }
} 
  1. 컨텐츠 값 설정:

  • ContentValues 객체를 생성하고 이를 사용하여 새로 촬영될 사진의 데이터를 정의
  • MediaStore.Images.Media.DISPLAY_NAME에는 파일 이름으로 현재 시간을 기반으로 한 문자열을 지정
  • MediaStore.Images.Media.MIME_TYPE에는 파일의 MIME 타입으로 "image/jpeg"을 지정
  1. URI 생성:

  • requireContext().contentResolver.insert 메소드를 사용하여 새로운 사진을 위한 URI를 생성하고 이 URI는 사진이 저장될 위치를 나타냄
  • URI는 MediaStore.Images.Media.EXTERNAL_CONTENT_URI를 기반으로 하며 앞서 정의한 contentValues를 사용
  1. 카메라 인텐트 설정 및 실행:

  • Intent 객체를 생성하여 카메라 애플리케이션을 시작하기 위한 인텐트를 정의 (MediaStore.ACTION_IMAGE_CAPTURE)
  • 생성된 URI가 null이 아닌 경우, 이 URI를 인텐트에 추가cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri) 카메라 앱이 촬영한 사진을 해당 URI에 저장하도록 지시
  • cameraLauncher.launch(cameraIntent)를 호출하여 카메라 앱을 실행
  1. URI 업데이트:

  • updateUri 콜백 함수를 호출하여 새로 생성된 사진의 URI를 전달하고 호출하는 측에서 URI를 처리하는 데 사용


Storage

  • 외부 파일 앱 및 파일의 Uri 및 확장자와 관련하여 처리

registerLauncher

이 함수는 파일 저장소에서 파일을 선택하거나, 다른 유사한 액티비티로부터 결과를 받을 때 사용

private fun registerLauncher(callback: (Uri?) -> Unit) =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val uri = result.data?.data
                callback(uri)
            }
        }
  1. 함수 정의:

  • registerLauncher 함수는 하나의 매개변수를 받으며: callback이라는 이름의 람다 함수로, Uri? 타입의 인자를 받음
  1. ActivityResultLauncher 초기화:

  • registerForActivityResult 메소드는 ActivityResultContracts.StartActivityForResult() 사용하여 ActivityResultLauncher 인스턴스를 생성
  • 이 인스턴스는 다른 액티비티를 시작하고, 그 결과를 처리하는 데 사용
  1. 결과 처리 로직:

  • 액티비티가 결과를 반환할 때, 콜백 함수가 호출
  • 만약 결과 코드가 Activity.RESULT_OK (즉, 성공적)이라면, 반환된 Intent 객체에서 URI (result.data?.data)를 추출
  • 그 후, 이 URI를 callback 함수에 전달하여 호출고 이 람다 함수는 호출하는 쪽에서 정의될 수 있으며 URI를 사용하여 필요한 추가 작업을 수행

handleStorageResult

파일의 Uri와 관련된 여러 UI 컴포넌트 (Button과 TextView)의 상태를 처리하며 또한 파일의 확장자와 크기를 검사하여 에러 메시지를 표시 함

private fun handleStorageResult(
        uri: Uri?,
        buttonEnd: Button,
        buttonDelete: Button,
        errorView: TextView,
        updateUri: (Uri) -> Unit
    ) {
        uri?.let {
            val extension = getFileName(it).substringAfterLast('.', "").lowercase(Locale.ROOT)
            val fileSize =
                requireContext().contentResolver.openFileDescriptor(it, "r")?.statSize ?: 0

            when {
                !listOf("jpg", "jpeg").contains(extension) -> {
                    errorView.visibility = View.VISIBLE
                    errorView.text = "형식이 틀립니다"
                    return
                }

                // 크기 -> byte
                fileSize > 300000 -> {
                    errorView.visibility = View.VISIBLE
                    errorView.text = "파일 크기가 초과 했습니다"
                    return
                }

                else -> {
                    errorView.visibility = View.GONE
                    buttonEnd.text = getFileName(it)
                    buttonEnd.visibility = View.VISIBLE
                    buttonDelete.visibility = View.VISIBLE
                    updateUri(it)
                }
            }
        }
    }
  1. 매개변수:

  • uri : 선택된 파일의 URI
  • buttonEnd : 파일이 유효하면 보여질 버튼
  • buttonDelete : 파일을 삭제하는 버튼
  • errorView : 오류 메시지를 표시하는 텍스트 뷰
  • updateUri : 파일의 URI를 업데이트하는 콜백 함수
  1. 파일 유효성 검사:

  • urinull이 아닌 경우에만 작업을 수행
  • 파일의 확장자를 추출하여 소문자로 변환하고 getFileName 함수를 사용하여 파일 이름을 가져온 후 마지막 '.' 이후의 문자열을 추출하는 방식
  • 파일의 크기를 가져오고 ContentResolver의 openFileDescriptor 메소드를 사용하여 파일의 크기를 바이트 단위로 얻음
  1. 조건에 따른 처리:

  • 파일 확장자가 "jpg" 또는 "jpeg"가 아니면, 오류 메시지를 표시하고 함수를 종료
  • 파일 크기가 300,000 바이트(약 300KB)를 초과하면, 오류 메시지를 표시하고 함수를 종료
  • 위 조건에 모두 해당하지 않으면 오류 메시지를 숨기고 buttonEnd에 파일 이름을 표시하며 buttonEndbuttonDelete 버튼을 보이게 함 그리고 updateUri 함수를 호출하여 선택된 파일의 URI를 업데이트

getFileName

주어진 Uri를 사용하여 파일 이름을 가져오는 기능

private fun getFileName(uri: Uri): String {
        var name = ""
        val returnCursor = requireContext().contentResolver.query(uri, null, null, null, null)
        returnCursor?.let { cursor ->
            val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
            cursor.moveToFirst()
            name = cursor.getString(nameIndex)
            cursor.close()
        }
        return name
    }
  1. 함수 정의:

  • getFileName 함수는 Uri 타입의 매개변수 uri를 받아 해당 URI에서 파일의 이름을 추출하여 문자열로 반환
  1. 컨텐츠 리졸버 사용:

  • requireContext().contentResolver.query 메소드를 사용하여 주어진 uri에 대한 쿼리를 실행하고 파일 시스템이나 다른 앱이 제공하는 컨텐츠에 접근할 수 있게 해줌
  1. 커서를 통한 파일 이름 추출:

  • 쿼리 결과로 반환된 Cursor 객체(returnCursor)를 사용하여, 실제 파일 이름을 찾음
  • getColumnNameIndex 메소드를 사용하여 OpenableColumns.DISPLAY_NAME(파일의 표시 이름)에 해당하는 컬럼의 인덱스를 찾음
  • 커서를 첫 번째 행으로 이동시킨 후 (cursor.moveToFirst()), 이 인덱스를 사용하여 파일 이름을 추출
  1. 리소스 정리 및 결과 반환:

  • 커서 사용이 끝난 후에는 cursor.close() 를 호출하여 리소스를 해제
  • 추출한 파일 이름을 name 변수에 저장한 후, 이를 반환

handleButtonUpload

카메라를 사용하여 사진을 찍거나 파일을 선택한 후에 해당 사진을 표시하고 필요한 추가 작업(미리보기)을 수행할 수 있도록 UI를 관리

private fun handleButtonUpload(buttonEnd: Button, buttonDelete: Button, uri: Uri?) {
        uri?.let {
            if (isCameraAppClosed) {  // 카메라 앱이 종료되지 않았을 때만 미리보기 표시
                showDialogImage(it)
            }
            // 데이터 베이스 저장 별도 처리 필요
        }
        buttonEnd.text = uri?.let { getFileName(it) }
        buttonEnd.visibility = View.VISIBLE
        buttonDelete.visibility = View.VISIBLE
    }
  1. URI 유효성 검사 및 미리보기:

  • urinull이 아닌 경우에만 작업을 수행
  • isCameraAppClosed 변수를 확인하여, 카메라 앱이 이미 종료된 경우에만 showDialogImage 함수를 호출하여 이미지의 미리보기를 표시
    이는 사용자가 카메라 앱에서 사진을 찍고 돌아온 직후에만 미리보기를 표시하도록 하기 위함
  1. UI 업데이트:

  • buttonEnd에 파일의 이름을 표시하며 이는 getFileName 함수를 사용하여 파일 이름을 가져옴
  • buttonEndbuttonDelete 버튼을 보이게 설정하고 이를 통해 선택된 파일을 확인하고 필요한 경우 삭제할 수 있음


정리

현재 안드로이드에서는 기존의 카메라 라이브러리는 지원이 중단되어 카메라2 또는 카메라X를 추천을 하고 있다.
그래서 접근 방식 및 처리 방법도 변경이 되어 최신 안드로이드 버전에서 앱을 개발할 때는 이러한 변화를 고려해야 함
지금은 기존의 카메라 라이브러리로 하였지만 카메라X도 샘플로 만들어서 정리를 할려고 한다.




깃허브 : https://github.com/GEUN-TAE-KIM/CameraApiAndStorageSave_Sample.git


profile
Study Note

0개의 댓글