MediaStore에서 Image/Video/Audio 등 미디어 파일 삭제하는 방법을 소개합니다. Query, Update 등과 유사하게, ContentResolver.delete() API로 MediaStore의 아이템을 삭제할 수 있습니다.
Android 10(TargetSDK 29)에서 Scoped Storage 적용으로, 어떤 파일을 삭제하려면 사용자로부터 허락을 받아야 할 수도 있습니다. 예외로, 앱이 추가한 미디어 파일은 허락없이 삭제가 가능합니다.
반면에, TargetSDK 29 미만에서 앱이 미디어 파일을 삭제하려면 WRITE_EXTERNAL_STORAGE
권한을 갖고 있어야 합니다.
Android 10과 그 미만의 SDK에서 어떻게 아이템을 삭제하는지 알아보겠습니다.
MediaStore의 아이템을 쿼리하는 방법은 MediaStore에서 미디어 파일 정보 읽기를, MediaStore에 아이템을 추가하는 방법은 MediaStore에 미디어 파일 저장하는 방법 참고해주세요.
미디어 파일 삭제 (Android 10)
Android 10에서 Scoped Storage의 적용으로 3rd party app은 WRITE_EXTERNAL_STORAGE
권한을 받을 필요가 없어졌습니다.
모든 앱은 WRITE_EXTERNAL_STORAGE
권한이 있어도 파일을 쓸 수 없기 때문입니다.
파일을 쓰려면, 필요할 때마다 사용자에게 권한을 받아야 합니다.
ContentResolver.delete()
는 MediaStore의 아이템을 삭제합니다.
앱이 등록한 미디어 파일은 사용자의 허락없이 삭제할 수 있고, 다른 앱에서 등록한 미디어 파일을 삭제하려면 사용자의 허락을 받아야 합니다.
앱이 직접 등록한 미디어 파일
앱이 직접 등록한 미디어 파일은 사용자 허락없이 삭제할 수 있습니다.
다음 코드는 특정 이름의 미디어 파일을 찾고, 그 Uri를 MediaStore에서 삭제하는 코드입니다.
// fileName 과 동일한 아이템을 찾은 후 Uri를 저장
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME
)
var displayName: String? = null
var contentUri: Uri? = null
contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
null
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
displayName = cursor.getString(displayNameColumn)
if (displayName != fileName) {
continue
}
contentUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id.toString()
)
}
}
// 찾은 Uri를 MediaStore에서 삭제
contentUri?.let{
contentResolver.delete(contentUri!!, null, null)
Log.d(TAG, "Removed $displayName from MediaStore: $contentUri")
}
다른 앱이 등록한 미디어 파일
만약 다른 앱이 저장한 미디어 파일이나, 사용자가 PC에서 디바이스에 저장한 미디어파일을 삭제하고 싶을 수도 있습니다. 이런 경우, 앱은 삭제할 수 있는 권한이 없습니다.
만약 위와 같이 삭제하려고 시도하면 RecoverableSecurityException
예외가 발생하며 실패합니다.
이 예외가 발생할 때 Media Provider는 RemoteAction 객체를 생성하여 Exception에 저장합니다.
RemoteAction에는 IntentSender 객체가 있으며, 이를 이용하여 사용자에게 이 파일을 삭제해도 되는지 묻는 팝업을 띄울 수 있습니다.
사용자가 확인 버튼을 누르면, 앱은 삭제 권한을 부여받게 되고, 다시 삭제를 시도하면 삭제가 성공합니다.
다음은 방금 말한 내용에 대한 예제입니다.
private const val DELETE_PERMISSION_REQUEST = 0x1033
try {
contentUri?.let{
contentResolver.delete(contentUri!!, null, null)
Log.d(TAG, "Removed $displayName from MediaStore: $contentUri")
}
} catch (e: RecoverableSecurityException) {
// 권한이 없기 때문에 예외가 발생됩니다.
// RemoteAction은 Exception과 함께 전달됩니다.
// RemoteAction에서 IntentSender 객체를 가져올 수 있습니다.
// startIntentSenderForResult()를 호출하여 팝업을 띄웁니다.
val intentSender = e.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(
intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
}
}
// 팝업이 종료되면, 결과가 onActivityResult로 콜백됩니다.
// 이 때 다시 삭제를 시도할 수 있습니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && requestCode == DELETE_PERMISSION_REQUEST) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
removeMediaFile("my_image.jpg")
}
}
}
IntentSender를 실행하면 다음과 같은 팝업이 보이며, 확인 버튼을 누르면 앱은 삭제 권한을 부여받습니다.
권한을 받으면 onActivityResult()로 콜백이 오며 다시 삭제를 시도할 수 있습니다.
에뮬레이터에서 위의 코드를 테스트했을 때 MediaStore에서 아이템이 삭제되는 것을 확인하였습니다.
하지만 adb shell로 그 파일이 저장된 실제 경로에 들어가면 파일은 삭제되지 않고 남아있습니다. 이 부분이 버그인지, 정책에 의한 동작인지는 확실히 모르겠습니다. 자신이 등록한 미디어 파일은 실제 파일까지 삭제되는 것으로 보아 실제 파일도 삭제되는 것이 올바른 동작인 것 같습니다.
미디어 파일 삭제 (Android 10 미만)
Android 10 미만에서 MediaStore의 파일을 삭제하려면 WRITE_EXTERNAL_STORAGE
권한을 받아야 합니다.
위와는 다르게 권한을 미리 받았기 때문에 다음과 같은 코드로 바로 삭제할 수 있습니다.
contentResolver.delete(contentUri!!, null, null)
이 글에서 사용한 코드는 GitHub - MediaStore에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력