[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 등장 이유?(영어)

 

 

 

작성자

Posted by 드리머즈

관련 글

댓글 영역