자신의 앱에서 어떤 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"
처럼 번호로 표기한 것을 아래에 자세히 설명하였습니다.
- 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_paths
는 res/xml/file_paths.xml
파일을 가리키며 다음과 같이 정의하시면 됩니다.
이렇게 설정하면 내 앱의 데이터 폴더의 ../files/app.apk
파일을 다른 앱과 공유할 수 있습니다.
<paths>
<files-path path="/" name="app.apk" />
</paths>
위 코드를 실행시키면 다음 화면이 나옵니다. 이 화면은 세팅에 가서 앱에게 설치를 요청할 수 있는 권한을 줘야 한다는 것입니다. 보안적인 이유로, 앱이 apk설치를 요청하려면 사용자에게 허락을 맡아야 합니다. 한번만 설정하면 그 다음부터는 뜨지 않습니다.
Settings 버튼을 누르면 다음 화면이 나옵니다. Allow from this source
을 눌러 활성화 시키면, 이 앱은 apk를 설치 요청할 수 있는 권한을 부여받습니다.
다시 설치화면으로 돌아오면 다음과 같은 화면이 보입니다. 이제 Install
버튼을 누르면 설치가 됩니다.
퍼미션 세팅 화면 띄우기
앱 설치를 요청할 때 위에서 본 것처럼, 권한이 없으면 설정화면으로 이동하라는 안내 메시지가 뜹니다. 하지만 앱에서 미리 보여주고 권한을 받고 싶을 수도 있습니다.
다음 코드는 권한을 갖고 있는지 체크하고 없다면 세팅 화면을 띄우는 코드입니다.
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
}
- 권한이 있다면 canRequestPackageInstalls()는 true를 리턴합니다.
- 권한을 부여하는 세팅 화면에 대한 인텐트입니다.
- 실행하면 설정화면이 뜹니다.
세팅화면이 종료되면 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
- "package:[삭제하길 원하는 패키지 이름]" 형식으로 Uri를 만들어야 합니다.
- 인텐트의 Action은 "ACTION_DELETE"를 사용합니다.
- 인텐트를 실행하면 삭제 요청 화면이 뜹니다.
위의 코드가 실행되면 다음과 같은 화면이 뜨고 사용자가 OK 버튼을 누르면 앱이 삭제됩니다.
정리
앱에서 apk를 설치하거나 삭제하는 방법에 대해서 알아보았습니다. 플랫폼 API를 직접 사용할 수 없기 때문에 PackageInstaller라는 앱을 통해 삭제 및 설치를 할 수 있습니다. PackageInstaller를 통해 설치하는 이유는, 사용자 몰래 악성 앱이 설치되거나, 앱이 삭제되는 것을 방지하기 위해서 입니다. 따라서, 어떤 앱을 설치하고 싶다면 사용자에게 이 앱이 설치되는 이유를 충분히 납득할 수 있도록 UX를 구성해야 합니다.
이 글에서 만든 샘플 앱은 GitHub에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력