Group Study (2021-2022)/Android

[Android] 3주차 스터디 - Camera, Firebase

알 수 없는 사용자 2021. 10. 16. 23:20

1. 카메라 기능 사용 및 사진 저장

 

"기존의 카메라 앱 (핸드폰에 설치되어 있는 카메라)를 호출하여 사진을 찍는 방법 "

 

intent의 암시적 호출 방법으로, 카메라를 호출하여 찍은 사진을 폴더에 저장할 수 있다.


1. Android.Manifest.xml 수정

 

카메라와 저장소는 개인정보와 관련된 기능으로 위험 권한에 속한다.

따라서, Android.Manifest.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"/> <!--카메라 기능 사용-->

안드로이드에서는 안전하게 앱 간 파일 전송하는 방법으로 콘텐츠 URI를 전송하는 방법을 사용한다.

FileProvider 라이브러리는 지정된 파일의 콘텐츠 URI를 생성할 수 있는 getUriForFile() 메서드를 제공한다.

<application> 태그 안에 <provider> 태그의 FileProvider 설정 코드를 추가해준다.

<application>
	...
	<provider
            android:authorities="com.example.usecamera.fileprovider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
   </provider>
</application>

<완성된 manifest.xml 코드>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.usecamera">

    <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"/> <!--카메라 기능 사용-->

    <application android:requestLegacyExternalStorage="true"
        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.UseCamera">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <provider
            android:authorities="com.example.usecamera.fileprovider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

    </application>

</manifest>

2. app/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="my_images"
        path="Android/data/com.example.usecamera/files/Pictures"/>
</paths>

3. activity_main.xml 파일 수정 (레이아웃 파일)

 

안드로이드의 mainActivity.java 파일에서 해당 버튼을 클릭 시의 이벤트 처리는 버튼의 id 값을 가져와 처리하기 때문에, 버튼의 id를 지정해주어야 한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <Button
        android:id="@+id/btn_camera"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="카메라 촬영"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <ImageView
        android:id="@+id/iv_profile"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/btn_camera"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_launcher_background" />
</androidx.constraintlayout.widget.ConstraintLayout>

4. MainActivity.kt 파일 작성

 

안드로이드의 사진 찍기 -> 찍은 사진 저장에 있어 핵심적인 역할들을 수행하게 만드는 코드들이 있다.

 

<권한 코드 작성>

  • (사전작업)
    권한에 대해 사용자에게 물어보는 코드를 짜기 위한 tedpermission을 사용하기 위해, 
    build.gradle(:app)에 다음과 같은 코드를 추가해 준다.
    dependencies {
        // TedPermission
        implementation 'gun0912.ted:tedpermission:2.2.3'
    }​
/*테드 퍼미션 실행 */
    private fun setPermission() {
        val permission=object : PermissionListener{
            override fun onPermissionGranted() { //설정해놓은 위험 권한들이 허용 되었을 경우 수행함
                Toast.makeText(this@MainActivity,"권한이 허용 되었습니다.",Toast.LENGTH_SHORT).show()
            }

            override fun onPermissionDenied(deniedPermissions: ArrayList<String>?) { //설정해놓은 위험권한 중 거부를 한 경우
                Toast.makeText(this@MainActivity,"권한이 거부 되었습니다.",Toast.LENGTH_SHORT).show()
            }
        }

        TedPermission.with(this)
            .setPermissionListener(permission)
            .setRationaleMessage("카메라 앱을 사용하려면 권한을 허용해 주세요.")
            .setDeniedMessage("권한을 거부하셨습니다. [앱 설정] -> [권한] 항목에서 허용해주세요")
            .setPermissions(android.Manifest.permission.WRITE_EXTERNAL_STORAGE,android.Manifest.permission.CAMERA)
            .check()
    }

위와 같은 코드를 작성하면, 어플을 처음 실행 시에 다음과 같은 권한 요청 메시지가 나타난다.

사용자가 권한을 수락하지 않으면, 앱에서 안내 메시지가 출력되며 종료된다.

권한 요청 승인 메시지 알림


<버튼 이벤트 (카메라 앱 실행) 코드 작성>

 

Intent는 암시적 호출 방법으로 카메라 앱을 호출한다. resolveActivity() 메서드는 암시적 인텐트 호출 대상 앱이 기기에 존재하는지를 확인한다. 만약, 카메라 앱이 존재하지 않고, startActivityForResult() 함수로 액티비티를 실행하게 된다면 프로그램이 종료되게 된다. 따라서 이를 방지하기 위해 resolveActivity() 메서드를 사용한다. 

그러나, StartAcitivyForResult, onActivityResult는 deprecated 처리가 된 메서드로, 이 두 메서드는 registerForActivityResult launcher로 대체하여 사용해주어야 한다.

 

카메라 앱을 실행한 후, 원본 사진 저장할 uri를 지정하고 싶은 경우 intent.putExtra(Media.EXTRA_OUTPUT, Uri)의 Uri 부분에 Content의 Uri를 입력해주면 된다. 원하는 경로에 파일을 생성하고, FileProvider.getUriForFile() 메서드를 사용하여 Content의 Uri 생성이 가능하다. 이때, Manifest의 FileProvider에서 정의한 Authority와 동일하게 UriForFile()의 두 번째 변수를 입력해주어야 한다.

class MainActivity : AppCompatActivity() {
    private var mBinding: ActivityMainBinding? = null
    private val binding get() = mBinding!!

    lateinit var curPhotoPath : String  //문자열 형태의 사진 경로 값

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setPermission() // 최초 권한 체크

        binding.btnCamera.setOnClickListener {
            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,
                        "com.example.usecamera.fileprovider",
                        it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    launcher.launch(takePictureIntent)
                }
            }
        }
    }
override fun onDestroy() {
        mBinding = null
        super.onDestroy()
    }


    val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val bitmap: Bitmap
            val file = File(curPhotoPath)
            if (Build.VERSION.SDK_INT < 28) {
                bitmap = MediaStore.Images.Media.getBitmap(contentResolver, Uri.fromFile(file))
                binding.ivProfile.setImageBitmap(bitmap)
            } else {
                val decode = ImageDecoder.createSource(
                    this.contentResolver,
                    Uri.fromFile(file)
                )
                bitmap = ImageDecoder.decodeBitmap(decode)
                binding.ivProfile.setImageBitmap(bitmap)
            }
            savePhoto(bitmap)
        }
    }

<찍은 사진 저장하는 코드 작성>

 

  /**
     * 이미지 파일 생성
     */
    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 {  curPhotoPath = absolutePath}
    }
    
     private fun savePhoto(bitmap: Bitmap) {
        val folderPath = Environment.getExternalStorageDirectory().absolutePath + "/Pictures/"
        val timestamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val fileName = "${timestamp}.jpeg"
        val folder = File(folderPath)
        if (!folder.isDirectory) {
            folder.mkdirs()
        }

        val out = FileOutputStream(folderPath + fileName)
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
        Toast.makeText(this, "사진이 앨범에 저장되었습니다.", Toast.LENGTH_LONG).show()

    }

<완성된 MainActivity.kt 코드>

class MainActivity : AppCompatActivity() {
    private var mBinding: ActivityMainBinding? = null
    private val binding get() = mBinding!!

    lateinit var curPhotoPath : String  //문자열 형태의 사진 경로 값

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setPermission() // 최초 권한 체크

        binding.btnCamera.setOnClickListener {
            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,
                        "com.example.usecamera.fileprovider",
                        it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    launcher.launch(takePictureIntent)
                }
            }
        }
    }

    /**
     * 이미지 파일 생성
     */
    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 {  curPhotoPath = absolutePath}
    }

    /**
     * 테드 퍼미션 설정
     */
    private fun setPermission() {
        val permission = object : PermissionListener {
            override fun onPermissionGranted() {    // 설정해놓은 위험 권한들이 허용 되었을 경우에 이 곳을 수행
                Toast.makeText(this@MainActivity, "권한이 허용되었습니다.", Toast.LENGTH_LONG).show()
            }

            override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {  // 설정해놓은 위험 권한을 거부한 경우에 이 곳을 수행
                Toast.makeText(this@MainActivity, "권한이 거부되었습니다.", Toast.LENGTH_LONG).show()
            }
        }

        TedPermission.with(this)
            .setPermissionListener(permission)
            .setRationaleMessage("카메라 앱을 사용하시려면 권한을 허용해주세요.")
            .setDeniedMessage("권한을 거부하셨습니다. [앱 설정] -> [권한] 항목에서 허용해주세요.")
            .setPermissions(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.CAMERA)
            .check()
    }

    override fun onDestroy() {
        mBinding = null
        super.onDestroy()
    }


    val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val bitmap: Bitmap
            val file = File(curPhotoPath)
            if (Build.VERSION.SDK_INT < 28) {
                bitmap = MediaStore.Images.Media.getBitmap(contentResolver, Uri.fromFile(file))
                binding.ivProfile.setImageBitmap(bitmap)
            } else {
                val decode = ImageDecoder.createSource(
                    this.contentResolver,
                    Uri.fromFile(file)
                )
                bitmap = ImageDecoder.decodeBitmap(decode)
                binding.ivProfile.setImageBitmap(bitmap)
            }
            savePhoto(bitmap)
        }
    }

    private fun savePhoto(bitmap: Bitmap) {
        val folderPath = Environment.getExternalStorageDirectory().absolutePath + "/Pictures/"
        val timestamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val fileName = "${timestamp}.jpeg"
        val folder = File(folderPath)
        if (!folder.isDirectory) {
            folder.mkdirs()
        }

        val out = FileOutputStream(folderPath + fileName)
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
        Toast.makeText(this, "사진이 앨범에 저장되었습니다.", Toast.LENGTH_LONG).show()

    }
}

 


2. firebase

 

1. Firebase란?

 

Firebase는 웹과 모바일 개발에 필요한 기능을 제공하는 Baas(Backend as a Service)이다.

즉, 백엔드 개발을 통해 서버를 따로 설계/구현하지 않고, 프론트엔드 개발에 집중할 수 있도록 도와주는 서비스이다.


2. Firebase의 대표적인 기능

 

firebase의 대표적인 기능

 

Firebase는 크게 제품 개발과 제품 성장을 도와주는 두 가지 콘텐츠를 제공한다.

 

제품 개발을 돕는 기능 제품 성장을 돕는 기능
실시간 데이터베이스, 인증, 클라우드 저장소, 클라우드 함수, 안드로이드용 test lab, 호스팅, 성능모니터링, 오류 보고 Google 애널리틱스, 동적링크, 초대, AdMob, 클라우드 메시징, 원격 구성, 애드워즈

3. Firebase의 장점

 

1. 인증 시스템을 지원한다.

 

인증은 firebase에서 로그인을 담당하는 부분이다. 로그인을 담당하는 부분을 직접 서버로 개발할 경우, 인증된 사용자인지 확인하는 세션 처리, 그 세션으로 데이터베이스/저장소에 접근해도 문제가 없는지 확인하는 보안 처리, 비밀번호/아이디 찾기, 비밀번호 바꾸기 등 복잡한 것을 구축해야 한다. 그런데, firebase는 이 모든 것들을 지원한다.

 

2. Firebase는 NoSQL 기반의 3세대 데이터베이스이다.

 

오라클, MySQL과 같은 관계형 데이터베이스보다 firebase는 document 형식의 빠르고 간편한 NoSQL 기반의 데이터베이스를 도입하였다. 또한, firebase는 다른 데이터베이스와 달리 RTSP(Real Time Stream Protocol) 방식의 데이터베이스를 지원하고 있다. RTSP는 실시간으로 데이터들을 전송해주는 방식을 말하는데, 이 방식을 사용하면, 소켓 기반의 서버를 만들어서 통신하는 것보다 비약적으로 코드의 양이 줄어 코드 몇 줄만으로도 원하는 구성을 만들 수 있다.

 

3. 원격 구성을 지원한다.

 

원격 구성은 원격으로 앱의 환경상태를 구성하는 것을 말한다. 앱의 배경화면 테마/폰트를 바꾼다거나, 업데이트창, 알림 창을 띄우는 것과 같이, 앱의 환경을 원격으로 구성할 때 사용하는 기능이다.

 

4. 콘솔을 제공한다.

 

firebase는 서버 관리자 페이지로 생각하면 되는 콘솔을 제공한다. 앱의 서버를 만들게 되면, 리눅스, ftp, mysql, nodejs, 서버, spring서버, push 보내기, api 등을 구축하는 것뿐만 아니라 이 모든 것을 관리할 수 있는 관리자 페이지가 필요하다. 그러나 firebase는 위와 같은 모든 것을 지원해 준다.

 

5. Analytics를 제공한다.

 

앱의 현재 접속자부터 오류 통계, 사용자 유지율, 고객들의 앱 업데이트 상태, 사용자가 특정 페이지에 머문 시간, 이벤트 등을 추적할 수 있다. 이러한 데이터들을 수집하여 사용자가 어떤 페이지에서 흥미를 잃었는지, 어떤 페이지가 인기가 많은지 등을 찾아낼 수 있으며 맞춤 마케팅을 할 수 있다.


 

 firebase에 대한 추가적인 자세한 내용은 다음의 링크에서 확인할 수 있습니다.

 

Firebase Products

Firebase는 고품질 앱을 빠르게 개발하고 비즈니스를 성장시키는 데 도움이 되는 Google의 모바일 플랫폼입니다.

firebase.google.com