목표
외부 앱에서 이미지 공유 버튼을 눌렀을 때, 내 프로젝트 앱으로 이미지를 가져와 바로 업로드하는 기능을 만들고 싶었다.
외부 앱(갤러리, 인터넷 등) -> 이미지 공유 -> PICLY -> 업로드 (Firbase Storage)
그래서 생각한 방법은 Intent-Filter를 설정해 주고 Intent를 이용해 이미지를 가져오는 것이었다.
Intent와 Intent-filter
참고: https://developer.android.com/guide/components/intents-filters
Intent는
- 특정 컴포넌트에 대한 실행정보를 넣어 실행하며 원하는 데이터를 전달이 가능하다
- 현재 목표는 암시적 인텐트를 사용해야 한다.
Intent-filter는
- 필터링 역할. 앱에 Intent가 전달되었을 때 Intent Filter에 기술된 조건에 맞는 Intent만 통과시킨다.
- Manifest에서 activity 내부에 선언하며, 해당 activity가 어떤 동작인지, 어떤 카테고리인지 명시하여 일치하는 암시적 인텐트를 받을 수 있도록 한다.
자자 그럼 Manifest에서 선언해 봅시다.
<activity android:name="com.easyhz.picly.view.StartActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 선언한 부분 -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- -->
</activity>
해당 코드에서 두 번째 intent-filter 부분을 선언해 주었다. 선언만으로 위에서 본 사진과 같이 어디서든 이미지 공유 버튼을 누르면 내 앱으로 연결이 된다.
그리고 액티비티에서 공유받은 이미지를 핸들링해 준다. (버전 분기가 필요했음.)
해당 프로젝트에서는 여러 장을 공유받을 가능성이 있기 때문에 분기 처리를 통해 리스트로 반환해 주었다.
private fun handleIncomingImage(): List<Uri> {
if (intent.type?.startsWith("image/") == false) return emptyList<Uri>()
val imageUris = when(intent.action) {
Intent.ACTION_SEND -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOfNotNull(intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
} else {
@Suppress("DEPRECATION")
listOfNotNull(intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))
}
}
Intent.ACTION_SEND_MULTIPLE -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.toList()
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.toList()
}
}
else -> emptyList<Uri>()
} ?: emptyList<Uri>()
return imageUris
}
별다른 문제없이 Content URI를 공유받았다.
content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F40/REQUIRE_ORIGINAL/NONE/image%2Fjpeg/1827778420
이렇게 불러온 URI로 이미지 등록을 해주면 되는 듯했으나.. 업로드하는 과정에서 이런 에러가 발생했다.
Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{658cd95 4923:packageName} (pid=4923, uid=10163) that is not exported from UID 10113
왜 이럴까?
에러에서 알 수 있듯이 크롬에 대한 권한이 없다..
Content Provider를 쓰려면 해당 앱에 권한을 요청하고 읽어와야 한다. 그러나 사용자가 어디에서 공유할지도 모르고, 언제 그 권한을 받고 있을까..라는 생각이 들었다. 근데 프로젝트 특성상 이미지의 가로 세로를 읽어와야 했는데, 이 로직은 문제가 없었다.
그럼 여기서 알 수 있는 사실 2가지
1. 이미지에 접근은 가능한 상태이다
2. 업로드 과정에서 이미지에 URI로 다시 접근할 때 오류가 발생했다
역시 예상대로 파이어베이스 스토리지 업로드 메서드를 뜯어보면 uri에 재접근하는 것이었다.
UploadTask(
StorageReference targetRef, StorageMetadata metadata, Uri file, Uri existingUploadUri) {
/* 중략 .. */
this.mUri = file; // putFile 메서드의 파라미터인 Uri
/* 중략 .. */
try {
ContentResolver resolver =
mStorageRef.getStorage().getApp().getApplicationContext().getContentResolver();
ParcelFileDescriptor fd = null;
try {
fd = resolver.openFileDescriptor(mUri, "r");
if (fd != null) {
size = fd.getStatSize();
fd.close();
}
}
/* 중략 .. */
}
/* 중략 .. */
inputStream = resolver.openInputStream(mUri);
/* 중략 .. */
}
그래서 이제 어떻게 하는데..
일단 본인이 생각한 방법은.. file provider로 사진을 캐시 폴더에 저장하고, 해당 URI로 접근하는 것이다.
> ???: 그냥 File 객체 만들어서 저장해도 되는데 File provider 왜 씀 ? ㅋㅋ
File Provider를 쓰지 않고 File 객체를 만들어서 저장하게 된다면 직관적이겠지만, 파일 보안에 대한 책임이 사용자에게 있고, 추후 프로젝트 고도화할 때 파일 공유에 대한 제약이 있을 것 같았다.
그래서 보안을 강화할 수 있으며 다른 앱에 대해 파일 공유를 간편하게 하기 위해 File provider를 쓴 것이었다. (프로젝트의 정체성이 '공유'에 있음)
File Provider
File Provider 를 사용하려면 Manifest에 명시해줘야 한다. 필자는 일단 export = "false"로 명시해줬다.(고도화 전이라서)
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
그리고 xml 디렉토리에 file_paths 선언 ( 일단 캐시 path 만 사용할 예정)
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="incoming_images"
path="images/" />
<cache-path
name="incoming_images"
path="images/" />
</paths>
그리고 FileProvider를 상속받은 클래스를 선언해 준다.
이 과정에서, 임시 파일을 생성하고 공유받은 이미지 Uri를 저장해 준다. 그리고 그 이미지의 contentUri 로 파이어베이스 스토리지에 업로드한다.
class PiclyFileProvider: FileProvider(R.xml.file_paths) {
companion object {
/**
* 공유받은 이미지 캐시 파일 생성하는 함수
* @return 캐시 파일 Content Uri
*/
suspend fun getIncomingImageUri(context: Context, uri: Uri): Uri {
val directory = File(context.cacheDir, "images")
directory.mkdirs()
// 임시 파일 생성
val file = File.createTempFile(
"incoming_image_",
".jpeg",
directory,
)
context.saveImage(uri, file) // 공유받은 이미지 Uri 를 활용해 임시 파일에 저장
val authority = context.packageName + ".fileprovider"
// Contnet Uri 반환
return getUriForFile(
context,
authority,
file,
)
}
}
}
공유받은 이미지 Uri 를 활용해 파일 저장하는 함수
/**
* 이미지 캐시 디렉토리에 저장하는 함수
*
*/
suspend fun Context.saveImage(uri: Uri, cacheFile: File) {
withContext(Dispatchers.IO) {
try {
contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(cacheFile).use { outputStream ->
inputStream.copyTo(outputStream, bufferSize = 1024)
}
}
} catch (e: Exception) {
throw e
}
}
}
앱을 실행해 주고, 앱으로 공유하면 정상적으로 작동한다! (업로드도 성공)
개선 사항
우려되는 문제점이 몇 가지 있다..
1. 사용자가 갤러리에 있는 사진 1,000억 장을 공유하면 어떡할래? ㅋㅋ
- 이 문제는 미리 고려하고, intent filter에서 이미지 uri에 대해 정제할 때 처리 완료했다. (maximum : 10)
2. 캐시 디렉토리에 데이터가 무한대로 쌓이면 어떡할래? ㅋㅋ
- 앱이 분명 무거워질 것이다..
방법 1) 주기적으로 사용자에게 캐시 폴더를 비워주는 작업을 요청한다.
방법 2) 프로젝트 특성상 만료된 사진이 있는데, 만료되면 캐시 폴더에서도 삭제한다.
=> 팀원들과 상의 후에 적용 여부를 판단
3. 캐시 디렉토리에 중복되는 데이터는 어떡할래? ㅋㅋ 비효율적인데 ㅋㅋ
- 업로드하고 해당 이미지를 캐시 폴더에서 지운다 -> 문제 2번과 연관이 있다. 그러나 고도화 작업 시 공유할 이미지가 없어진다는 것.
- 프로젝트 특성상 만료된 사진이 있는데, 만료되면 캐시 폴더에서도 삭제한다. -> 이 부분도 문제 2번과도 연관이 있다.
이러한 문제점에 대해서는 조금 더 고민이 필요할 것 같다
다른 생각이나 방법이 있다면 공유 부탁드립니다!
참고자료
https://developer.android.com/guide/components/intents-filters
https://developer.android.com/reference/androidx/core/content/FileProvider
'안드로이드' 카테고리의 다른 글
[Android] 전략 패턴(Strategy Pattern)을 적용하여 소셜 로그인 리팩토링 하기 (0) | 2024.08.12 |
---|---|
[Android] 네트워크 예외 처리 (에러 핸들링)- Retrofit CallAdapter (0) | 2024.06.16 |
[Android] MVI 패턴을 알아볼까? (1) | 2024.06.07 |
[Android] 메인 화면 성능 개선하기 대작전 (1) | 2024.04.19 |
[Android] Animation Transition 시 이미지 로딩 개선하기 (0) | 2024.04.14 |