핀수로그
  • [Android] 플레이스토어 없이 앱 업데이트하기
    2024년 04월 03일 00시 22분 21초에 업로드 된 글입니다.
    작성자: 핀수
    728x90
    반응형
     

    들어가며

    앱을 플레이스토어에 게시하지 않고, 앱 자체에서 업데이트를 수행하는 기능을 검토하게 되었다.

    사실 제일 깔끔한 것이 플레이스토어에 업로드하고, 구글이 시키는대로 하는 것이다.

    앱 내부에서 업데이트를 진행하고 싶다면 인 앱 업데이트 키워드로 검색하면 된다.

    아무튼 여러 이유로 앱을 게시하지 않고, 특정 집단에만 배포하고 싶은 경우도 있을 것이다.

    이럴 때는 어떻게 앱이 최신상태를 유지할 수 있을까?

    내가 알아본 것을 기술해보도록 하겠다...

     

    1. 라이브러리 이용하기

    공교롭게도(?) 22년에 해당 기능을 검토한 적이 있었는데, 이때는 라이브러리를 이용해 대강 구현을 했었다.

    여러 라이브러리가 존재하겠지만 나는 AppUpdater라는 라이브러리를 이용했다.

    사용 방법이 굉장히 간단하니 간단하게 작성해보도록 하겠다.

     

    GitHub - javiersantos/AppUpdater: A library that checks for your apps' updates on Google Play, GitHub, Amazon, F-Droid or your o

    A library that checks for your apps' updates on Google Play, GitHub, Amazon, F-Droid or your own server. API 9+ required. - javiersantos/AppUpdater

    github.com

    종속 항목 추가

    // AppUpdater
    implementation 'com.github.javiersantos:AppUpdater:2.7'

    코드 작성

    // 앱 업데이트
    AppUpdaterUtils(this)
        .setUpdateFrom(UpdateFrom.JSON)
        .setUpdateJSON("your path")
        .withListener(appUpdateListener)
        .start()

     

    - setUpdateFrom : 업데이트에 관한 정보의 확장자명을 의미한다.

    아래와 같은 형식으로 작성해주면 된다.

    {
        "latestVersion": "0.0.0", // 최신 버전 이름
        "latestVersionCode": 0, // 최신 버전 코드
        "url": "your path", // apk를 다운받을 수 있는 경로
        "releaseNotes": [
            "- Write something you want"
        ]
    }

     

    - setUpdateJSON : 업데이트 정보를 확인할 수 있는 경로를 입력한다.

    그러니까 저 경로를 타고 갔을 때 위의 업데이트 정보를 읽어올 수 있으면 된다.

    나는 테스트를 위해 깃허브를 데이터 저장소처럼 사용하여 확인했다.

    - withListener : 업데이트 정보를 읽어왔을 때의 리스너를 지정한다.

    private val appUpdateListener = object : AppUpdaterUtils.UpdateListener {
        override fun onSuccess(update: Update?, isUpdateAvailable: Boolean?) {
            update?.let {
                Log.d(TAG, "lastest version: ${it.latestVersion}")
                Log.d(TAG, "latestVersionCode: ${it.latestVersionCode}")
                Log.d(TAG, "releaseNotes: ${it.releaseNotes}")
                Log.d(TAG, "urlToDownload: ${it.urlToDownload}")
    
                if (it.latestVersionCode > BuildConfig.VERSION_CODE) {
                    appDownload()
                }
    
    
            }
        }
    
        override fun onFailed(error: AppUpdaterError?) {
            Log.d(TAG, "onFailed: ${error.toString()}")
        }
    
    }

     

    이 부분에서 현재 앱의 버전 코드와 최신 버전의 버전 코드를 비교해

    최신 부분이 존재하면 앱을 설치할 수 있도록 했다.

    2. 직접 구현하기

    흐름은 라이브러리를 사용하는 것과 별반 다르지 않다.

    앱의 최신 버전이 있는지 확인하고, 있으면 앱을 설치하는 것이다.

    앱을 다운받기

    /**
     * 내부저장소에 파일 저장
     * - see : https://stackoverflow.com/questions/12585263/save-file-to-internal-memory-in-android
     */
    private fun downloadApk(path: String): Boolean {
        try {
            val url = URL(path)
    
            val urlConnection = url.openConnection()
            urlConnection.readTimeout = 5000
            urlConnection.connectTimeout = 10000
    
            val inputStream = urlConnection.getInputStream()
            val bufferedInputStream = BufferedInputStream(inputStream, 1024 * 5)
    
            val file = File(filesDir, APP_NAME)
    
            if (file.exists()) {
                file.delete()
            }
            file.createNewFile()
    
            val fileOutputStream = FileOutputStream(file)
            val buff = ByteArray(size = 4 * 1024)
    
            var len: Int
            while (bufferedInputStream.read(buff).also { len = it } != -1) {
                fileOutputStream.write(buff, 0, len)
            }
    
            fileOutputStream.flush()
            fileOutputStream.close()
            bufferedInputStream.close()
        } catch (e: Exception) {
            e.printStackTrace()
            return false
        }
    
        return true
    }

     

    원래는 DownloadManager를 통해서 apk를 다운받았었는데, 

    이후에 앱을 디바이스에 다운받은 apk를 설치하려고 하니 접근이 안되는 것이다.

    앱이 있는데도 없다고 하거나, 권한이 없다는 에러가 발생했다.

    알고보니 버전 10(Q)부터 정책이 바뀌어 외부 저장소 접근이 제한되었다는 것이다.

    그래서 이를 해결하는 방법이 외부 저장소 파일을 내부로 복사하면 된다는 거였는데

    아니 접근이 안된다니까욧...(내가 잘못 짰을 확률 100)

    그래서 그냥 바로 내부저장소에 다운받을 수 있도록 했다.

     

    DownloadManaer를 통해 내부 저장소에 앱을 다운받으려고 했었는데

    앱의 내부 저장소는 앱 자체에서만 접근할 수 있어서, DM으로는 진행할 수 없었다.

     

    앱이 내부 저장소에 다운로드 된 것을 확인할 수 있다.

    apk를 설치하기

    PackageInstallerService API를 이용해 앱을 업데이트할 수 있다고 구글링을 통해 확인했는데,

    해당 API를 사용하려면 INSTALL_PACKAGES 권한이 필요한데 이 권한은 기기 소유자에게만 부여된다는 것이다.

    그러니까 시스템 앱에만 주어진다는 것이다.

     

    *시스템 앱 

    스마트폰이나 태블릿 등 안드로이드 기반 기기에서 운영체제와 함께 실행되는 핵심 애플리케이션

    마켓에 등록되지 않고, 기기 제조사나 통신사에 의해 사전설치, 업데이트 된다. 

    (참고: https://blogdeveloperspot.blogspot.com/2023/08/blog-post_50.html)

     

    그래서 시스템에 설치되어 있는 PackageInstaller 앱을 통해 원하는 apk를 설치해달라고 요청하는 방식으로

    apk를 설치할 수 있다고 해서, 한번 시도해보았다.

    권한 등록

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

    코드 작성

    코드에 관한 자세한 설명은 여기에서 확인할 수 있다.

    이 분이 작성한 코드를 그대로 옮겨왔기 때문에, 별다른 설명을 하진 않겠다.

    정말 자세하게 작성해두셨음..

    /**
     * apk 설치
     * - see: https://codechacha.com/ko/how-to-install-and-uninstall-app-in-android/
     */
    private fun installAPK() {
        CoroutineScope(Dispatchers.Main).launch {
            val apkPath = "${filesDir.absolutePath}/${APP_NAME}"
            val apkUri = FileProvider.getUriForFile(
                applicationContext,
                "${BuildConfig.APPLICATION_ID}.fileprovider",
                File(apkPath)
            )
            val intent = Intent(Intent.ACTION_VIEW)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
            startActivity(intent)
        }
    
    }

    provider 정의

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="[your_application_id].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

    아래와 같이 설정하면 내 앱의 데이터 폴더의 ../files/~.apk 파일을 다른 앱과 공유할 수 있다고 한다.

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <files-path path="/" name="your_app_name.apk" />
    </paths>

     

    구현 결과

    앱이 업데이트 되어도 데이터는 그대로 남아있는지 확인해보기 위해 간략한 테스트도 같이 해보았다.

    그런데 첫번째 시도는 잘 되는데 두번째 설치에서는 다운로드는 되는데 설치가 되지 않는 문제가 있다.

    자세한 것은 실제 적용하게 될 경우에 좀 더 파봐야 알 것 같다...뭐가 문제인지..아시는 분 계시면 댓글 부탁드립니다 ㅜ0ㅜ

     

    그리고 출처를 알 수 없는 앱 설치를 허용해주어야 한다.

    외부 앱을 설치할 수 있도록 최초 한번만 허용해주면 되는데

    무시하고 설치 팝업은 계속해서 뜨더라 ㅜㅜ 이것도 안나왔으면 좋겠다 하지만 그럴 순 없겠지

    이건 어디까지나 적법한?루트를 타는 것이 아니니 말이다...

    암튼 오랜만에 안드로이드 할 수 있어서 즐거웠다.

    역시 배우는 것은 신난당


    공부하며 작성된 글이라 잘못된 정보가 있을 수 있습니다.

    말씀해주시면 수정하겠습니다. 감사합니다.

    References

    아래 글을 참고하여 작성 되었습니다.

     

    안드로이드 - 코드로 앱(apk) 설치, 삭제하는 방법

    안드로이드에서 코드로 앱(apk) 설치 및 삭제하는 방법을 설명합니다. 앱은 Google PackageInstaller에게 앱을 삭제하거나 설치하도록 요청할 수 있습니다. 설치 요청하려면 REQUEST_INSTALL_PACKAGES 권한이

    codechacha.com

     

    [Android] DownloadManager로 파일 다운로드 받기

    🌠 DownloadManager 사용하기 🌿

    khs613.github.io

     

     

    [Android][scoped storage] open failed: EACCES (Permission denied)

    안드로이드 스튜디오에서 AVD를 새로 만들고 테스트를 진행하던 중.. 갑자기 기존에는 발생하지 않던 에러(exception)이 발생했습니다. W/System.err: java.io.FileNotFoundException: /storage/emulated/0/Download/photo-1

    darkstart.tistory.com

     

     

    Android PackageInstaller, re-open the app after it updates itself

    I'm developing an app that runs as Device Owner, and I want to build an automatic updater inside it. To do it I use the PackageInstaller, as I have the privileges to use it due to my Device owner

    stackoverflow.com

     

    728x90
    반응형
    댓글