본문 바로가기
Mobile App/Android

[안드로이드] 이전에 설치한 앱 이미지 파일이 재설치 후 왜 삭제가 안될까? (다른 앱 이미지 삭제 불가) *갤러리 폴더 삭제

by Jman 2023. 11. 15.

 

아니 왜!!!!!
이전 빌드한 앱에서 외부저장소(공용저장소)에 이미지파일을 저장한 게,
삭제 후 재설치한 앱에서 모든 이미지 삭제로직에서 삭제가 안돼....

왜 삭제가 안돼? 왜!!!!!

권한이 필요했다........

 

다른 앱 이미지 업데이트 불가

 

 

모든 이미지가 삭제가 안되는 경우,

 

    // 이슈 상황
    
    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/reference/android/provider/MediaStore#createDeleteRequest(android.content.ContentResolver,%20java.util.Collection%3Candroid.net.Uri%3E)

 

MediaStore  |  Android Developers

 

developer.android.com

https://developer.android.com/training/data-storage/shared/media?hl=ko

 

공유 저장소의 미디어 파일에 액세스  |  Android 개발자  |  Android Developers

DataStore는 로컬 데이터를 저장하는 최신 방법을 제공합니다. SharedPreferences 대신 DataStore를 사용해야 합니다. 자세한 내용은 DataStore 가이드를 참고하세요. 공유 저장소의 미디어 파일에 액세스 컬

developer.android.com

https://hooun.tistory.com/103

 

Android Q 파일 삭제 ScopeStorage 삽질

이미지를 서버에 업로드후에 삭제해야 하는 이슈. * 이미지를 업로드 후에 다른 Task(runnable)에서 파일을 삭제 시켜주는 로직이 들어가 있음.(해당 액티비티에서 삭제가 아님) 나의 삽질을 적어둔

hooun.tistory.com

https://stackoverflow.com/questions/67798210/can-we-delete-an-image-file-using-mediastore-api-if-yes-then-how

 

Can we delete an image file using MediaStore API? if yes then how

I have a requirement to delete screenshot image file after a certain time using background service in my app and it was working fine using the above method private void deleteTheFile(String path) {...

stackoverflow.com

https://brunch.co.kr/@huewu/14

 

안드로이드 Q Scoped Storage에서 살아남기

개발자를 위한 안드로이드 Q #6 | 시작하기 전에 이 포스트는 안드로이드 Q 베타 2 패치 버전을 기준으로 작성되었습니다. Q 정식 버전에서는 기능 및 API가 변경될 수 있습니다. 기능에 관한 소감

brunch.co.kr

https://eitu97.tistory.com/64

 

Android 11 이상 버전에서 외부 저장소 이미지 삭제하기

Android R(API 30) 이상 버전부터는 WRITE_EXTERNAL_STORAGE 권한을 받을 수 없게되었다. 이에 따라 기존의 파일 저장 방식이나 삭제 방식을 사용할 수 없게 되었는데 이번 포스트에서는 그 해결법을 공유하

eitu97.tistory.com