Android

안드로이드(android) 저장소 권한을 사용하지 않고 FilePorvider를 사용하여 다른 앱과 파일 공유

앱 내에서 파일 혹은 이미지등을 생성하여 다른 사람에게 공유하기 위해서는 사용자 휴대전화의 외부 저장소를 읽고 쓸 수 있는 저장소 권한을 사용자에게 요청해야 합니다. 그러나 사용자들은 이 권한을 상당히 꺼려합니다. 저장소 권한이 사용자의 사진 및 미디어에 액세스하도록 허용해야하기 때문입니다. 개발자가 권한을 요구하면서 나는 너의 사진 및 동영상 등의 개인정보에 접근하지 않는다고 공지를 해도 많은 사용자들은 의심하며 문의해오거나 앱 리뷰에 왜 필요하냐는 문의를 많이 합니다. 이러한 곤욕스러운 상황을 피하기 위한 방법이 있습니다. 만약 내가 만들려는 파일이 영구적 사용자의 저장소에 저장해둘 필요가 없다면 캐시 디렉토리를 사용하여 파일을 저장하고 공유하는 방법을 사용해보세요.

AndroidMenifest.xml 파일에 <uses-permission android:name=”android.permission.READ_EXTERNAL_STORAGE” />
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”
android:maxSdkVersion=”28″ /> 와 같은 접근 권한을 명시할 필요도 없으며, 사용자에게 요구하지 않아도 됩니다.

 

작업 순서

1. FileProvider 사용 설정(AndroidX)

2. 캐시 디렉토리를 사용하여 캐시 파일을 저장

3. 캐시 파일을 공유

4. java.lang.SecurityException 권한 오류 발생시 처리

 

 

FileProvider 사용 설정

FileProvider 클래스를 열어서 살펴보면 ContentProvider를 확장하여 구현되었음을 알 수 있습니다.  이 클래스는 file:// uri 대신 content://uri를 만들어 앱과 관련된 파일을 공유하는데 도움이 됩니다. 구글의 정책변경으로 파일 공유시  FilerProvider 사용하여야 하며 content://uri를 사용해야합니다. 그리고 ndroid.os.FileUriExposedException 오류가 발생한다면 동일하게 FilerProvider를 사용해야합니다.

public class FileProvider extends ContentProvider {
    private static final String[] COLUMNS = {
            OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };

    private static final String
            META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";

    private static final String TAG_ROOT_PATH = "root-path";
    private static final String TAG_FILES_PATH = "files-path";
    private static final String TAG_CACHE_PATH = "cache-path";
    private static final String TAG_EXTERNAL = "external-path";
    private static final String TAG_EXTERNAL_FILES = "external-files-path";
    private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
    private static final String TAG_EXTERNAL_MEDIA = "external-media-path";

    private static final String ATTR_NAME = "name";
    private static final String ATTR_PATH = "path";

    private static final File DEVICE_ROOT = new File("/");

    @GuardedBy("sCache")
    private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();

    private PathStrategy mStrategy;
    ..............이하 생략

콘텐츠URI를 사용하면 임이 액세스 권한을 사용하여 읽기 및 쓰기 액세스 권한을 부여할 수 있습니다. 그렇기 때문에 WRITE_EXTERNAL_STORAGE 권한을 요구하지 않아도 되며, 캐시 디렉토리의 파일에 대한 임시 접근 권한을 제공할 수 있습니다.  

 

작업1 : AndroidManifest파일에  FilerProvider 설정을 먼저 시작합니다. android:authorities 속성의 값은 어플리케이션ID 값을 타이핑해주고, 그 뒤에 .provider는 개발자가 임의로 명시할 수 있습니다. provider 해도 되고 , .fileprovider로 해도 됩니다. 백앤드단에 코딩할때 이곳에서 명시했던 이름을 그대로 가져와서 사용만 해주면 됩니다.  android:resource 속성값의 @xml/fileprovider는 개발자가 생성하게될 xml 파일 이름입니다.

<manifest> 
    ... 
    <application> 
        ... 
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="[applicationId(앱 패키지명).provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/fileprovider"/>
        </provider>
         ... 
    </ application> 
</ manifest>

 

작업2: res 폴더 아래에 xml 리소스 폴더를 생성 후 fileprovider.xml 파일을 생성 합니다.

FileProvider는 미리 지정한 디렉터리의 파일에 대한 콘텐츠 URI 만 생성 할 수 있습니다. 그럼으로 디렉토리를 지정하려면 저장영역과 경로를 XML로 설정해야합니다.

fileproider.xml 파일에 cache-path 저장영역 설정을 합니다. 

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="cache" path="." />
</paths>

 

캐시 디렉토리를 사용하여 캐시 파일을 저장

다음 예제는 비트맵 이미지를 캐시 디렉토리에 파일로 저장하는 방법을 구현합니다. bitmap클래스의 compress메소드를 사용하여 이미지 파일생성합니다.

// 캐시 디렉토리 지정
File storage = getCacheDir();

//저장할 파일 이름
String timeStamp = new SimpleDateFormat("HHmmss").format(new Date());
String fileName = name + "_" + timeStamp + ".jpg";

//캐시 디렉토리에 파일 생성
File tempFile = new File(storage, fileName);

try {

	//빈 파일 생성
	tempFile.createNewFile();

	//파일 쓰기 위한 스트림 init
	FileOutputStream out = new FileOutputStream(tempFile);

	// compress메소드 사용, 스트림에 비트맵을 저장.
	bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);

	// 스트림 사용후 닫기
	out.close();

} catch (IOException e) {
	e.printStackTrace(); 
}

 

캐시 파일 공유 (안드로이드 파일 공유 소스)

파일프로바이더를의 getUriForFile()메소드를 사용하여 캐시폴더의 파일을 가져옵니다. 안드로이드 앱간 파일을 공유하려면 file://대신 content://를 사용해야합니다. 이미지파일을 공유할 것임으로 setType에 “image/jpg”를 지정하였습니다. png파일이라면 image/png를 지정합니다. 확장자를 지정하지않고 이미지 전체를 사용할 경우 “image/*를 지정합니다.

Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.test.sample.provider", tempFile);

Intent intent = ShareCompat.IntentBuilder.from(MainActivity.this)
		.setType("image/jpg")
		.setStream(uri)
		.createChooserIntent()
		.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

공유용 인텐트를 생성할 때 공유앱으로 사용될 앱들이 URI를 읽을 수 있도록 Intent.FLAG_GRANT_READ_URI_PERMISSION플래그를 인텐트에 추가합니다. 

 

 

 

java.lang.SecurityException 권한 오류 발생시 처리 방법

requires the provider be exported, or grantUriPermission() 오류가 발생할 수 있습니다.

Writing exception to parcel
    java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.test.sample.provider/image/generate_115927.jpg from pid=17400, uid=1000 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:731)
        at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:603)
        at android.content.ContentProvider$Transport.enforceFilePermission(ContentProvider.java:594)
        at android.content.ContentProvider$Transport.openTypedAssetFile(ContentProvider.java:508)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:307)
        at android.os.Binder.execTransactInternal(Binder.java:1046)
        at android.os.Binder.execTransact(Binder.java:1019)

[해결방법]

공유 어플로 사용할 어플리케이션을 찾아서 캐시파일에 대한 권한을 미리 부여한 후에 인텐트를 실행합니다. 

List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
	String packageName = resolveInfo.activityInfo.packageName;
	getApplicationContext().grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

 

 

[REFERENCE]

medium.com/androidsrc/share-cache-files-with-other-android-apps-using-fileprovider-897fe5705e45

developer.android.com/reference/androidx/core/content/FileProvider

developer.android.com/training/secure-file-sharing/share-file?hl=ko

developer.android.com/training/secure-file-sharing/setup-sharing?hl=ko

stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed/38858040#38858040

 

Leave a Reply

error: Content is protected !!