Dexter 라이브러리&기존퍼미션&카메라/갤러리 이미지 처리&내부 저장소 저장

소정·2024년 11월 7일
0

Kotlin

목록 보기
32/40

1. 덱스터 라이브러리?

퍼미션을 도와주는 라이브러리
https://github.com/Karumi/Dexter

2. 사용 방법

1. gradle에 추가

내 프로젝트는 현재 libs 형식이라 아래와 같이 임플리먼트함

implementation (libs.dexter)

2. manifest에 퍼미션 추가

받아야하는 퍼미션 적기

	<!--
	앱에서 카메라 기능을 사용하지만, 카메라가 필수적인 기능은 아니라는 것
    android:required="false"로 설정하면, 카메라가 없는 기기에서도 앱을 설치 가능
	-->
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />

    <!--
    - 외부 저장소에 저장된 파일을 읽기 위한 권한
    - Android 13 이전 버전에서는 이미지나 동영상 등의 파일에 접근하려면 이 권한이 필요
    - Android 13(API 33)부터는 READ_EXTERNAL_STORAGE 대신 특정 미디어 유형에 접근하는
    새로운 권한(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO)을 사용해야함
    -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <!--
    - 외부 저장소에 파일을 쓰기 위한 권한
    - Android 10(API 29)부터는 이 권한이 더 이상 필요하지 않고,
    대신 Scoped Storage가 도입되어 앱이 특정 폴더나 자체 저장소에 접근하도록 변경
    -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!--
    - 기기의 외부 저장소에서 이미지 파일에 접근하기 위한 권한
    - Android 13(API 33) 이상에서만 적용
    - tools:ignore="SelectedPhotoAccess" 속성은 Lint 경고를 무시하는 설정으로,
    특정 파일에 접근할 때 선택적인 접근 권한을 설정하는 것과 관련
    -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
        tools:ignore="SelectedPhotoAccess" />
    <!--
    - 외부 저장소에서 비디오 파일에 접근하기 위한 권한
    - Android 13(API 33) 이상에서 비디오 파일을 읽기 위해 필요
    -->
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
    <!--
    - 카메라 기능을 사용하기 위한 권한
    - 앱이 기기 카메라에 접근하고 사진 또는 비디오를 촬영할 수 있도록 허용
    -->
    <uses-permission android:name="android.permission.CAMERA"/>

3. 코드 작성


멀티 퍼미션 코드 사용
현재 나의 프로젝트는 minSdk가 26 이고 targetSdk는 33임
안드로이드 13이상과 받아야 할 퍼미션을 나눔

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private fun choosePhotoFromGallery() {
        //멀티 퍼미션 도와주는 라이브러리 - 덱스터
        // https://github.com/Karumi/Dexter

        //안드로이드 13이산과 미만 버전 권한 나누기
        val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            listOf(
                Manifest.permission.CAMERA,
                Manifest.permission.READ_MEDIA_IMAGES,
                Manifest.permission.READ_MEDIA_VIDEO
            )
        } else {
            listOf(
                Manifest.permission.CAMERA,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            )
        }


        Dexter.withContext(this).withPermissions(permissions)
            .withListener(object : MultiplePermissionsListener {
                override fun onPermissionsChecked(report: MultiplePermissionsReport?)
                {
                    //모든 퍼미션 부여됐을 경우
                    if (report != null) {
                        if (report.areAllPermissionsGranted()){
                            Toast.makeText(this@HappyPlaceActivity, "갤러리 저장,읽기 쓰기 권한", Toast.LENGTH_SHORT).show()
                        }
                    }
                }

                // 유저에게 이 권한이 왜 필요한지 알려주는 부분
                override fun onPermissionRationaleShouldBeShown(
                    permission: MutableList<PermissionRequest>?,
                    permissionToken: PermissionToken?
                ) {
                    showRationalDialogForPermission() //사용자에게 이유 알려주기 위한 메서드
                }
        }).onSameThread().check()
    }

    private fun showRationalDialogForPermission() {
        AlertDialog.Builder(this).setMessage(
            "이 기능에 필요한 권한이 거절되었습니다. 앱 세팅에서 변경할 수 있습니다"
        ).setPositiveButton("Go to settings")
        {
            _,_->
            try {
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                val uri = Uri.fromParts("package", packageName,null) //앱설정 화면으로 넘어간 뒤 사용자 권한을 바꿀수 있게함
                intent.data = uri
                startActivity(intent)
            } catch (e:ActivityNotFoundException) {
                e.printStackTrace()
            }
        }.setNegativeButton("Cancel"){
            dialog, which->
            dialog.dismiss()
        }.show()
    }

참고) dexter없이 퍼미션 받기 & 카메라 퍼미션

1. 매니페스트에 카메라 퍼미션


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

2.카메라 사용 하기

  1. 퍼미션 확인용 상수와 현재는 퍼미션 카메라만 받고 있지만 여러 퍼미션을 받을 경우 분류용으로 사용 할 상수 준비
  2. ContextCompat.checkSelfPermission 함수를 사용하여 퍼미션 체크 되었는지 확인
    퍼미션 수락했으면 카메라 실행, 수락하지 않았으면 퍼미션 요청 파업 띄우기
    2-1) 퍼미션 수락 : 더이상 퍼미션을 묻지않고 바로 Intent를 사용하여 카메라 실행
    startActivityForResult로 카메라로 찍어온 데이터 onActivityResult() 함수로 전달한다
    2-2) 퍼미션 미수락 : ActivityCompat.requestPermissions를 사용해 받아야 하는 퍼미션 목록을 보내고 퍼미션 요청 팝업을 띄운다.
    onRequestPermissionsResult() 함수에서 퍼미션 결과 처리

onActivityResult()

1.카메라 데이터 처리

  • data.extras에서 Bitmap 썸네일을 직접 가져옴. 썸네일 크기이며 메모리 효율적.
  • 카메라로 사진을 찍으면 Intent extras에 작은 크기의 썸네일 이미지가 Bitmap 형태로 저장

2.갤러리 데이터 처리

  • data.data를 사용해 선택한 이미지의 URI를 가져오고, contentResolver를 통해 이미지를 불러옴. 고해상도 이미지로 메모리를 더 사용

    ContentResolver 클래스
    앱 간 데이터 접근을 관리하는 클래스
    외부 저장소나 다른 앱(ex. 갤러리 이미지, 연락처 정보, 미디어 파일 등)에 저장된 데이터에 접근할 때 사용
    contentResolver는 Content Provider와 연결하여 데이터를 읽거나 쓰는 데 필요한 인터페이스 역할
    contentResolver는 content://로 시작하는 URI를 통해 외부 저장소의 데이터를 접근 (ex. 갤러리의 이미지 URI : content://media/external/images/media/)


package com.airapssinsj.cameraimagedemo

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class MainActivity : AppCompatActivity() {

    //상수 저장할 객체 필요
    companion object {
        private const val CAMERA_PERMISSION_CODE = 1
        private const val CAMERA_REQUEST_CODE = 2 //요청 코드
    }

    private val ivImage : AppCompatImageView by lazy { findViewById(R.id.iv_image) }
    private val btnCamera : Button by lazy { findViewById(R.id.btn_camera) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        btnCamera.setOnClickListener {
            if (ContextCompat.checkSelfPermission(
                    this,Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED){
                val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                startActivityForResult(intent, CAMERA_REQUEST_CODE)
            } else {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.CAMERA),
                    CAMERA_PERMISSION_CODE
                )
            }
        }

    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode == CAMERA_PERMISSION_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //권한 부여
                val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                startActivityForResult(intent, CAMERA_REQUEST_CODE)
            } else {
                //권한 부여 못받았을 때
                Toast.makeText(this, "카메라 접근이 거부되었습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }

    //활동 결과와 카메라 사용 두가지 활용 결과
    //결과값이 있으면 자동으로 불러지는 함수
    //startActivityForResult가 실행되고 난 뒤의 결과 데이터를 가져옴
    //카메라에서 이미지 가져오기
    @Deprecated("This method has been deprecated in favor of using the Activity Result API\n      which brings increased type safety via an {@link ActivityResultContract} and the prebuilt\n      contracts for common intents available in\n      {@link androidx.activity.result.contract.ActivityResultContracts}, provides hooks for\n      testing, and allow receiving results in separate, testable classes independent from your\n      activity. Use\n      {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}\n      with the appropriate {@link ActivityResultContract} and handling the result in the\n      {@link ActivityResultCallback#onActivityResult(Object) callback}.")
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode == Activity.RESULT_OK) { //활동이 성공적으로 완료
            if (requestCode == CAMERA_REQUEST_CODE) { //startActivityForResult에 지정한 요청 코드(CAMERA_REQUEST_CODE)랑 비교
                val thumbnail : Bitmap = data!!.extras!!.get("data") as Bitmap // data:요청한 활동이 종료되면서 전달하는 추가적인 데이터, 카메라 요청의 경우 Intent 객체가 반환, ata가 null일 수도 있으니, 안전하게 사용하려면 null 체크
                ivImage.setImageBitmap(thumbnail)
            }
        }
    }
    
    //갤러리에서 이미지 가져오기
	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    
    if (resultCode == Activity.RESULT_OK) {
        if (requestCode == GALLERY) {
            if (data != null) {
                val contentURI = data.data
                try {
                    val selectedImageBitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, contentURI)
                    ivImage.setImageBitmap(selectedImageBitmap)
                } catch (e: IOException) {
                    e.printStackTrace()
                    Toast.makeText(this, "갤러리에서 사진을 불러오지 못했습니다.", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

}

라이브러리없이 다중 퍼미션 처리 예제

private fun choosePhotoFromGallery() {
        //안드로이드 13이산과 미만 버전 권한 나누기
        val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            listOf(
                Manifest.permission.READ_MEDIA_IMAGES,
                Manifest.permission.READ_MEDIA_VIDEO
            )
        } else {
            listOf(
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            )
        }

        // 필요한 권한 중 아직 허용되지 않은 권한 필터링
        val deniedPermissions = permissions.filter { permission ->
            ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
        }

        if (deniedPermissions.isEmpty()) {
            // 모든 권한이 이미 허용된 경우
            Toast.makeText(this, "이미 모든 권한이 허용되었습니다.", Toast.LENGTH_SHORT).show()
        } else {
            // 허용되지 않은 권한을 요청
            requestPermissionLauncher.launch(deniedPermissions.toTypedArray())
        }
    }

    // 권한 요청 결과를 처리
    // registerForActivityResult : 액티비티 결과 처리하는 함수
    // ActivityResultContracts 계약서 들고 있는애
    // RequestMultiplePermissions 여러 계약 한번에 처리 하는 애
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
            // 모든 권한이 허용된 경우
            if (permissions.all { it.value }) {
                Toast.makeText(this, "갤러리 접근 권한이 허용되었습니다.", Toast.LENGTH_SHORT).show()
            } else {
                // 하나 이상의 권한이 거부된 경우
                showRationalDialogForPermission()
            }
        }

    private fun showRationalDialogForPermission() {
        AlertDialog.Builder(this).setMessage(
            "이 기능에 필요한 권한이 거절되었습니다. 앱 세팅에서 변경할 수 있습니다"
        ).setPositiveButton("Go to settings")
        {
            _,_->
            try {
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                val uri = Uri.fromParts("package", packageName,null) //앱설정 화면으로 넘어간 뒤 사용자 권한을 바꿀수 있게함
                intent.data = uri
                startActivity(intent)
            } catch (e:ActivityNotFoundException) {
                e.printStackTrace()
            }
        }.setNegativeButton("Cancel"){
            dialog, which->
            dialog.dismiss()
        }.show()
    }



3. 이미지 내부 저장소에 저장하기

  • 비트맵 이미지를 앱의 내부 저장소에 저장하고, 저장된 이미지의 URI를 반환

1.ContextWrapper는 컨텍스트에 접근할 수 있는 래퍼 클래스 준비

  • applicationContext로 앱의 컨텍스트를 사용해 내부 저장소에 안전하게 접근한다
    2.디렉터리 가져오기, getDir로 폴더를 생성하거나 가져옴
  • getDir(폴더 이름, Context.MODE_PRIVATE)
  • Context.MODE_PRIVATE : 이 앱 내에서만 접근 가능하게 하란것
    3.파일 생성 하기
  • File(디렉터리, 파일 생성 이름)
    4.파일 쓰기
  • 출력 스트림 만들기 : FileOutputStream(file)
  • 이미지 압축 및 저장 : bitmap.compress(저장 포멧, 압축품질(~100), 출력 스트림)
  • 스트림 저장 및 닫기 : stream.flush() / stream.close()

//찍은 사진 내가 지정한 폴더에 저장하기
val saveImageToInternalStorage = saveImageToInternalStorage(selectedImageBitmap)
Log.d("TAG", "save image path :: $saveImageToInternalStorage")
///data/user/0/com.airapssinsj.happyplaceapp/app_HappyPlaceImg/db7ec4f4-0c1a-4935-98a1-91e04c5cc66b.jpg


private fun saveImageToInternalStorage(bitmap: Bitmap) : Uri {
        val wrapper = ContextWrapper(applicationContext) //어플리케이션 컨텍스트 담은 변수
        var file = wrapper.getDir(IMAGE_DIRECTORY, Context.MODE_PRIVATE)
        file = File(file,"${UUID.randomUUID()}.jpg") //(file디렉터리 위치,압축해서 만든 비트맵 이름)"
        //파일 출력 스트림
        try {
            val stream : OutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) //(파일형식, 품질, 출력 스트림)
            stream.flush()
            stream.close()
        }catch (e:IOException) {
            e.printStackTrace()
        }
        return Uri.parse(file.absolutePath)
    }

applicationContext vs activity context

1. applicationContext

  • 앱 전체에서 공유되는 컨텍스트로, 앱의 생명주기에 맞춰 지속 (특정 액티비티 생명주기에 영향x)
  • 데이터베이스 인스턴스나 네트워크 연결 관리와 같은 앱의 전체적인 상태와 연결된 작업을 할 때 주로 사용

2. activity context

  • 특정 액티비티와 연결된 컨텍스트로, 액티비티의 생명주기에 종속
  • UI 업데이트나 특정 화면에 종속된 작업에 적합
profile
보조기억장치

0개의 댓글