안드로이드 앱을 만들다보면 외부의 다운로드 링크나 인터넷 주소가 아니라
기기 내부의 파일을 uri를 사용하여 이용하는 경우가 있습니다.
Content Provider를 이용해서 파일에 접근하는 경우에 uri로 해당 파일을 얻게 되는데
보통 아래와 같이 uri를 얻게 됩니다.
content://authority/path/id
문제는, Content Provider에서 정보를 읽어온뒤 사용해버리거나 바로 편집하는 경우에는 상관없으나
이때 얻어진 uri를 가지고 Content Provider를 거치치 않고 직접 접근 하기위한 경로를 얻기위해서
Uri.getpath()를 시도하면 /docume/FILE:1234 와 같은 경로가 반환되게 됩니다.
사진파일이나 음악파일등, 프로바이더로 접근한 뒤에 나중에 실제경로로 파일을 사용해야 하는 경우가 있는데
이때 상기와 같은 경로로는 파일을 사용할 수가 없습니다.
이 때 실제경로를 얻을 때 필요한 코드가 하단과 같은 코드입니다.
public static String getRealPathFromURI(final Context context, final Uri uri) {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/"
+ split[1];
} else {
String SDcardpath = getRemovableSDCardPath(context).split("/Android")[0];
return SDcardpath +"/"+ split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] { split[1] };
return getDataColumn(context, contentUri, selection,
selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getRemovableSDCardPath(Context context) {
File[] storages = ContextCompat.getExternalFilesDirs(context, null);
if (storages.length > 1 && storages[0] != null && storages[1] != null)
return storages[1].toString();
else
return "";
}
public static String getDataColumn(Context context, Uri uri,
String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection,
selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri
.getAuthority());
}
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri
.getAuthority());
}
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri
.getAuthority());
}
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri
.getAuthority());
}
본래는, [< API 11], [API11 <, < API 19], [<API 19] 로 세가지 API 분기점이 나뉘어져 있는 코드가 잘 돌아다니는데
현재시점에서 킷캣<API 19>은 잘 사용하지 않기도 하고, 프로바이더를 사용한 것이 킷캣 이후이기 때문에
필요없는 부분은 삭제하였습니다.
기존의 오래된 소스에서 문제가 되는부분은 외장 SD 카드에 있는 파일에 접근할때인데 본래의 코드는 아래와 같습니다.
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/"
+ split[1];
}
}
외부 SD 카드에 프로바이더로 접근하는 유저들에게서 자꾸 RuntimeException 이 발생하며
java.lang.IllegalStateException: getRealPath(context, uri) must not be null
과 같은 Crash Report 가 들어오는 것을 보고 코드를 살펴보니,
킷캣이 나올때즈음 만들어진 오래된 코드라
외장 SD 카드 경로 부분이 제대로 되어 있지 않는것을 뒤늦게 확인하였습니다.
아마도 SDK 21버전까지는 제대로 작동하지만, 26버전 이상부터는 반드시 null 값을 반환하게 될것으로 생각됩니다.
원인은 안드로이드에 SD카드가 마운트 되었을때 가지게 되는 절대경로가 다르기 때문인데요.
안드로이드 기기에 외장 SD 카드가 마운트 되어 있을때, 버전마다 해당 SD 카드의 절대경로가 다르게 나타납니다.
Android SDK: 19 (4.4.4) : /storage/emulated/0
Android SDK: 21 (5.0.2) : /storage/sdcard
Android SDK: 23 (6.0) : /storage/emulated/0
심지어 SDK 26 버전 이상부터는 /storage/xxxx-xxxx/로, sd카드 마다 마운트 되는 xxxx-xxxx 값이 달라지게 됩니다.
따라서 Environment.getExternalStorageDirectory() 로 가져오는 기존의 코드를 사용하게 되면
외장 SD 카드에 있는 파일을 선택할때 null 값을 반환하며 앱이 종료되게 되기 때문에
sd카드의 실제경로인 /storage/xxxx-xxxx/를 찾아주는 아래와 같은 부분을 추가로 삽입 해 주었습니다.
public static String getRemovableSDCardPath(Context context) {
File[] storages = ContextCompat.getExternalFilesDirs(context, null);
if (storages.length > 1 && storages[0] != null && storages[1] != null)
return storages[1].toString();
else
return "";
}
상기 코드는 간단한 앱을 만들때 자꾸 외장 SD 카드 경로를 못읽어들여 만들어둔 코드인데 다시 쓰일줄은 몰랐습니다.
기기에 마운트되어 있는 storage들을 읽어 내부의 스토리지인 storages[0] 이 아닌 storages[1]이 존재할때
해당 경로를 반환해주는 코드입니다.
sd 카드가 없다면 storages[1]은 존재하지 않기 때문에 " "값을 반환하게 해놓았습니다.
따라서 먼저 기존의 방법대로 Environment.getExternalStorageDirectory() 로 sd카드의 경로를 가져오게 되지만,
실패할 경우에는 상기 코드의 분기점으로 들어가서 sd카드의 실제경로인 /storage/xxxx-xxxx/를 반환하게 됩니다.
또한 해당코드의 경우에는 /storage/xxxx-xxxx/Android/app/name 등의 경로를 반환하게 되기 때문에
필요한 코드 이후로는 잘라주었습니다.
즉, URI는 모르겠고 난 외장 SD 카드의 경로만 필요하다 라면 다음과 같이 사용하면 됩니다.
String SDcardpath = getRemovableSDCardPath(context).split("/Android")[0];
'Android > Dev' 카테고리의 다른 글
리사이클러뷰 역순으로 출력하기 (0) | 2020.01.29 |
---|---|
화면크기에 따라 일정한 비율로 크기가 자동조절되는 TextView (3) | 2019.07.13 |
SwipeRefreshLayout 을 사용할 때 가로 스크롤 과 충돌하지 않게하기 (0) | 2019.03.17 |
오늘이 양력/음력 공휴일인지 확인하는 코드 (0) | 2019.03.17 |
ScrollView 사용시 스크롤 중인지 판단하여 작업 제한하기 (0) | 2019.03.17 |
댓글