SAF (Storage Access Framework) 는 파일에 접근 할 수 있는 시스템 UI 를 제공 받을 수 있습니다.  일종의 파일 탐색기(?) 같은 느낌을 받았어요. 꽤 오래전(Android 4.4 이후) 부터 지원하고 있었는데 많이 사용...

[안드로이드] SAF(Storage Access Framwork) 파일 생성/열기 예제

 


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: