관리 메뉴

나만을 위한 블로그

[Android] 안드로이드 13에서 Photo Picker 사용하는 법 본문

Android

[Android] 안드로이드 13에서 Photo Picker 사용하는 법

참깨빵위에참깨빵 2023. 8. 27. 23:54
728x90
반응형

24.07.20) 영상 크기 오류로 인해 영상 제거

 

안드로이드 13부터 WRITE_EXTERNAL_STORAGE 권한을 요청하면 무조건 거부된다. 그래서 오늘 날짜 기준으로 저번주에 이미지 선택 라이브러리로 자주 사용하는 TedBottomPicker에 이와 관련된 이슈가 올라왔다.

 

https://github.com/ParkSangGwon/TedBottomPicker/issues/155

 

android 13 WRITE_EXTERNAL_STORAGE 퍼미션 문제 · Issue #155 · ParkSangGwon/TedBottomPicker

android 13에서 WRITE_EXTERNAL_STORAGE 퍼미션 문제가 발생합니다.

github.com

 

앱 gradle에서 targetSdkVersion을 33으로 올린 후에 이 라이브러리를 사용하면 작동하지 않는다.

개인적인 생각으로는 위에서 말했듯 안드로이드 13부터 WRITE_EXTERNAL_SCOPE 권한을 요청하고 유저가 허용하더라도 거부된 것으로 처리된다. 이 현상 때문에 TedBottomPicker 라이브러리 안의 함수 중 아래 부분에서 예외가 던져진다.

 

public TedBottomSheetDialogFragment create() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
            && ContextCompat.checkSelfPermission(fragmentActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        throw new RuntimeException("Missing required WRITE_EXTERNAL_STORAGE permission. Did you remember to request it first?");
    }
    .
    .
    .
}

 

그래서 작동을 하지 않는 것 같은데, 라이브러리기 때문에 만드신 분이 수정해주셔야 하지만 그 전까지 안드로이드 13에선 손가락만 빨고 있을 순 없다. 안드로이드 13부터 지원하는 Photo Picker를 사용해서 사진, 영상을 선택하게 할 수 있다.

 

https://developer.android.com/about/versions/13/features/photopicker?hl=ko 

 

사진 선택 도구  |  Android 개발자  |  Android Developers

이제 Android 14 베타를 사용할 수 있습니다. 지금 사용해 보시고 의견을 알려 주세요. 사진 선택 도구 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 그림 1.

developer.android.com

 

이것을 사용할 때의 장점은 아래와 같다.

 

  • 런타임 권한을 요청하지 않아도 사진, 영상을 가져올 수 있다
  • 선택 가능한 사진 개수 제한 가능 (최소 1장)

 

권한을 따로 요청하지 않아도 사진, 영상을 가져올 수 있다는 건 엄청난 장점이다. 비록 안드로이드 13 미만 버전에선 여전히 권한을 요청해서 기존 로직을 써야 하지만, 안드로이드 13 이상만을 대상으로 하는 앱이라면 사진, 영상과 관련한 런타임 권한 요청 로직이 필요없어진다.

최대 선택 가능한 사진 개수도 설정할 수 있으니 개수 제한을 거는 것도 편하다.

 

아래는 전체 액티비티 코드다. 앱 gradle에서 33으로 버전을 설정하고 Sync now를 눌러 동기화해주기만 하면 곧바로 사용할 수 있다.

참고로 로그 라이브러리로 Timber를 사용했으니 이 라이브러리를 사용하고 있지 않다면 Log 클래스로 바꿔주면 된다.

 

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".photopicker.PhotoPickerTestActivity">

        <Button
            android:id="@+id/btnSelectSingleImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="단일 이미지 선택"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

        <Button
            android:id="@+id/btnSelectMultipleImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="여러 이미지 선택"
            app:layout_constraintTop_toBottomOf="@+id/btnSelectSingleImage"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

        <Button
            android:id="@+id/btnSelectVideo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="단일 동영상 선택"
            app:layout_constraintTop_toBottomOf="@+id/btnSelectMultipleImage"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import com.example.kotlinprac.BaseActivity
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityPhotoPickerTestBinding
import timber.log.Timber

class PhotoPickerTestActivity :
    BaseActivity<ActivityPhotoPickerTestBinding>(R.layout.activity_photo_picker_test) {

    private lateinit var pickSingleMediaLauncher: ActivityResultLauncher<Intent>
    private lateinit var pickMultipleMediaLauncher: ActivityResultLauncher<Intent>

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        pickSingleMediaLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == RESULT_OK) {
                    it.data?.data?.let { uri ->
                        Timber.e("## uri : $uri")
                    }
                }
            }

        pickMultipleMediaLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == RESULT_OK) {
                    val uris = it.data?.clipData ?: return@registerForActivityResult
                    var uriPaths = ""
                    for (index in 0 until uris.itemCount) {
                        uriPaths += uris.getItemAt(index).uri.path
                        Timber.e("## uriPaths : $uriPaths")
                    }
                }
            }

        bind {
            btnSelectSingleImage.setOnClickListener {
                pickSingleMediaLauncher.launch(
                    Intent(MediaStore.ACTION_PICK_IMAGES).apply {
                        type = "image/*"
                    })
            }

            btnSelectMultipleImage.setOnClickListener {
                pickSingleMediaLauncher.launch(
                    Intent(MediaStore.ACTION_PICK_IMAGES).apply {
                        type = "image/*"
                        putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 3) // 최대 3장까지
                    })
            }

            btnSelectVideo.setOnClickListener {
                pickSingleMediaLauncher.launch(
                    Intent(MediaStore.ACTION_PICK_IMAGES)
                        .apply {
                            type = "video/*"
                        }
                )
            }
        }
    }
}

 

 

단일 이미지 선택 버튼을 눌렀을 경우 사진의 uri는 아래와 같은 형태다.

 

content://media/picker/0/com.android.providers.media.photopicker/media/1000000044

 

앞부분이 content로 시작하는 것을 볼 수 있다. 일부 크롭 라이브러리의 경우 이렇게 시작하는 URI를 인식하지 못할 수 있으니 실제 파일 경로로 변환해주는 헬퍼 함수가 필요하다.

 

아래는 여러 이미지 선택 버튼을 눌렀을 경우다.

최대 3장으로 선택 가능한 사진 개수 제한을 걸었기 때문에 3장 이상 선택하려고 하면 하단에 스낵바 같은 팝업이 표시된다. 한글로 언어를 변경했다면 "최대 3개 항목을 선택하세요" 라고 표시될 것이다.

이후 Add 버튼을 누르고 로그캣을 확인하면 선택한 사진들의 URI들을 로그로 확인할 수 있다.

 

단일 동영상 선택 버튼을 눌렀을 때는 단일 이미지 선택 버튼을 눌렀을 때와 동일하게 작동한다. 동영상만 선택 가능한 바텀 시트가 올라오고, 영상을 선택하면 그 영상의 URI를 얻을 수 있다. URI의 형태도 이미지 선택 후 얻은 URI들과 같은 형태다.

 

귀찮아진 것은 기존 앱 로직 중 영상 압축이나 이미지 크롭 등의 로직이 있다면 이 방법으로 얻은 URI를 곧바로 쓸 수 없을 수도 있기에, 한 번 가공해서 기존 로직으로 넘겨야 할 수 있다는 것이다.

빨리 안드로이드 13에서도 TedBottomPicker를 사용할 수 있게 되기를 기원한다.

반응형
Comments