SAF (Storage Access Framework) 는 파일에 접근 할 수 있는 시스템 UI 를 제공 받을 수 있습니다.
일종의 파일 탐색기(?) 같은 느낌을 받았어요.
꽤 오래전(Android 4.4 이후) 부터 지원하고 있었는데 많이 사용되었을지는 의문입니다..ㅎㅎ
근데 많은 분들이 이미 알고 계시겠지만,
보안 관련하여 안드로이드 저장소 정책이 변경됨에 따라(..ㅠㅠ)
SAF, MediaStore 에 익숙해질 필요가 있다고 생각해서 SAF 먼저 간단한 예제로 정리하려고 합니다.
1. 예제 다운로드
https://github.com/bictoselfdev/exampleSAF
2. SAF Manager
SAF 를 사용하기위해 따로 SafManager object 를 만들었습니다.
이를 활용하여 파일 생성 / 열기 를 하려고합니다.
object SafManager {
const val REQUEST_CODE_CREATE_SAF = 100
const val REQUEST_CODE_OPEN_SAF = 101
@JvmStatic
fun createFileSAF(activity: Activity, mimeType: String, fileName: String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, fileName)
}
activity.startActivityForResult(intent, REQUEST_CODE_CREATE_SAF)
}
@JvmStatic
fun openFileSAF(activity: Activity, mimeType: String) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
}
activity.startActivityForResult(intent, REQUEST_CODE_OPEN_SAF)
}
@JvmStatic
fun getFileFromSAF(context: Context, uri: Uri): File {
val file = File(context.filesDir.path + File.separatorChar + queryName(context, uri))
try {
context.contentResolver.openInputStream(uri).use { inputStream -> createFileFromStream(inputStream, file) }
} catch (e: Exception) {
e.printStackTrace()
}
return file
}
@JvmStatic
fun createFileFromStream(inputStream: InputStream?, file: File?) {
try {
if (inputStream == null) return
FileOutputStream(file).use { os ->
val buffer = ByteArray(4096)
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
os.write(buffer, 0, length)
}
os.flush()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@JvmStatic
private fun queryName(context: Context, uri: Uri): String? {
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.let {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val name = cursor.getString(nameIndex)
cursor.close()
return name
}
return null
}
@JvmStatic
fun getTextFromSAF(context: Context, uri: Uri): String {
val byteArrayOutputStream = ByteArrayOutputStream()
var readLength = 0
try {
context.contentResolver.openInputStream(uri).use { inputStream ->
while (inputStream?.read()?.also { readLength = it } != -1) {
byteArrayOutputStream.write(readLength)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return byteArrayOutputStream.toString()
}
@JvmStatic
fun getImageFromSAF(context: Context, uri: Uri): Bitmap? {
var bitmap: Bitmap? = null
try {
context.contentResolver.openInputStream(uri).use { inputStream -> bitmap = BitmapFactory.decodeStream(inputStream) }
} catch (e: Exception) {
e.printStackTrace()
}
return bitmap
}
}
3. 파일 생성 (*.txt)
A. 파일 생성을 위한 ACTION_CREATE_DOCUMENT Intent 실행
fun createFileSAF(activity: Activity, mimeType: String, fileName: String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, fileName)
}
activity.startActivityForResult(intent, REQUEST_CODE_CREATE_SAF)
}
B. ACTION_CREATE_DOCUMENT Intent 가 실행되었다면 "저장" 버튼으로 파일 생성
C. onActivityResult 메서드에서 생성한 파일의 uri 정보 받고 활용하기
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when (requestCode) {
REQUEST_CODE_CREATE_SAF -> {
val uri = data?.data
uri?.let {
/**************************
* 생성된 파일 uri 활용하기
**************************/
// Hello World 쓰기
try {
val fileDescriptor = contentResolver.openFileDescriptor(uri, "w")
val fileOutputStream = FileOutputStream(fileDescriptor?.fileDescriptor)
fileOutputStream.write("Hello World".toByteArray())
fileOutputStream.close()
fileDescriptor?.close()
} catch (e: Exception) {
e.printStackTrace()
}
// 파일 정보 가져오기
val file = SafManager.getFileFromSAF(this, uri)
setFileInfo(uri, file)
// 파일 내용 가져오기
when (file.extension) {
"txt" -> {
val contents = SafManager.getTextFromSAF(this, uri)
binding.layoutFileContents.tvContents.text = contents
binding.layoutFileContents.ivImage.setImageBitmap(null)
}
else -> {
binding.layoutFileContents.tvContents.text = ""
binding.layoutFileContents.ivImage.setImageBitmap(null)
}
}
}
}
... (생략)
}
}
}
4. 파일 열기
A. 파일 열기를 위한 ACTION_OPEN_DOCUMENT Intent 실행
fun openFileSAF(activity: Activity, mimeType: String) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
}
activity.startActivityForResult(intent, REQUEST_CODE_OPEN_SAF)
}
B. ACTION_OPEN_DOCUMENT Intent 가 실행되었다면 파일 선택
C. onActivityResult 메서드에서 선택한 파일의 uri 정보 받고 활용하기
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when (requestCode) {
...(생략)
REQUEST_CODE_OPEN_SAF -> {
val uri = data?.data
uri?.let {
/**************************
* 선택한 파일 uri 활용하기
**************************/
// 파일 정보 가져오기
val file = SafManager.getFileFromSAF(this, uri)
setFileInfo(uri, file)
// 파일 내용 가져오기 (txt 또는 jpg,png)
when (file.extension) {
"txt" -> {
val contents = SafManager.getTextFromSAF(this, uri)
binding.layoutFileContents.tvContents.text = contents
binding.layoutFileContents.ivImage.setImageBitmap(null)
}
"jpg",
"png" -> {
val bitmap = SafManager.getImageFromSAF(this, uri)
binding.layoutFileContents.tvContents.text = ""
binding.layoutFileContents.ivImage.setImageBitmap(bitmap)
}
else -> {
binding.layoutFileContents.tvContents.text = ""
binding.layoutFileContents.ivImage.setImageBitmap(null)
}
}
}
}
}
}
}
4. 결과
[참고자료]
https://codechacha.com/ko/android-storage-access-framework/
0 comments: