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

자신의 앱에서 어떤 apk 파일을 설치하고 싶을 때가 있습니다. 안드로이드의 PackageInstallerService는 디바이스에 apk를 설치할 수 있는 API를 제공합니다. 앱은 이 API를 사용하면 apk를 설치할 수 있습니다. 하지만 이 API를 사용하려면 android.permission.INSTALL_PACKAGES 이라는 퍼미션이 필요합니다. 이 퍼미션은 시스템(privileged)앱 또는 플랫폼 key로 서명된 앱만 받을 수 있기 때문에 일반적인 앱들은 이 방법을 사용할 수 없습니다.

그럼 어떻게 앱을 설치해야 할까요? 시스템에는 PackageInstaller라는 앱이 있습니다. 이 앱은 위의 퍼미션을 갖고 있어 PackageInstallerService의 API를 사용하여 apk를 설치할 수 있습니다. 우리는 이 앱에 어떤 apk파일을 대신 설치해달라고 요청할 수 있습니다. 몇몇 조건만 만족되면 PackageInstaller앱은 우리의 apk를 대신 설치해 줍니다.

앱이 PackageInstaller앱에 설치를 요청하는 방법에 대해서 알아보겠습니다.

이 글에서 쓰인 코드는 모두 코틀린으로 작성되었습니다.

PackageInstaller 앱은 무엇인가?

이 앱의 패키지 이름은 com.google.android.packageinstaller입니다. 이 앱은 구글에서 제공하는 app입니다. 모든 디바이스에는 이 앱이 설치되어있습니다. 우리는 이 앱에 인텐트를 보내 설치를 요청할 수 있습니다.

Apk 준비하기

앱 설치를 요청하기 전에 apk파일이 있어야 합니다. 여러분들의 앱은 서버에서 가져온 apk, 또는 미리 준비된 apk를 설치하는 시나리오를 갖고 있을 수 있습니다. 저는 Assets에 apk를 저장해두고, 이것을 앱의 files 폴더에 복사 후 PackageInstaller에 설치를 요청하도록 구현하였습니다.

아래 코드는 /assets/app.apk파일을 앱의 files폴더인 "/data/data/comd.codechacha.sample/files/app.apk"에 복사하는 코드입니다.

val inputStream = assets.open("app.apk")
val outPath= filesDir.absolutePath + "/app.apk"
val outputStream = FileOutputStream(outPath)
while (true) {
    val data = inputStream.read()
    if (data == -1) {
        break
    }
    outputStream.write(data)
}
inputStream.close()
outputStream.close()

adb shell로 원하는 위치에 파일이 복사된 것을 확인하였습니다.

generic_x86:/data/data/com.codechacha.sample/files # ls
app.apk

Apk 설치 요청하기

지금까지 앱의 데이터 폴더에 apk를 받아둔 상태입니다. 이제 이 파일을 PackageInstaller에게 설치해달라고 요청하면 됩니다.

먼저 앱의 AndroidManifest.xml에 다음과 같이 퍼미션을 등록해야 합니다. 앱이 설치되도 이 권한을 받지 못합니다. 이 권한은 사용자가 설정앱에서 직접 부여해야 받을 수 있습니다. 권한을 받는 방법은 아래에서 설명하겠습니다.

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

다음 코드는 앱의 apk를 PackageInstaller에게 설치해달라고 요청하는 코드입니다.

val apkPath= filesDir.absolutePath + "/app.apk"
val apkUri =
    FileProvider.getUriForFile(applicationContext,
        BuildConfig.APPLICATION_ID + ".fileprovider", File(apkPath))  // 1

val intent = Intent(Intent.ACTION_VIEW)   // 2
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK  
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)  // 3
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")  // 4
startActivity(intent)  // 6

"// 1"처럼 번호로 표기한 것을 아래에 자세히 설명하였습니다.

  1. Uri는 Apk파일의 위치 정보를 갖고 있습니다. 여기서 FileProvider를 사용하였는데요.

안드로이드는 서로 다른 앱의 데이터 폴더에 직접적으로 접근하는 것을 금지하기 때문입니다. FileProvider를 사용하면 앱 끼리 데이터를 공유할 수 있습니다. 2. PackageInstaller를 실행하기 위해, 암시적 인텐트의 action은 ACTION_VIEW로 설정합니다. 3. 인텐트를 받은 PackageInstaller가 자신의 앱의 데이터에 접근할 수 있는 권한을 부여합니다. 4. Data는 위에서 만든 Uri를 넣고, 인텐트의 타입은 "application/vnd.android.package-archive"로 설정합니다. 타입은 PackageInstaller를 암시적 인텐트로 찾기 위해 필요합니다.

위의 코드에서 apk파일을 PackageInstaller에 제공할 때 FileProvider를 이용하였습니다.

FileProvider로 다른 앱과 파일을 공유하려면 AndroidManifest.xml에 다음과 같이 provider가 정의되어야 합니다.

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

xml/file_pathsres/xml/file_paths.xml 파일을 가리키며 다음과 같이 정의하시면 됩니다. 이렇게 설정하면 내 앱의 데이터 폴더의 ../files/app.apk 파일을 다른 앱과 공유할 수 있습니다.

<paths>
    <files-path path="/" name="app.apk" />
</paths>

위 코드를 실행시키면 다음 화면이 나옵니다. 이 화면은 세팅에 가서 앱에게 설치를 요청할 수 있는 권한을 줘야 한다는 것입니다. 보안적인 이유로, 앱이 apk설치를 요청하려면 사용자에게 허락을 맡아야 합니다. 한번만 설정하면 그 다음부터는 뜨지 않습니다. package installer

Settings 버튼을 누르면 다음 화면이 나옵니다. Allow from this source을 눌러 활성화 시키면, 이 앱은 apk를 설치 요청할 수 있는 권한을 부여받습니다. package installer

다시 설치화면으로 돌아오면 다음과 같은 화면이 보입니다. 이제 Install버튼을 누르면 설치가 됩니다. package installer

다음 화면이 보이면 설치가 모두 완료된 것입니다. package installer

퍼미션 세팅 화면 띄우기

앱 설치를 요청할 때 위에서 본 것처럼, 권한이 없으면 설정화면으로 이동하라는 안내 메시지가 뜹니다. 하지만 앱에서 미리 보여주고 권한을 받고 싶을 수도 있습니다.

다음 코드는 권한을 갖고 있는지 체크하고 없다면 세팅 화면을 띄우는 코드입니다.

if (packageManager.canRequestPackageInstalls()) {  // 1
    // request to install apk
} else {
    val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,  // 2
            Uri.parse("package:$packageName"))
    startActivityForResult(intent, REQUEST_INSTALL_PERMISSION)  // 3
}
  1. 권한이 있다면 canRequestPackageInstalls()는 true를 리턴합니다.
  2. 권한을 부여하는 세팅 화면에 대한 인텐트입니다.
  3. 실행하면 설정화면이 뜹니다.

세팅화면이 종료되면 onActivityResult()가 호출되어 앱을 설치하거나 다른 처리를 할 수 있습니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_INSTALL_PERMISSION) {
      //....
    }
}

앱 삭제 요청하기

PackageManager에 어떤 앱을 삭제해달라고 요청하면 팝업이 뜨고, 사용자가 허락을 하면 앱은 삭제됩니다.

앱이 삭제를 요청하려면 먼저 다음과 같은 퍼미션을 갖고 있어야 합니다. 설치 권한과는 다르게 삭제 권한은 AndroidManifest.xml에 등록만 하면 앱이 설치될 때 권한을 받습니다.

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

그리고 다음과 같은 코드로 어떤 패키지에 대해 삭제를 요청합니다.

val packageURI = Uri.parse("package:com.komorebi.memo")  // 1
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI)  // 2
startActivity(uninstallIntent)  // 3
  1. "package:[삭제하길 원하는 패키지 이름]" 형식으로 Uri를 만들어야 합니다.
  2. 인텐트의 Action은 "ACTION_DELETE"를 사용합니다.
  3. 인텐트를 실행하면 삭제 요청 화면이 뜹니다.

위의 코드가 실행되면 다음과 같은 화면이 뜨고 사용자가 OK 버튼을 누르면 앱이 삭제됩니다. uninstall package

정리

앱에서 apk를 설치하거나 삭제하는 방법에 대해서 알아보았습니다. 플랫폼 API를 직접 사용할 수 없기 때문에 PackageInstaller라는 앱을 통해 삭제 및 설치를 할 수 있습니다. PackageInstaller를 통해 설치하는 이유는, 사용자 몰래 악성 앱이 설치되거나, 앱이 삭제되는 것을 방지하기 위해서 입니다. 따라서, 어떤 앱을 설치하고 싶다면 사용자에게 이 앱이 설치되는 이유를 충분히 납득할 수 있도록 UX를 구성해야 합니다.

이 글에서 만든 샘플 앱은 GitHub에서 확인할 수 있습니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha