본문 바로가기

Android/Bottom-Up

Nougat 7+ 대응, FileProvider

게시물에 첨부된 파일을 다운로드하고 실행할 때, FileUriExposeException이 발생하면서 앱이 죽는다.




Android 7.0 Behavior Changes | Android Developers


안드로이드 7.0 변경된 사항들을 보니, 앱과 앱간의 파일 공유에 새로운 정책이 반영되었다.

앱 외부에서 file://URI를 참조하면 FileUriExposeException이 발생한다.

앱 사이에 파일을 공유하려면 반드시 content://URI를 사용해야하고, 이 URI에 대한 임시 접근권한을 줘야한다.

FileProvider가 그 권한을 가장 쉽게 부여하는 방법이다.






FileProvider | Android Developers


FileProvider란?


앱과 앱 사이의 안전한 파일 공유를 가능하게하는, ContentProvider의 특별한 하위클래스다!

file://URI 대신 content://URI를 사용한다.


content URI는 임시 접근권한을 이용하여 read && write 접근을 허용한다.

content URI를 포함한 인텐트를 생성할때, client 앱에 content URI를 전송하기 위해, 권한을 추가하는 Intent.setFlags()를 호출할 수 있다.

이러한 권한은, 액티비티가 위치한 스택이 활성화되어있는 동안, client 앱에서 사용가능하다.


content URI가 제공하는 보안 수준이 높아지면서 FileProvider는 안드로이드 보안 인프라의 핵심 요소로 자리잡았다.




1. Defining a FileProvider

FileProvider는 기본으로 파일에 대한 content URI 생성 기능이 포함되어 있다.
XML에다가 FileProvider에 대한 전체적인 명시를 해야 FileProvider를 앱에 포함시킬 수 있다.
매니페스트 파일에 <provider> element를 추가하고 그 하위에는 4가지 속성을 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>
cs


2. Specifying Available Files

FileProvider는 미리 지정한 디렉토리에 위치할 파일에 대한 content URI만을 생성할 수 있다.
디렉토리를 지정하기위해, <paths> element를 이용해, XML에다가 path와 저장 영역을 명시해야한다. 

name element
- URI path segment
- 보안을 강화하기위해 공유하는 하위 디렉토리 이름을 감춘다.
- 하위 디렉토리 이름은 path element에 포함된다.

path element
- 파일을 공유하는 실제 하위 디렉토리

1
2
3
4
5
6
7
<!-- xml/file_paths -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <files-path name="name" path="path"/>           : 내부저장소 / Context.getFilesDir()
  <cache-path name="name" path="path" />          : 내부저장소 / getCacheDir()
  <external-path name="name" path="path" />       : 외부저장소 / Environment.getexternalStorageDirectory()
  <external-files-path name="name" path="path" /> : 외부저장소 / Context.getExternalFilesDir()
  <external-cache-path name="name" path="path" /> : 외부캐시영역 / Context.getExternalCacheDir()
</paths>
cs


FileProvider path를 별도의 XML 파일로 명시했으면, 매니페스트의 <provider> element 하위에 <meta-data> element의 속성으로 리소스를 지정해준다.

1
2
3
4
5
6
7
8
9
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mydomain.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
cs


3. Retrieving the Content URI for a File

content URI를 이용해서 다른 앱에게 파일을 공유하려면, content URI를 생성해야한다.

content URI를 생성하려면, 기존 파일에 대한 새로운 File을 생성하고, 그 File을 getUriForFile()에 넘긴다.

getUriForFile()이 return한 content URI를 인텐트에 싣어서 다른 앱에서 보낼 수 있다.

content URI를 받은 client 앱은, ParcelFileDescriptor를 얻기위해 ContentResolver.openFileDescripter를 호출함으로써, 그 파일을 열어볼 수 있고 접근할 수 있다.


4. Granting Temporary Permissions to a URI

getUriForFile()이 return한 content URI에 대한 접근권한을 승인하려면, 아래를 수행해야 한다.

    • Context.grantUriPermission() 호출하기

: mode_flags 파라미터의 값에 따라, 지정한 패키지에 대해 content URI를 위한 임시 접근을 승인한다.
mode_flags 파라미터로 가능한 값은 FLAG_GRANT_READ_URI_PERMISSION과 FLAG_GRANT_WRITE_URI_PERMISSION 또는 둘 다이다.
권한은, 기기가 리부팅되거나 revokeUriPermission()을 호출하여 승인내역을 취소할 때까지, 유지된다.
    • setData()를 호출해서 인텐트에 content URI를 싣는다.
    • 다음으로, Intent.setFlags()를 호출해서 mode_flags 파라미터 값을 설정한다.
    • 마지막으로, (setResult()를 이용해서) 다른앱에 인텐트를 보낸다. 수신한 액티비티를 포함한 스택이 활성화되어 있는 동안 권한들이 승인된다. 그 스택이 finish되면 그 권한들도 자동으로 제거된다. client 앱에서 한 액티비티에 승인된 권한들은 자동으로 그 앱의 다른 컴포넌트들에도 확장된다. 

5. Serving a Content URI to Another App

client 앱에 파일에 대한 content URI를 전달하는 다양한 방법이 존재한다. 한가지 일반적인 방법은 client 앱이 startActivityResult()를 호출하여 나의 앱을 실행하는것이다. 그러면 내 앱은 즉시 client 앱이나 유저가 선택한 파일을 허용하는 현재 UI에 content URI를 return할 수 있다.

유저가 선택한 파일을 허용하는 현재 UI에 content URI를 전달하는 경우, 유저가 파일을 선택하면 내 앱은 그 파일의 content URI를 return 할 수 있다. 위 두가지 경우에서 내 앱은 setResult()를 통해 전송된 인텐트의 content URI를 return 한다.


또한 ClipData 객체에 content URI를 싣을 수 있고, 그 객체를 인텐트에 추가하여 client 앱에 보낼 수 있다. 이렇게 하려면 Intent.setClipData()를 호출한다. 이러한 접근을 할 때 인텐트에 여러 ClipData 객체를 싣을 수 있다. Intent.setFlags()를 호출할 때 동일한 권한들이 모든 content URI에 적용된다.