[Android][scoped storage] open failed: EACCES (Permission denied)
안드로이드 스튜디오에서 AVD를 새로 만들고 테스트를 진행하던 중.. 갑자기 기존에는 발생하지 않던 에러(exception)이 발생했습니다.
W/System.err: java.io.FileNotFoundException: /storage/emulated/0/Download/photo-1579165466741-7f35e4755660.jpeg: open failed: EACCES (Permission denied)
에러 로그는 위와 같습니다.
접근 권한 에러가 발생하네요..
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
AndroidManifest.xml에 이미 퍼미션을 사용하고 있고.. 동적으로도 권한을 요청받기에 이상하다고 생각했습니다.
FileInputStream inputStream = new FileInputStream(originalFile);
에러가 발생한 위치는 위의 코드인데 originalFile이 가리키는 경로가.. /storage/emulated/0/Download/photo-1579165466741-7f35e4755660.jpeg 입니다.
안드로이드 10.0(Q, API29)부터 발생하는 문제인데 긴급 해결책은 아래의 답변을 참고하면 됩니다.
https://stackoverflow.com/a/57804657/7225691
<manifest ... >
<!-- This attribute is "false" by default on apps targeting Android Q. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
<application> 내부에 android:requestLegacyExternalStorage="true"를 추가하면 됩니다.
그런데 이건.. 임시해결방편이라고 봐야할 것 같습니다. 안드로이드가 10(Q)버전부터.. Scoped storage라 하여 파일 권한을 좀 더 엄격하게 처리하는 것 같습니다.(관련 내용: https://developer.android.com/training/data-storage#scoped-storage)
앱에서 External Storage 관련해서 마구잡이로 사용하다보니.. 그 앱을 지워도 관련 파일들은 남아있어 사용자가 용량 부족 문제를 겪는 것 때문에 그렇다고 합니다.. 흠.. 확실히 안드로이드 폰은 사용하다보면.. 불필요한 앱을 다 지워도 용량부족을 겪게 됩니다.. ㅜㅜ 이게 원인이 있었군요..
안드로이드는 앞으로? 앱에서만 쓰는 공간인 Internal Storage가 아닌 다른 영역, 즉 External Storage에 대해서는 scoped accesss into external storage(=scoped storage)라 하여.. 제한된 권한만 주려고 합니다.
음.. 좀 제대로 된 해결책은.. External Storage영역의 파일에 대하여 File로 접근하지 않는 것 같네요.
다운로드 폴더(/storage/emulated/0/Download)에 있는 파일을 접근(open)해야 한다면.. Internal Storage로 복사하여 File로 접근하면 됩니다.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Uri imgUri = data.getData();
String imgPath = getPath(getApplicationContext(), imgUri);
if(imgUri != null && imgPath != null){
InputStream in = getContentResolver().openInputStream(imgUri);//src
String extension = imgPath.substring(imgPath.lastIndexOf("."));
File localImgFile = new File(getApplicationContext().getFilesDir(), "localImgFile"+extension);
if(in != null) {
try {
OutputStream out = new FileOutputStream(localImgFile);//dst
try {
// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
//InternalStorage로 복사된 localImgFile을 통하여 File에 접근가능
}
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The activity.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
// DocumentProvider
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}else{
Toast.makeText(context, "Could not get file path. Please try again", Toast.LENGTH_SHORT).show();
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
} else {
contentUri = MediaStore.Files.getContentUri("external");
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
대략.. 위 코드와 같이 사용하면 될 것 같네요.
관련 글
scoped storage 등장 이유?(영어)
댓글 영역