2018년 1월 6일 토요일

FileUriExposedException


갑자기 잘되던 어플에서 android.os.FileUriExposedException 와 같은 오류가 발생하였습니다. (구글 마켓에 등록된 어플에서 구글 개발자 콘솔로 확인하면 마켓에 등록된 어플에서 어떤 오류가 발생했는지 확인이 가능합니다.)

오류 내용
android.os.FileUriExposedException: file:///storage/emulated/0/Download/xxx_user_release-keys.csv exposed beyond app through ClipData.Item.getUri()
 at android.os.StrictMode.onFileUriExposed(StrictMode.java:1958)
 at android.net.Uri.checkFileUriExposed(Uri.java:2348)
 at android.content.ClipData.prepareToLeaveProcess(ClipData.java:944)
 at android.content.Intent.prepareToLeaveProcess(Intent.java:10480)
 at android.content.Intent.prepareToLeaveProcess(Intent.java:10486)
 at android.content.Intent.prepareToLeaveProcess(Intent.java:10465)
 at android.app.Instrumentation.execStartActivity(Instrumentation.java:1616)
 at android.app.Activity.startActivityForResult(Activity.java:4564)
 at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:48)
 at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:75)
 at android.app.Activity.startActivityForResult(Activity.java:4522)
 at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:856)
 at android.app.Activity.startActivity(Activity.java:4883)
 at android.app.Activity.startActivity(Activity.java:4851)

해당 오류는 7.0 API 24부터 발생하는 오류입니다.

https://developer.android.com/about/versions/nougat/android-7.0-changes.html?hl=ko

앱 사이의 파일 공유


Android 7.0을 대상으로 하는 앱의 경우, Android 프레임워크는 앱 외부에서 file:// URI의 노출을 금지하는 StrictMode API 정책을 적용합니다. 파일 URI를 포함하는 인텐트가 앱을 떠나면 FileUriExposedException 예외와 함께 앱에 오류가 발생합니다.
애플리케이션 간에 파일을 공유하려면 content:// URI를 보내고 이 URI에 대해 임시 액세스 권한을 부여해야 합니다. 이 권한을 가장 쉽게 부여하는 방법은 FileProvider 클래스를 사용하는 방법입니다. 권한과 파일 공유에 대한 자세한 내용은 파일 공유를 참조하세요.

문제의 소스에서는 다른 어플로 intent를 전달하여 startActivity시키는 함수입니다. 이 부분이 파일 URI가 있어서 오류가 발생하게 됩니다.
문제의 소스
    private void fabUploadMenu() {
        if( getSelectedItemCount()!=1 ) {
            Toast.makeText(getApplicationContext(), "Please select only one item", Toast.LENGTH_SHORT).show();
            return;
        }
        Intent it = new Intent(Intent.ACTION_SEND);
        String fname = getSelectedItem();
        it.putExtra(Intent.EXTRA_SUBJECT, fname);
        it.putExtra(Intent.EXTRA_TEXT, fname);
        String path = LocalFileAccess.getDefaultPath();
        path += "/"+fname;
        File file = new File(path);
        if (!file.exists() || !file.canRead()) {
            Toast.makeText(this, "Attachment Error", Toast.LENGTH_SHORT).show();
            return;
        }
        Uri uri = Uri.parse("file://"+file);
        it.putExtra(Intent.EXTRA_STREAM, uri);
        it.setType("text/plain");
        startActivity(Intent.createChooser(it, "Choose a application for sending"));
    }


해결하기 위해서는 3단계로 처리합니다.

1단계

AndroidManifest.xml에 provider를 넣습니다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
    </application>

</manifest>

2단계

res/xml/provider_paths.xml 아래 내용으로 파일을 추가합니다.
provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>


3단계

FileProvider.getUriForFile api를 이용하여 문제의 코드를 변경합니다.
문제의 소스
Uri uri = Uri.parse("file://"+file);

=>

Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);


원본 출처 : 아래 코드는 screenshot 공유에 대한 부분 소스를 참고하 였습니다.
https://inthecheesefactory.com/blog/how-to-share-access-to-file-with-fileprovider-on-android-nougat/en







댓글 없음:

댓글 쓰기