본문 바로가기

Android

[Android] OPEN_DOCUMENT로 가져온 Uri를 Multipart.Part로 만들기(Retrofit2 파일 업로드)

SDK 29부터 적용딘 Scoped Stroage에 대응하여 제작

1. Retrofit2를 사용한 파일 업로드

Retrofit에서 파일 업로드를 구현하는 경우 Multipart를 사용합니다.

기존 인터넷에 있는 소스의 경우 File Picker에서 가져온 Uri를 가지고 파일의 경로를 직접 알아내어 구현하는 방식을 사용합니다.

    ...
    
    File file = FileUtils.getFile(this, fileUri);

    // create RequestBody instance from file
    RequestBody requestFile =
            RequestBody.create(
                         MediaType.parse(getContentResolver().getType(fileUri)),
                         file
             );

    // MultipartBody.Part is used to send also the actual file name
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("picture", file.getName(), requestFile);
            
    ...

하지만, Scoped Storage가 적용되면서 위 코드가 막힐 것으로 예상됩니다.

developer.android.com/preview/privacy/storage?hl=ko

 

Android 11의 저장소 업데이트  |  Android 개발자  |  Android Developers

Android 11에서는 플랫폼을 더욱 강화하여 외부 저장소의 앱 및 사용자 데이터를 보다 안전하게 보호합니다. 미리보기 출시에서는 지난해 Android Dev Summit에서 발표한 미디어의 원시 파일 경로 액세

developer.android.com

2. Cursor에 '_data' column이 없는 경우

IllegalArgumentException: column '_data' does not exist

보통 uri를 가지고 파일의 경로를 가져올 때 contentResolver에서 통해 가져온 Cursor에서 _data column의 값을 가져오는게 보통입니다.

하지만, 몇 몇 기기의 File Picker에서 주는 Uri를 가지고 얻은 Cursor에는 _data column이 없는 경우가 있습니다.

MediaStore를 통해 생성된 Uri에서는 _data column이 존재하지만 다른 방법으로 생성된 Uri의 경우 없을 가능성이 높습니다.

SDK 29부터 MediaStore.MediaColumns의 DATA 상수가 deprecated가 된 것이 이유가 될 수 있을 듯 합니다.

developer.android.com/reference/android/provider/MediaStore.MediaColumns#DATA

 

MediaStore.MediaColumns  |  Android 개발자  |  Android Developers

 

developer.android.com

3. ContentResolver의 openInputStream 활용

Scoped Storage 업데이트에 대비하여 만들어 본 kotlin Extension입니다.

// kotlin
fun Uri.asMultipart(name: String, contentResolver: ContentResolver): MultipartBody.Part? {
    return contentResolver.query(this, null, null, null, null)?.let {
        if (it.moveToNext()) {
            val displayName = it.it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            val requestBody = object : RequestBody() {
                override fun contentType(): MediaType? {
                    return contentResolver.getType(this@asMultipart)?.toMediaType()
                }

                override fun writeTo(sink: BufferedSink) {
                    sink.writeAll(contentResolver.openInputStream(this@asMultipart)?.source()!!)
                }
            }
            it.close()
            MultipartBody.Part.createFormData(name, displayName, requestBody)
        } else {
            it.close()
            null
        }
    }
}

Extension이 따로 없는 자바를 위한 코드입니다.

// Java
public static MultipartBody.Part uriToMultipart(final Uri uri, String name, final ContentResolver contentResolver) {
    final Cursor c = contentResolver.query(uri, null, null, null, null);
    if (c != null) { 
        if(c.moveToNext()) {
            final String displayName = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            RequestBody requestBody = new RequestBody() {
                @Override
                public MediaType contentType() {
                    return MediaType.parse(contentResolver.getType(uri));
                }

                @Override
                public void writeTo(BufferedSink sink) {
                    try {
                        sink.writeAll(Okio.source(contentResolver.openInputStream(uri)));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            c.close();
            return MultipartBody.Part.createFormData(name, displayName, requestBody);  
        } else {
            c.close();
            return null;
        }
    } else {
        return null;
    }
}

가볍게 만들어본 코드라 오류가 발생할 수 있습니다. 오류 발생시 댓글로 남겨주시면 감사하겠습니다.