관리 메뉴

나만을 위한 블로그

[Android] CameraX 코드랩 뜯어보기 - 3 - 본문

Android

[Android] CameraX 코드랩 뜯어보기 - 3 -

참깨빵위에참깨빵 2023. 3. 27. 21:07
728x90
반응형

https://onlyfor-me-blog.tistory.com/490

 

[Android] CameraX 코드랩 뜯어보기 - 1 -

카메라는 내게 많이 생소한 영역이기도 하고 예전에 CameraX인지 뭔지가 새로 나왔다고 들었어서 최근에 코드랩을 따라 쳐보고 공부하긴 했었는데, 블로그에 남겨두면 나중에 찾아보기 더 좋을

onlyfor-me-blog.tistory.com

 

https://onlyfor-me-blog.tistory.com/714

 

[Android] CameraX 코드랩 뜯어보기 - 2 -

https://onlyfor-me-blog.tistory.com/490 [Android] CameraX 코드랩 뜯어보기 - 1 - 카메라는 내게 많이 생소한 영역이기도 하고 예전에 CameraX인지 뭔지가 새로 나왔다고 들었어서 최근에 코드랩을 따라 쳐보고

onlyfor-me-blog.tistory.com

 

1편에선 환경설정과 뼈대 코드를 작성했고 2편에선 후면 카메라 렌즈를 통해 핸드폰 화면에 미리보기를 띄우는 처리를 했다. 이번에는 사진을 찍는 코드를 작성한다.

아래 takePhoto() 코드를 프로젝트에 복붙한다.

 

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions
           .Builder(contentResolver,
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues)
           .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken
   imageCapture.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun onImageSaved(output: ImageCapture.OutputFileResults) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

 

먼저 ImageCapture의 참조를 얻는다. 이 때 null이면 takePhoto()를 종료한다. 안드로이드 스튜디오 Electric Eel 기준으로 return을 클릭하면 return을 담은 함수명에 표시가 될 것이다. 이것을 통해 return이 함수를 종료하는지, 람다나 코루틴을 사용할 경우 람다 또는 코루틴을 종료하는지 확인할 수 있다.

그리고 ContentValues의 인스턴스를 만든 뒤 MediaStore 값들을 설정한다. 이 때 표시되는 MediaStore 이름이 고유하도록 타임스탬프를 활용한 걸 볼 수 있다. 추가로 안드로이드 파이보다 상위 버전인지를 if로 체크해서 RELATIVE_PATH 값을 별도로 설정하는 코드가 보인다. 이것은 이미지를 저장할 상대 경로를 설정하는 코드다.

에뮬레이터 기준으로 take photo 버튼을 눌러 사진을 찍은 뒤 갤러리로 들어가면 "CameraX-Image"라는 폴더에 내가 찍은 사진이 들어가 있는 걸 볼 수 있다.

 

그럼 왜 안드로이드 파이부터인지 짚고 가자. 왜일까? 그 이유는 안드로이드 파이의 상위 버전인 안드로이드 10부터 도입된 Scoped Storage 기능 때문이다. 관련 디벨로퍼 링크는 아래에 첨부한다.

 

https://developer.android.com/about/versions/10/privacy/changes?hl=ko 

 

Android 10의 개인정보 보호 변경사항  |  Android 개발자  |  Android Developers

Android 10의 개인정보 보호 변경사항 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 10(API 레벨 29)에는 사용자의 개인정보 보호 강화를 위해 많은 기능

developer.android.com

 

https://developer.android.com/training/data-storage?hl=ko#scoped-storage 

 

데이터 및 파일 저장소 개요  |  Android Developers

데이터 및 파일 저장소 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android는 다른 플랫폼의 디스크 기반 파일 시스템과 유사한 파일 시스템을 사용

developer.android.com

 

2번째 링크를 확인하면 이런 내용이 있다.

 

사용자에게 더 많은 권한을 제공하고 파일이 복잡해지지 않게 하기 위해 안드로이드 10(API 레벨 29) 이상을 타겟팅하는 앱에는 기본적으로 외부 저장소로 범위가 지정된 접근 권한 또는 범위 지정 저장소(scoped storage)가 부여된다. 이런 앱은 외부 저장소의 앱별 디렉토리와 앱에서 만든 특정 타입의 미디어에만 접근할 수 있다. 앱이 런타임에 저장소 관련 권한을 요청하는 경우 사용자가 보는 대화상자에는 범위 지정 저장소가 설정돼 있어도 앱이 외부 저장소에 대한 광범위한 접근을 요청한다고 표시된다
앱이 앱별 디렉토리 외부 및 MediaStore API가 접근할 수 있는 디렉토리 외부에 저장된 파일에 접근해야 하는 경우가 아니면 범위 지정 저장소를 사용하라. 앱별 파일을 외부 저장소에 저장하면 이런 파일을 외부 저장소의 앱별 디렉토리에 배치함으로써 범위 지정 저장소를 더 쉽게 채택할 수 있게 된다. 이렇게 하면 범위 지정 저장소가 사용 설정됐을 때 앱이 해당 파일에 대한 접근 권한을 유지한다.

 

scoped storage는 왜 도입됐을까? 안드로이드 파이가 사용될 때 안드로이드 개발자들은 권한 중 WRITE_EXTERNAL_STORAGE 권한만 얻으면 외부 저장소에 별도의 폴더를 만들어서 거기에 파일을 다운로드하거나 만들 수 있었고, 또 어지간해선 사용자가 안 찾아볼 위치에 온점으로 시작하는 폴더, 파일 등을 만들 수 있었다. 즉 사용자 프라이버시가 전혀 지켜지지 않았다는 뜻이다. 그래서인지 핸드폰을 쓰다가 폴더 목록을 확인해보니 뭐에 쓰는지 모르겠는 폴더들이 즐비했던 걸 본 기억이 있다.

또한 안드로이드의 저장소는 내부 / 외부 저장소로 크게 2개가 존재한다. 내부 저장소는 각 앱이 혼자 사용할 수 있는 공간이 격리된 상태로 제공되었고(샌드박스), 외부 저장소는 모든 앱이 위에서 말한 WRITE_EXTERNAL_STORAGE 권한만 얻으면 자유롭게 사용할 수 있었다. 이것이 안드로이드 10부터는 외부 저장소도 앱별로 격리되게 되었다. 그래서 사진, 음악, 파일, 다운로드 등의 형태로 폴더가 분리되었고 지금도 이 구조를 유지하고 있다.

만약 A 앱에서 만들어진 파일을 B 앱에서 접근해 사용하려면 추가 절차가 필요하고 앱은 사진, 다운로드 등 공용 폴더에 접근할 순 있지만 권한을 요청해서 얻어야 읽고 쓸 수 있다. 이 때 추가 절차 중 하나가 위 코드의 MediaStore API를 사용하는 것이다. 이것으로 사용자의 프라이버시를 보호할 수 있게 된 것이다.

 

본론으로 돌아와서, 이후 OutputFileOptions 객체를 만든다. 이것은 카메라로 캡쳐한 이미지 또는 동영상을 파일로 저장할 때 이름, 경로, 파일 형식, 저장 옵션 등을 설정할 수 있게 해주는 클래스다.

이후 앞서 만든 imageCapture 객체에서 takePicture()를 호출하면서 outputOptions 등의 매개변수를 넘긴다. 그리고 OnImageSavedCallback 인터페이스를 구현해서 onError, onImageSaved 콜백을 구현한다.

 

이제 startCamera()의 코드를 수정한다.

 

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}

 

여기까지 완료됐다면 에뮬레이터 기준 사진을 찍고 갤러리로 들어가면 아래와 같이 표시된다.

 

 

CameraX-Image라는 폴더에 찍힌 사진이 들어있다. 이 사진을 클릭해서 세부정보를 확인하면 아래와 같다.

 

 

코드에서 지정한 경로에 타임스탬프 형태의 이름으로 사진이 저장된 걸 확인할 수 있다.

반응형
Comments