이것저것 공부한 기록

[Android] 어플리케이션 간 파일 공유하기 (broadcast로) 본문

Study/Kotlin&Android

[Android] 어플리케이션 간 파일 공유하기 (broadcast로)

블랜디 2022. 11. 22. 10:06

이번 프로젝트 추가 구현 사항으로 서로 다른 패키지의 어플리케이션 간에 파일을 옮기는 구현이 필요하여 저장소 권한에 대해서 찾아보게 되었다.

 

안드로이드 Q(버전 10) 이후 저장소 사용 방법이 변경됨에 따라, 안드로이드 어플리케이션들은 외부저장소에 공용공간과 각자의 영역을 나누어 갖게 되었다. 각자의 영역에는 서로 접근이 불가능하고, 공용공간에는 다같이 접근이 가능하나 사진, 동영상, 음악의 경우엔 MediaStore 등 안드로이드에서 제공하는 API를 사용해야하고, 그 외 기타 파일들은 익히 아는 Downloads 폴더에 접근해야하며 파일탐색기 등의 UI를 통해 명시적으로 사용자가 지정한 파일에만 접근할 수 있다.

 

아래 블로그에 간단히 잘 설명되어 있음.

 

[안드로이드] 저장소 사용하기 - 2. Scoped Storage

안드로이드 버전10 이상부터는 Scoped Storage를 사용합니다. 이전 버전이였던 Legacy Storage와 어떤차이가 있는걸까요? 지난 포스트에서도 말했듯이 안드로이드의 저장소는 크게 내부저장소와 외부저

kimdabang.tistory.com

 

내가 필요한 구현사항은 아래와 같다.

 

1. 기본 설치 된 앱 A가 있음

2. 추가 data를 asset에 가진 앱B가 기본 앱 A에 파일을 넘김 (Broadcast)

3. 파일을 받은 A는 추가 data파일을 외부저장소에 저장해서 사용

 

사용할 것들 : AssetManager, FileProvider, BroadcastReceiver callback, ContentResolver

 

우선 앱 B를 생성하여 asset에 필요한 파일들을 추가해준다.

실제 복사할 디렉토리 구조와 동일하게 구성해줌.

 

실제 코드를 쓰긴 좀 힘들고 그냥 대충 pseudo 코드 혼합으로..

 

 

1. 데이터 전달 앱 B

 

FileProvider를 사용하기위해 AndroidMenifest.xml 파일 application영역에 provider 설정을 추가한다.

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.프로젝트명.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true" >
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

 

위에 기재한대로 xml폴더에 file_paths.xml을 생성해준다. (위치: res/xml)

다른 앱에 FileProvider를 통해 생성한 URI를 공유할 경로를 지정한다.

<paths>
    <external-files-path
        name="external_files_path"
        path="/"/>
    <external-path
        name="external_files"
        path="/"/>
</paths>

 

 

 

이후 assetManager에서 파일을 가져와 외부저장소에 copy한다.

var inputStream = assetManager.open(filename)
var newFileName = context.getExternalFilesDir(null)!!.absolutePath + "/" + filename
var outputStream = FileOutputStream(newFileName)

// 파일 복사
// inputstream 및 outputstream 닫고 flush

 

 

FileProvider에서 getUriForFile을 해준다

var file = File(newFileName)
var fileUri: Uri = FileProvider.getUriForFile(
        this,
        "com.example.프로젝트명.fileprovider",
        file
)

 

Uri를 여러개 넘겨야 했고, 각 파일별로 다른 path에 저장해야 했는데

마땅한 방법을 찾지못해 uri list, Filepath list, Filename list를 별도로 전달했다.

//uri와 마지막 접근 경로/ 파일 이름 저장
uriList.add(fileUri)
pathList.add(lastFilePath)
nameList.add(lastFileName)

원래 StartActivity로 intent를 전달해줄 경우 전달하는 Intent에 permission을 지정해서 전달하면 되는데,

아무리 전달해도 권한이 없다고 자꾸 중지되어 왜 이러는지 확인해보니

나는 sendBroadcast로 intent를 전달하고 있는데, sendBroadcast는 intent에 permission을 지정해서 넘길 수 없다고 한다.

 

때문에 각 file uri별로 지정한 패키지에서 접근할 수 있는 permission을 지정해줬다.

this.grantUriPermission("받을 패키지명", fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)

 

 

추가로 Broadcast 전달 후 결과에 따라 토스트 팝업을 띄우고 추가 작업을 하기 위해 broadcastReceiver를 구현해서

sendOrderedBroadcast전달 시 인자로 넘겨 결과를 받았다.

    val notificationResultReceiver : BroadcastReceiver = object:BroadcastReceiver() {
        override fun onReceive(p0: Context?, p1: Intent?) {
            if( resultCode == RESULT_OK ){
                val toast = makeText(applicationContext, "data deliver success", Toast.LENGTH_SHORT)
                toast.show()
            } else {
                val toast = makeText(applicationContext, "data deliver failed", Toast.LENGTH_SHORT)
                toast.show()
            }
        }
    }
   
   //----intent 선언 및 extra에 list추가----//
    
    try {
        sendOrderedBroadcast(전달할 intent, null, notificationResultReceiver, null, RESULT_OK, null, null )
    } catch (e: Exception) {
        e.printStackTrace()
        log.e("Intent", "file copy failed")
        val toast = makeText(applicationContext, "Intent deliver failed", Toast.LENGTH_SHORT)
        toast.show()
        }

startActivity에서 결과를 받아올 수 있는 구현은 아래 블로그 참조.

 

registerForActivityResult 구현방법 정리 # 예전 onActivityResult

오늘은 Activity간에 데이터를 주고받을 때 사용해야 하는 registerForActivityResult에 대해서 정리해 보도록 하겠습니다. 기존에는 사용하지 않던 API이어서 조금은 낯설지도 모르겠습니다. 1. 예전구현

developer88.tistory.com

 

 

2. 데이터 수신 앱 A

 

file uri list를 받아 ContentResolver를 사용해 파일을 가져온 뒤

복사하여 외부 저장소에 저장하고, result code를 세팅하는 Broadcast receiver를 구현해준다.

class DataReceiverA : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if ("지정한 액션명"== intent.action ) {
            Timber.d("received update broadcast")
            val uris = intent.getParcelableArrayListExtra<Uri>("Extra명")
            val pathList = intent.getStringArrayListExtra("Extra명")
            val nameList = intent.getStringArrayListExtra("Extra명")
            Timber.d("receivedDatasize : ${uris!!.size}")


            try {
                for (i in 0 until uris!!.size) {
                    val newUri = uris[i]
                    val newFilePath = pathList?.get(i)
                    val newFileName = nameList?.get(i)

                    val resolver = context.contentResolver
                    val filesDirPath = context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
                    val mTargetBasePath = filesDirPath + "/app/"
                    Timber.d("filepath : $mTargetBasePath")
                    Timber.d("copyfileoutname : $mTargetBasePath$newFilePath")
                    val dir = File("$mTargetBasePath$newFilePath")
                    dir.mkdirs()

                    val inputStream = resolver.openInputStream(newUri!!)
                    val outputStream = FileOutputStream(mTargetBasePath + newFilePath + newFileName)
                    val buffer = ByteArray(1024)
                    var read: Int
                    while (inputStream!!.read(buffer).also { read = it } != -1) {
                        outputStream.write(buffer, 0, read)
                    }

                    inputStream!!.close()
                    outputStream.flush()
                    outputStream.close()
                    resultCode=RESULT_OK
                }
            } catch (e: FileNotFoundException) {
                Timber.e("file not found")
                resultCode= RESULT_CANCELED
            }
            Timber.d("end Copy")
        }
    }
}

 

Manifest 내에 Receiver를 선언해준다.

   <receiver
       android:name=".receiver.DataReceiverA"
       android:enabled="true"
       android:exported="true">
       <intent-filter>
            <action android:name="Intent명" />
       </intent-filter>
   </receiver>

 

 

휴 이번 난관도 어째저째 넘어갔다...