Android

[안드로이드 폴더 접근 권한 오류 처리 방법 ACTION_OPEN_DOCUMENT 인텐트를 사용하라]java.io.FileNotFoundException: /storage/emulated/0/Download/test_bak.db: open failed: EACCES (Permission denied)

안드로이드 사용자들이 인터넷이든, 이메일 앱이든, 어디서든 다운받은 파일들은 Download 폴더에 저장되게 되어 있습니다. 이런 파일들에 가끔 접근해야할 필요가 있습니다.  그러나 구글은 이러한 공용 저장소 마져 직접적인 접근에 대한 제한을 시작했습니다.

 

안드로이드 10(API 29) 부터 범위 지정 저장소로 범위가 지정된 액세스 권한 또는 범위 지정 저장소가 부여됩니다. 

즉, 내가 만든 앱이 있을 때 , 이 앱의 외부 저정소의 앱별 디렉터리와 앱에서 만든 특정 유형의 미디어에만 액세스 할 수 있습니다. 쉽게 얘기하면, 내가 만든 앱이 설치된 경로이외에 다른 앱들이 설치된 디렉터리 , 다운로드 폴더, 이미지 폴더 등에 접근할 수 없다는 얘기입니다. 

 

다음 오류는 스마트폰의 다운로드(Download) 폴더에 접근하여 파일을 가져오려고 했을 때 발생된 오류입니다.

다운로드 폴더에 접근 권한이 없기 때문에 FileNotFoundException이 발생합니다. 파일이 없어서 발생한 오류가 아니지요.

java.io.FileNotFoundException: /storage/emulated/0/Download/test_bak.db: open failed: EACCES (Permission denied)
 W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:492)
 W/System.err:     at java.io.FileInputStream.<init>(FileInputStream.java:160)
 W/System.err:     at com.test.SettingActivity.restoreDB(SettingActivity.java:286)
 W/System.err:     at com.test.SettingActivity.access$100(SettingActivity.java:70)
 W/System.err:     at com.test.SettingActivity$5.onClick(SettingActivity.java:404)
  W/System.err:     at android.view.View.performClick(View.java:7448)
  W/System.err:     at android.view.View.performClickInternal(View.java:7425)
  W/System.err:     at android.view.View.access$3600(View.java:810)
  W/System.err:     at android.view.View$PerformClick.run(View.java:28305)
  W/System.err:     at android.os.Handler.handleCallback(Handler.java:938)
  W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)
  W/System.err:     at android.os.Looper.loop(Looper.java:223)
  W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:7656)
  W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
  W/System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
  W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
  W/System.err: Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
  W/System.err:     at libcore.io.Linux.open(Native Method)
  W/System.err:     at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
  W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254)
  W/System.err:     at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
  W/System.err:     at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7542)
  W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:478)
  W/System.err:  … 15 more

AndroidManifest.xml파일에 퍼미션 권한도 추가되어 있으며, 코드상에서도 역시 사용자에게 궈한 부여 요청을 하고 있음에도 이것과 별개로 오류가 발생하게 됩니다. 물론 파일 프로바이더 정보도 선언 되어 있어요.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
	android:maxSdkVersion="28" />
    
    
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.test.app.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>    

 

오류가 발생한 코드 정보입니다.

// 다운로드 폴더 접근
File downloadFolder = new File(
	String.valueOf(
    	Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)));
        
ContextWrapper cw = new ContextWrapper(SettingActivity.this);
File sd = cw.getDir(BACKUP_PATH, Context.MODE_PRIVATE); 

//원본 데이터 경로
File data = Environment.getDataDirectory();        

File currentDB = new File(data, DATABASE_FILE_LOC); //원본 데이터 파일
File backupDB = new File(downloadFolder, BAK_FILE_NAME); // 다운로드 폴더에 다운받은 백업파일

FileChannel src = new FileInputStream(backupDB).getChannel();
FileChannel dst = new FileOutputStream(currentDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();        

 

외부저장소인 다운로드 폴더에 접근할 수 없음으로 오류가 발생하였는데, 아이러니한 부분이 있습니다.

아래 코드 스니펫을 보시면, 다운로드 폴더에 접근하여 해당 파일이 존재하는지 여부를 체크하는 로직입니다.

이 로직은 정상적으로 동작을 합니다. 

File downloadFolder = new File(String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)));  //DIRECTORY_DOWNLOADS

File backupedDB = new File(downloadFolder, BAK_FILE_NAME);
if(backupedDB.exists()){
   Log.d("TAG", "==== backUpExists: " + "다운로드 폴더에서 백업파일 찾음");
}

파일 존재여부 정도는 체크할 수 있도록 열려있습니다. 그러나 그 파읽을 읽어서 쓰려고 하는 순간 접근 권한 오류가 발생하게 됩니다.
이럴때 우리는 어떻게 처리하면 좋을까요? 

해결 방법은 사용자에게 파일을 선택하도록 파일 검색창을 열어줘야 합니다. ACTION_OPEN_DOCUMENT 또는 ACTION_GET_CONTENT 인텐트를 사용하여 해결할 수 있습니다. 사용자가 선택한 파일을 리턴 받은 후 내가 만든 앱의 내부 저장소로 파일을 복사해와야합니다.

 

 

 

 

자세한 정보는 다음 "저장소 액세스 프레임워크를 사용하여 파일 열기" 를 읽어보세요.

 

저장소 액세스 프레임워크를 사용하여 파일 열기  |  Android 개발자  |  Android Developers

Android 4.4(API 수준 19)에는 저장소 액세스 프레임워크(SAF)가 도입되었습니다. SAF는 사용자가 선호하는 문서 저장소 제공자 전체에서 문서, 이미지 및 각종 다른 파일을 탐색하고 여는 작업을 간편

developer.android.com

 

내부저장소로 파일을 복사하는 샘플코드는 아래 코드 스니펫을 참고하세요.

private void restoreDB(){
	Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
	intent.addCategory(Intent.CATEGORY_OPENABLE);
	//mimeTypes
	//intent.setType("text/csv");
	//intent.setType("text/plain");
	//intent.setType("application/pdf");
	intent.setType("application/*");
	//intent.setType("image/*");
	//intent.setType("application/x-excel");

	//String[] mimeTypes = {"image/*","application/pdf","application/msword","application/vnd.ms-powerpoint","application/vnd.ms-excel","text/plain",
	//    "application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .doc & .docx
	//            "application/vnd.ms-powerpoint","application/vnd.openxmlformats-officedocument.presentationml.presentation", // .ppt & .pptx
	//            "application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xls & .xlsx
	//            "application/zip"};
	startActivityForResult(intent, 1212);
}



@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
	super.onActivityResult(requestCode, resultCode, data);
	Log.i("TAG", "########### resultCode  :  " + resultCode + " requestCode  :  " + requestCode);

	if (resultCode == Activity.RESULT_OK && requestCode == 1212) {
		if (data != null) {
			Uri uri = data.getData();
			if(uri!=null)
				Log.e("TAG", "########### uri  :  "  + uri.toString());

			//파일에 쓰기
			//File file = new File(String.valueOf(uri));
			//File backupDB = new File(uri.getPath());

			//덮어쓰기할 대상 혹은 파일 쓰기할 위치
			ContextWrapper cw = new ContextWrapper(SettingActivity.this);
			File targetDir = cw.getDir(BACKUP_PATH, Context.MODE_PRIVATE); //앱의 내부 저장소
			File targetFile = new File(targetDir, BAK_FILE_NAME);
			InputStream inputStream = null;
			OutputStream outputStream = null;
			int DEFAULT_BUFFER_SIZE = 1024 * 4;  //파일 용량이 크다면 늘려줘요
			try {
				inputStream = getContentResolver().openInputStream(uri);
				outputStream = new FileOutputStream(targetFile);//dst


				byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
				int len;
				while ((len = inputStream.read(buffer)) > 0) {
					outputStream.write(buffer, 0, len);
				}
				outputStream.close();
				inputStream.close();

			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e){
				e.printStackTrace();
			} finally{
				if(inputStream!= null) {
					try {
						inputStream.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}

		}
	}
}

읽어들인 파일의 크기(바이트 수)를 알 수 없음으로 임으로 바이트 크기를 4096 사이즈로 임의 지정하였습니다. 파일의 사이즈가 가변적일 것임으로 생각됩니다. 읽어들인 파일이 4096 사이즈 보다 큰 경우 문제가 생길까요?

자바 9 버전을 사용하는 경우라면 readAllBytes()메소드를 사용하여 인풋스트림의 바이트어레이 사이즈를  확인할 수 있습니다.

byte[] buffer =  inputStream.readAllBytes(); //java 9

IOUtils 클래스에서 제공하는 toByteArray()메소드를 사용할 수 도 있으나 deprecated 되었으며, 이 메소드를 열어보면 4096 사이즈로 고정 되어 있습니다.

byte[] buffer = IOUtils.toByteArray(inputStream);

 

[IOUtils.class]

    /** @deprecated */
    @Deprecated
    @KeepForSdk
    @RecentlyNonNull
    public static byte[] toByteArray(@RecentlyNonNull InputStream var0) throws IOException {
        ByteArrayOutputStream var1 = new ByteArrayOutputStream();
        Preconditions.checkNotNull(var0);
        Preconditions.checkNotNull(var1);
        byte[] var2 = new byte[4096];

        while(true) {
            int var3 = var0.read(var2);
            if (var3 == -1) {
                return var1.toByteArray();
            }

            var1.write(var2, 0, var3);
        }
    }

 

MIME 타입에 따라 원하는 형식의 파일만 조회가 가능합니다.

https://developer.android.com/reference/android/content/Intent#EXTRA_MIME_TYPES

 

 

[REFERENCE]

https://developer.android.com/guide/topics/providers/document-provider

https://stackoverflow.com/questions/2975197/convert-file-uri-to-file-in-android

https://stackoverflow.com/questions/2975197/convert-file-uri-to-file-in-android/53425857

https://stackoverflow.com/questions/13209494/how-to-get-the-full-file-path-from-uri

https://stackoverflow.com/questions/58550549/how-to-use-intent-action-open-document-in-android-pie

https://developer.android.com/reference/androidx/core/content/FileProvider?hl=nb 

 

Leave a Reply

error: Content is protected !!