정신건강에 좋은 파일 공유하기



안드로이드 개발을 하다보면, 자신의 앱에서 생산한 파일을 다른 앱으로 공유해야 하는 경우가 종종 발생합니다.

보통 촬영한 사진, 동영상 등이 되겠지요.

그러나 안드로이드의 개떡같은 '하위호환 따위 개나 줘버려' 정책에 의해서 이 단순한 기능을 버그없이 개발하는것은 상당히 어렵습니다.




FileProvider 를 사용하자! 는 훼이크다!




안드로이드 Build SDK 버전 24 이상부터(N버전) 파일 공유시에 FileProvider 를 통해서 content:// 형태의 uri 를 만들어 공유하도록 강제되었습니다.

이전버전에서야 file:///storage/emulator/0/... 과 같은 uri 를 사용해서 공유해도 잘 되었습니다. 하지만 N 이상부터는 Error 가 발생하면서 App 이 Crash 됩니다. (하..ㅅㅂ것들..)


아무튼 그래서 FileProvider 라는 놈을 써서, file:// 대신 content:// 형태의 uri 를 손쉽게 뽑아낼 수 있습니다. 이것으로 공유를 하게되면 더 이상 에러가 발생하지 않습니다.


사용법도 간단합니다.



라고 말하지만, 그러면 우리의 하위호환성 개나줘버려 구글이 아니겠지요.

이렇게 FileProvider 를 통해서 content:// 를 뽑아내면, 일부 - 라고 부르지만 대부분 중국 앱들 - 에서는 정상적으로 파일 공유가 안됩니다.

FileProvider 를 안쓰면,, 앱이 뒤지고, 쓰면,, 앱에 정상적으로 파일 공유가 안되는 개떡같은 상황.

안드로이드 개발자라면 놀라지도 않을 상황.


간략히 설명하자면, FileProvider 를 미디어 파일을 content:// 형태로 뽑아줍니다. 앱에서 설정한 형식에 맞추어서 말이죠.

예를들면,,

content://my-application/images/a.mp4 이런 형태가 됩니다.


그러나~

앱에서 정상적으로 공유가 안됩니다.

App 의 Internal Storage 에 있는 파일들은 공유가 될지 모르겠지만, external storage 에 있는 파일들은 공유가 안됩니다.

그럼 어떻게 해야 하느냐??


OS 에서 기본적으로 동작되고 있는 MediaStore ( MediaScanner 친구)가 가지고 있는 uri 를 사용하면, 모든 앱에서 정상적으로 공유가 됩니다.


각설하고, 이를 구현한 코드는 아래와 같습니다.



package kr.co.crystalcube;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.StrictMode;
import android.provider.MediaStore;
import android.support.annotation.StringDef;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

import static android.content.ContentValues.TAG;

public class MediaUriManager {
@StringDef({ShareContentType.TEXT, ShareContentType.IMAGE, ShareContentType.AUDIO, ShareContentType.VIDEO, ShareContentType.File})
@Retention(RetentionPolicy.SOURCE)
@interface ShareContentType {
String TEXT = "text/plain";
String IMAGE = "image/*";
String AUDIO = "audio/*";
String VIDEO = "video/*";
String File = "*/*";
}


private static @ShareContentType String convertToContentType(String mediaType) {
String contentType = mediaType.toLowerCase().split("/")[0];
switch (contentType) {
case "text": return ShareContentType.TEXT;
case "image": return ShareContentType.IMAGE;
case "audio": return ShareContentType.AUDIO;
case "video": return ShareContentType.VIDEO;
case "*": return ShareContentType.File;
default: return ShareContentType.TEXT;
}
}


public static Uri getFileUri (Context context, String mediaType, File file){
if (context == null) {
Log.e(TAG,"getFileUri current activity is null.");
return null;
}

if (file == null || !file.exists()) {
Log.e(TAG,"getFileUri file is null or not exists.");
return null;
}

Uri uri = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
uri = Uri.fromFile(file);
} else {
@ShareContentType String shareContentType = convertToContentType(mediaType);
if (TextUtils.isEmpty(shareContentType)) {
shareContentType = "*/*";
}

switch (shareContentType) {
case ShareContentType.IMAGE :
uri = getImageContentUri(context, file);
break;
case ShareContentType.VIDEO :
uri = getVideoContentUri(context, file);
break;
case ShareContentType.AUDIO :
uri = getAudioContentUri(context, file);
break;
case ShareContentType.File :
uri = getFileContentUri(context, file);
break;
default: break;
}
}

if (uri == null) {
uri = forceGetFileUri(file);
}

return uri;
}


private static Uri getFileContentUri(Context context, File file) {
String volumeName = "external";
String filePath = file.getAbsolutePath();
String[] projection = new String[]{MediaStore.Files.FileColumns._ID};
Uri uri = null;

Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri(volumeName), projection,
MediaStore.Images.Media.DATA + "=? ", new String[] { filePath }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID));
uri = MediaStore.Files.getContentUri(volumeName, id);
}
cursor.close();
}

return uri;
}

private static Uri getImageContentUri(Context context, File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Images.Media._ID }, MediaStore.Images.Media.DATA + "=? ",
new String[] { filePath }, null);
Uri uri = null;

if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}

cursor.close();
}

if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}

return uri;
}

private static Uri getVideoContentUri(Context context, File videoFile) {
Uri uri = null;
String filePath = videoFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Video.Media._ID }, MediaStore.Video.Media.DATA + "=? ",
new String[] { filePath }, null);

if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/video/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}

cursor.close();
}

if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
}

return uri;
}


private static Uri getAudioContentUri(Context context, File audioFile) {
Uri uri = null;
String filePath = audioFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Audio.Media._ID }, MediaStore.Audio.Media.DATA + "=? ",
new String[] { filePath }, null);

if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/audio/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}

cursor.close();
}
if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
}

return uri;
}



private static Uri forceGetFileUri(File shareFile) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
@SuppressLint("PrivateApi")
Method rMethod = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
rMethod.invoke(null);
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
}

return Uri.parse("file://" + shareFile.getAbsolutePath());
}

}



WeChat, QQ, Weibo 등에 공유할땐 위 코드를 사용하시는게 정신건강에 이롭습니다.


끗.