아니 왜!!!!!
이전 빌드한 앱에서 외부저장소(공용저장소)에 이미지파일을 저장한 게,
삭제 후 재설치한 앱에서 모든 이미지 삭제로직에서 삭제가 안돼....
왜 삭제가 안돼? 왜!!!!!
권한이 필요했다........
다른 앱 이미지 업데이트 불가
// 이슈 상황
private fun removeImageFiles(context: Context) {
val contentResolver: ContentResolver = context.contentResolver
val externalStorageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val folderPath = File(externalStorageDir, "후후쿠폰박스").absolutePath
val uri = MediaStore.Files.getContentUri("external")
// 삭제 대상 폴더를 검색
val selection = "${MediaStore.Files.FileColumns.DATA} like ?"
val selectionArgs = arrayOf("$folderPath%")
val cursor = contentResolver.query(uri, null, selection, selectionArgs, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val fileId = cursor.getLong(idColumn)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentUri = ContentUris.withAppendedId(uri, fileId)
val deleted = contentResolver.delete(contentUri, null, null)
if (deleted > 0) {
Log.d("Folder Deletion", "Deleted file with id: $fileId")
} else {
Log.d("Folder Deletion", "Could not delete file with id: $fileId")
}
}
}
cursor.close()
}
}
왜 안될까??
Android 공식 Document 를 확인해보면?
다른 앱이 액세스할 수 있나요?
예, 하지만 다른 앱에 READ_EXTERNAL_STORAGE 권한 필요
도표에 위와같이 적혀 있다.
이걸 보면, 반대로 WRITE_EXTERNAL_STORAGE 는 안된다고 생각해야 한다.
고로, 재설치한 앱에서 이전 앱에서 저장한 이미지를 삭제할 수가 없다.
그럼 어떻게 해야하는데?
정답은 권한이다.
권한을 주어 이미지를 삭제 허용을 주어야한다.
그래서 일단, 윗 코드에서 삭제가 되지 않은 걸 따로 이미지 저장하여, 권한 요청을해서 이미지를 삭제를 하는 로직을 추가하겠다.
1. 다른 앱 이미지 삭제(업데이트) 가능하게 끔 로직 수정 (사진 하나 씩 권한 요청 후 삭제)
1. 특정 폴더 안에 있는 모든 이미지를 삭제한다.
2. 삭제가 안되는 이미지가 존재한다 (타 앱에서 저장한 이미지)
3. 권한 문제 때문에 따로 저장되지 않은 이미지파일을 fileNotDeleted 리스트에 저장시킨다.
4. removeImageFilesOneByOne 함수에서 삭제되지 않은 이미지를 하나하나 RecoverableSecurityException 를 일으켜 이미지 write 권한 요청을한다.
5. 권한 허용을 하게 되면, 삭제가 된다.
private val launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// 성공 시,
} else {
// 실패 시,
}
}
private fun removeImageFiles(context: Context) : Boolean {
val filesNotDeleted = mutableListOf<String>()
val contentResolver: ContentResolver = context.contentResolver
val externalStorageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val folderPath = File(externalStorageDir, "후후쿠폰박스").absolutePath
val uri = MediaStore.Files.getContentUri("external")
// 삭제 대상 폴더를 검색
val selection = "${MediaStore.Files.FileColumns.DATA} like ?"
val selectionArgs = arrayOf("$folderPath%")
val cursor = contentResolver.query(uri, null, selection, selectionArgs, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val fileId = cursor.getLong(idColumn)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentUri = ContentUris.withAppendedId(uri, fileId)
// 현재 앱이 삭제할 수 있는 이미지 파일만 제거
val deleted = contentResolver.delete(contentUri, null, null)
if (deleted > 0) {
Log.d("Folder Deletion", "Deleted file with id: $fileId")
} else {
// 현재 앱이 삭제할 수 없는 이미지 파일은 권한을 받아 제거
val filePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)
val filePath = cursor.getString(filePathColumn)
Log.d("Folder Deletion", "Could not delete file with id: $fileId, File Path: $filePath")
filesNotDeleted.add(filePath)
}
}
}
cursor.close()
}
return if (filesNotDeleted.isEmpty()) true
else {
for (file in filesNotDeleted) removeImageFilesOneByOne(context, file)
false
}
}
// 이미지 파일 하나씩 권한 허용을 통해 삭제
private fun removeImageFilesOneByOne(context: Context, path: String) {
val contentResolver: ContentResolver = context.contentResolver
val selection = "${MediaStore.Images.Media.DATA} = ?"
val selectionArgs = arrayOf(path)
val projection = arrayOf(MediaStore.Images.Media._ID)
val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val fileId = cursor.getLong(idColumn)
try {
val contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileId)
val deleted = contentResolver.delete(contentUri, null, null)
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && e is RecoverableSecurityException) {
Log.d("File Deletion", "RecoverableSecurityException: $e ")
val intentSender = e.userAction.actionIntent.intentSender
launcher.launch(IntentSenderRequest.Builder(intentSender).build())
}
}
}
cursor.close()
} else {
Log.d("File Deletion", "Cursor is null")
}
}
그런데..
사진이 70장이면?
권한 허용 버튼을 70번 눌러야한다.
광클!!!!!!!!!
한 번에 삭제할 수 있는 방법 없어?
있다! 당연히 있다.
아래와 같은 방법을 보자.
2. 다른 앱 이미지 업데이트를 하되, 한 번 권한 요청으로 여러 장을 삭제
1. 특정 폴더 안에 있는 모든 이미지를 삭제한다.
2. 삭제가 안되는 이미지가 존재한다 (타 앱에서 저장한 이미지)
3. 권한 문제 때문에 따로 저장되지 않은 이미지파일을 fileNotDeleted 리스트에 저장시킨다.
4. removeAllImageFiles 함수에서 삭제되지 않은 이미지를 하나하나 path -> uri 를 변환해서 uris 리스트에 담는다.
5. Uris 에 담긴 삭제되지 않은 이미지 uri 들을 deleteImageCheckedPermission 함수에서 한 번에 MediaStore.createDeleteRequest 를 통해 권한요청을 진행하여 삭제한다.
// 이미지 파일 한 번에 삭제하는 방법
private val launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// 성공 시,
} else {
// 실패 시,
}
}
private fun removeImageFiles(context: Context) : Boolean {
val filesNotDeleted = mutableListOf<String>()
val contentResolver: ContentResolver = context.contentResolver
val externalStorageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val folderPath = File(externalStorageDir, "후후쿠폰박스").absolutePath
val uri = MediaStore.Files.getContentUri("external")
// 삭제 대상 폴더를 검색
val selection = "${MediaStore.Files.FileColumns.DATA} like ?"
val selectionArgs = arrayOf("$folderPath%")
val cursor = contentResolver.query(uri, null, selection, selectionArgs, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val fileId = cursor.getLong(idColumn)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentUri = ContentUris.withAppendedId(uri, fileId)
// 현재 앱이 삭제할 수 있는 이미지 파일만 제거
val deleted = contentResolver.delete(contentUri, null, null)
if (deleted > 0) {
Log.d("Folder Deletion", "Deleted file with id: $fileId")
} else {
// 현재 앱이 삭제할 수 없는 이미지 파일은 권한을 받아 제거
val filePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)
val filePath = cursor.getString(filePathColumn)
Log.d("Folder Deletion", "Could not delete file with id: $fileId, File Path: $filePath")
filesNotDeleted.add(filePath)
}
}
}
cursor.close()
}
return if (filesNotDeleted.isEmpty()) true
else {
val uris = mutableListOf<Uri>()
for (file in filesNotDeleted) {
removeAllImageFiles(context, file).let {
if (it != null) {
uris.add(it)
}
}
}
deleteImageCheckedPermission(uris)
false
}
}
private fun removeAllImageFiles(context: Context, path: String) : Uri? {
val contentResolver: ContentResolver = context.contentResolver
val selection = "${MediaStore.Images.Media.DATA} = ?"
val selectionArgs = arrayOf(path)
val projection = arrayOf(MediaStore.Images.Media._ID)
val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val fileId = cursor.getLong(idColumn)
try {
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fileId)
} catch (e: Exception) {
Log.d("File Deletion", "RecoverableSecurityException: $e ")
}
}
cursor.close()
} else {
Log.d("File Deletion", "Cursor is null")
}
return null
}
private fun deleteImageCheckedPermission(uris: List<Uri>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val pendingIntent = MediaStore.createDeleteRequest(requireContext().contentResolver, uris.filter {
requireContext().checkUriPermission(
it,
Binder.getCallingPid(),
Binder.getCallingUid(),
Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PackageManager.PERMISSION_GRANTED
})
launcher.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build())
}
}
참고
https://developer.android.com/training/data-storage/shared/media?hl=ko
https://brunch.co.kr/@huewu/14