본문 바로가기
Android/Kotlin

Android 파일 다운로드 받기

by wannagohome97 2024. 1. 5.

플레이스토어에 배포되지 않은 앱을 다운로드/업데이트

일단 앱의 apk 파일 다운로드 링크(아래 코드의 url)를 서버에 올려두고 진행했고

버전 정보를 체크하는 방법은 생략했다

 

일단 코드부터 살펴보자

val intent = Intent(requireActivity(), ApkDownloadService::class.java)
intent.putExtra(ApkDownloadService.PATH, filePath)
intent.putExtra(ApkDownloadService.RECEIVER, ProgressReceiver())
requireActivity().startService(intent)
class ApkDownloadService: IntentService(TAG) {
    companion object{
        const val TAG = "ApkDownloadService"
        const val PATH = "apk_download_service"
        const val RECEIVER = "apk_result_receiver"
        const val RESULT_PROGRESS = "apk_download_progress"
        const val RESULT_CODE = 20240104
        const val RESULT_CODE_FAIL = -1
    }
    private val mIoScope = CoroutineScope(Dispatchers.IO)


    override fun onHandleIntent(intent: Intent?) {
        val url = intent?.getStringExtra(PATH)
        val receiver = intent?.getParcelableExtra(RECEIVER) as ResultReceiver?
        mIoScope.launch {
            getApkFile(url = url, receiver = receiver)
        }
    }
    fun getApkFile(url: String?, receiver: ResultReceiver?){
        val client = OkHttpClient.Builder().build()
        val request = Request.Builder().url(url!!).build()
        // 서버와 통신에 필요한 옵션(Cookie 나 인증 같은 부분)은 위 두줄의 각 Builder()에서 세팅
        val response = client.newCall(request).execute()
        if (response.isSuccessful){
            try{
                val body = response.body
                val data = body?.byteStream()
                val length = body?.contentLength()?.toInt()!!
                val byteArray = ByteArray(length)
                val wrapper = ContextWrapper(baseContext)
                val dirDownload = wrapper.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
                val file = File(dirDownload, "update.apk") // 업데이트용 임시 apk 파일
                val fos = FileOutputStream(file)
                var isRead = true
                var total = 0L
                while (isRead){
                    val read = data?.read(byteArray)!!
                    if (read<=0){
                        isRead = false
                    }
                    else {
                        total += read
                        // 이 쪽에서 ResultReceiver 를 사용하여 프로그레스를 UI 스레드에서 전달받을 수 있다
                        // 추후 Install/Update 코드에서 사용 된다 
                        val resultData = Bundle()
                        resultData.putInt(RESULT_PROGRESS, (total*100/length).toInt())
                        receiver?.send(RESULT_CODE, resultData)
                        fos.write(byteArray, 0, read)

                    }
                }
                data?.close()
                fos.flush()
                fos.close()
                response.close()
            }
            catch(e:Exception){
                e.printStackTrace()
            }
        }
    }
}

 

REST 쪽은 OkHttp 라이브러리를 사용했다.

 

파일이 다운로드 되는 원리는 다운로드 Url 의 ResponseBody 를 ByteStream 으로 받아온 뒤

FileOutputStream 으로 지정한 임시 파일에 write 하는 형태이다.

 

 

여기까지하면 "Download" 까지 진행 된 것이다.

굳이 다운로드라고 강조한 이유는 추후 Install/Update 까지 해 주어야 비로소 앱이 설치 혹은 업데이트 되었다고

할 수 있기 때문이다.

 

그럼 이 File 을 Install 해달라고 요청해보...기 전에 우선 Manifest 작업이 필요하다.

 

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.FileProvider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>

 

그리고 provider_paths.xml 은 

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external_path"
        path="."/>
    <external-files-path
        name="external_files_path"
        path="."/>
</paths>

 

이런 식으로 작성해주면 된다

 

이제 Download 메서드 실행직후 ~ 완료까지의 작업을 처리해줄 Receiver 를 아래와 같이 작성하면 된다. 

class ProgressReceiver(resultHandler: Handler): ResultReceiver(resultHandler){

        override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
            super.onReceiveResult(resultCode, resultData)
            if (resultCode == ApkDownloadService.RESULT_CODE){
                val progress = resultData?.getInt(ApkDownloadService.PROGRESS)!!
                if (progress<100){
                    resultHandler.post {
                        updateProgress(progress)
                        // 여기서 Percentage 를 UI 에 표기했다.
                    }
                }
                if (progress == 100){
                    val intent = Intent()
                    val file = ApkDownloadService.getFile(requireContext())
                    val uri = FileProvider.getUriForFile(
                        context!!,
                        AUTHORITY,
                        file
                    )
                    intent.setDataAndType(uri, "application/vnd.android.package-archive")
                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    startActivity(intent)
                    resultHandler.removeCallbacksAndMessages(null)
                }
            }
        }
    }

 

위에 Manifest와 xml 작업을 했던 이유가 마지막 코드블록에 있다.

FileProvider 를 통해 해당 파일을 실행시킬 수 있는 Uri 가 필요하다.

이 Uri 를 Intent 에 넣어서 실행시키면 권한 부여에 동의하라는 여러 체크박스들이 나오고 다 넘기면

앱이 비로소 Install / Update 된다.