- 예상 구현
- 준비 사항
- Manifest.xml 추가
- Provider 권한 관련 경로 XML 파일 추가
- 첫 번째 시도
- 두 번째 시도
- 결론
예상 구현
모바일 기기 저장소에 .pdf 파일을 저장한 후 앱에서 파일의 이름을 추적하여 해당 .pdf 파일을 불러올 수 있다. 해당 기능 구현을 테스트하기 위해서 Test Application이라는 새로운 프로젝트를 생성하였다.
준비 사항
오래 전 학생 때 저장소 URI를 통해서 파일에 직접 접근이 가능하던 것이 떠올랐으나, Android SDK 24 버전 이상부터는 File Provider를 사용해서 파일에 접근해야 접근이 가능하도록 설계되었다고 한다. 이 둘의 차이점은 URI 앞부분에서 쉽게 확인할 수 있었다. 변경된 이유 중 한 가지를 예상했을 때 보안에 중점을 둔 조치라고 생각된다.
FileProvider Manifest.xml에 추가
먼저 Storage 관련 권한을 획득하기 위해서 권한을 설정한다. 읽기 권한만 필요하다면 READ_EXTERNAL_STORAGE만 추가해도 좋다. 그 다음 FileProvider로 접근하기 위해 Manifest.xml 파일에 <provider>를 정의하였다. File Provider를 사용하기 위해서 반드시 선언해야 한다. File Provider에 역할은 Contents URI 생성 사용에 대한 권한을 설정하는 일이다. 또한, 앱이 공유할 수 있는 directory를 지정하는 xml을 선언한다.
<manifest ...>
...
// Storage 권한 설정
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application ...>
...
// File Provider 설정
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${your application id here}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
...
</application>
</manifest>
android:authorities 속성은 File Provider를 통해서 생성한 Contents URI에 사용할 URI 권한을 지정한다. 매우 중요한 한 문장이며, 이 글을 쓴 이유이다. Contents URI는 Provider에서 데이터를 식별하는 URI이다. Contents URI에는 전체 제공자의 상징적인 이름(Provider의 Authority)과 테이블을 가리키는 이름(경로)이 포함되어 있다. 그 형식은 예를 들면 아래와 같다.
content://com.example.myapp.fileprovider/Download/test.pdf
Provider 권한 관련 경로 XML 파일 추가
FileProvider는 사용자가 미리 지정한 directory의 파일에 대한 Contents URI를 생성할 수 있다. directory를 지정하려면 <paths> 요소의 하위 요소를 사용하여 XML에 저장 영역과 경로를 지정할 수 있다. 예를 들어, Download 폴더나 images 폴더 하위에 있는 directory에 대한 Contents URI를 요청할 수 있다. 혹은 “.”으로 정의하여 전체 경로를 지정할 수도 있다.
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="files" path="Download/"/> // context.getExternalFilesDir()
<external-path name="external_files" path="."/> // Environment.getExternalStorageDirectory()
<files-path name="files" path="images/"/> // context.filesDir()
</paths>
첫 번째 시도
Android USB 파일 전송을 통해서 test.pdf 파일을 Download 폴더에 복사했다. 복사한 파일에 접근하기 위해서 FileProvider와 관련된 코드를 위처럼 구성한 후 Test Application MainActivity에서 간단한 버튼을 통해서 접근하려고 했다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
pdfButtonClick()
}
private fun pdfButtonClick() {
val button1 = findViewById<Button>(R.id.button1)
val file = File(File(Environment.getExternalStorageDirectory(), "Download"), "test.pdf")
val uri = FileProvider.getUriForFile(this, applicationContext.packageName + ".provider", file)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/pdf")
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
button1.setOnClickListener {
startActivity(intent)
}
}
실행 결과 PDF 파일을 불러올 수 있는 앱을 열었지만 미리보기가 없거나 바로 닫히면서 test.pdf 파일을 불러올 수 없었다. 다만, 위 코드에서 Intent(Intent.ACTION_VIEW)를 Intent(Intent.ACTION_GET_CONTENT)로 수정한 후 실행하면 중간에 System App이 실행되면서 test.pdf 파일에 접근은 가능하다. 다만, 원래 의도했던 것은 버튼을 클릭하면 PDF Loader 앱에 Intent를 보내서 바로 test.pdf 파일을 불러오는 것이었는데, ACTION_GET_CONTENT는 중간에 System App이 하나 더 추가되어 불편하다.
두 번째 시도
결론부터 말하자면, 첫 번째 시도의 문제점은 test.pdf를 밖에서 저장한 파일이라는 점이다. test.pdf 파일을 Test Application에서 다운 받고 FileProvider로 접근하면 같은 앱에서 다운로드와 접근을 하기 때문에 내부적으로 해당 URI에 동일한 Authority가 설정되어 직접 접근이 가능하게 된다.
private fun pdfButtonClick() {
val button1 = findViewById<Button>(R.id.button1)
val file = File(File(Environment.getExternalStorageDirectory(), "Download"), "test.pdf")
val uri = FileProvider.getUriForFile(this, applicationContext.packageName + ".provider", file)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/pdf")
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
button1.setOnClickListener {
startActivity(intent)
}
val button2 = findViewById<Button>(R.id.button2)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://something url for download test.pdf"))
button2.setOnClickListener {
startActivity(browserIntent)
}
}
테스트를 위해서 코드를 간단하게 작성했다. browserIntent는 서버에서 test.pdf 파일을 다운로드할 수 있는 경로로 Intent를 보냈다. 이렇게 하면 Chrome이나 웹브라우저에서 test.pdf 파일을 다운로드할 수 있다. button2를 통해서 test.pdf 파일을 다운받은 후 button1로 test.pdf 파일에 접근해 봤다. 접근한 결과 PDF Loader 앱을 통해서 test.pdf 파일에 직접 접근이 가능했고, 원하는 기능을 구현할 수 있었다.
결론
코드를 찾아볼 때, 스택오버플로우나 블로그에서 같은 코드로 동일한 이슈를 겪는 사람이 많았다. 같은 코드를 작성하였는데, 어떤 사람은 잘 동작한다고 이야기하는 반면, 어떤 사람은 동작하지 않는다고 이슈에 올렸는데, 아마 위와 같은 상황이지 않을까 생각된다. 개인적으로 이번 일을 계기로 안드로이드 기본에 대해서 더 알아갈 수 있었고, 또 공부가 필요하다는 것을 느낀 좋은 경험이었다. 이건 예상이지만, 아마 리눅스에 대해서 더 잘 알았다면, 좀 더 쉽게 떠올릴 수 있었을 것 같다는 생각이 든다. 그 이유는 앱이 하나의 사용자로써 권한을 부여한다고 생각했기 때문이다.
참고자료 :
https://developer.android.com/training/secure-file-sharing/setup-sharing?hl=ko
https://developer.android.com/guide/topics/providers/content-provider-basics?hl=ko#ContentURIs
https://eunplay.tistory.com/81
'Mobile > Issues' 카테고리의 다른 글
[Android] Android Studio에서 logcat이 동작하지 않는 이슈 해결. (2) | 2023.05.15 |
---|---|
[Android] 안드로이드 스튜디오 리소스 에러 해결 (0) | 2023.04.08 |
[Android/Kotlin] 원격 호스트에 의해 강제로 끊겼습니다. (0) | 2022.05.24 |
[Android] app bundle명 변경 후 Binding Error (InflateException) (0) | 2021.11.22 |
[Android] SHA-1 확인하는 방법 (zsh: permission denied) (0) | 2021.11.19 |