Skip to content
This repository has been archived by the owner on Dec 5, 2023. It is now read-only.
/ SimpleInstaller Public archive

Easy to use Android package installer wrapper leveraging Kotlin coroutines (API 16+). Newer alternative: https://github.com/solrudev/Ackpine

License

Notifications You must be signed in to change notification settings

solrudev/SimpleInstaller

Repository files navigation

SimpleInstaller

Publish to MavenCentral Maven Central License

Deprecated!

SimpleInstaller is deprecated in favor of Ackpine, a robust package installer library with more rich API. SimpleInstaller won't receive updates anymore.

Contents

Overview

SimpleInstaller is an Android library which provides wrapper over Android packages install and uninstall functionality leveraging Kotlin coroutines.

It supports Android versions starting from API 16. Split packages installation is also supported (note that this is only available on Android versions starting from API 21).

SimpleInstaller was developed with deferred execution in mind. You can launch an install or uninstall session when user is not interacting with your app directly, for example, while foreground service is running and your application was removed from recents. The way it works is that the user is shown a high-priority notification which launches a standard Android confirmation by clicking on it.

Note: SimpleInstaller does not support process death scenario.

Gradle

All versions are available here.

implementation("io.github.solrudev:simpleinstaller:5.0.0")

Usage

Permissions

If your application relies on WRITE_EXTERNAL_STORAGE permission, change your permission's declaration in application's AndroidManifest.xml to this:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    tools:remove="android:maxSdkVersion" />

Here we are saying to manifest merger that we don't want to include maxSdkVersion attribute which is declared in SimpleInstaller's manifest. SimpleInstaller needs this permission only on API levels < 21.

If you're targeting API level 33, you also need to request POST_NOTIFICATIONS runtime permission, so SimpleInstaller can work with DEFERRED confirmation strategy.

Install permission

On Oreo and higher PackageInstaller sets an install reason PackageManager.INSTALL_REASON_USER, so on first install there should be a prompt from Android to allow installation. But relying on this is not recommended, because your process may be restarted if user chooses Always allow, so the result and progress won't be received anymore. There's InstallPermissionContract in activityresult package which you should use to request user to turn on install from unknown sources for your app.

In your Activity:

val requestInstallPermissionLauncher = registerForActivityResult(InstallPermissionContract()) { isGranted ->
    if (isGranted) { /* do something */ }
}

...

requestInstallPermissionLauncher.launch(Unit)
// or using `launch()` extension from androidx.activity
requestInstallPermissionLauncher.launch()

Installation

Installation functionality is provided by PackageInstaller interface.

SimpleInstaller has out-of-the-box support for Uri (URIs must have file: or content: scheme), AssetFileDescriptor and File. It is possible to subclass ApkSource and pass it to PackageInstaller.

Kotlin
// for split packages: packageInstaller.installSplitPackage(apk1, apk2, apk3) { ... }
val result = packageInstaller.installPackage(apk) {
    confirmationStrategy = ConfirmationStrategy.DEFERRED
    notification {
        title = "Notification title"
        contentText = "Notification text"
        icon = R.drawable.icon
    }
}
when (result) {
    InstallResult.Success -> println("Install succeeded.")
    is InstallResult.Failure -> println(result.cause)
}

Here DSL block is an extension on SessionOptions.Builder.

To obtain PackageInstaller instance in Kotlin one can just treat it as a singleton object because its companion object implements PackageInstaller interface. For example:

val result = PackageInstaller.installPackage(apk)
val packageInstallerInstance = PackageInstaller

You can get if PackageInstaller has an active session through a property:

val hasActiveSession: Boolean

Progress updates can be collected from progress SharedFlow property:

val progress: SharedFlow<ProgressData>
Java
// for split packages: packageInstaller.installSplitPackage(apkSourceArray, sessionOptions, callback)
UriApkSource apkSource = new UriApkSource(apkUri);
// you can provide your own SessionOptions instead of SessionOptions.DEFAULT
packageInstaller.installPackage(apkSource, SessionOptions.DEFAULT, new PackageInstaller.Callback() {
    @Override
    public void onSuccess() {}

    @Override
    public void onFailure(@Nullable InstallFailureCause cause) {}

    @Override
    public void onException(@NonNull Throwable exception) {}

    @Override
    public void onCanceled() {}

    @Override
    public void onProgressChanged(@NonNull ProgressData progress) {}
});

Callback interface methods are empty default methods, so you are not forced to always implement all of them.

As you may notice, callback instance is held on until session is completed, so don't reference any short-lived objects such as Views or Activities in callback implementation to prevent possible memory leaks. Usage directly from UI layer is hereby discouraged.

To obtain an instance of PackageInstaller use static getInstance() method:

PackageInstaller packageInstaller = PackageInstaller.getInstance();

You can get if PackageInstaller has an active session through a getter method:

public boolean getHasActiveSession();

Also it's possible to cancel current install session:

public void cancel();

ApkSource

SimpleInstaller provides an abstract ApkSource class with the following public interface:

val progress: SharedFlow<ProgressData>
open val file: File
abstract suspend fun getUri(): Uri
open fun openAssetFileDescriptor(signal: CancellationSignal): AssetFileDescriptor?
open fun clearTempFiles()

You can provide your own implementation and pass it to PackageInstaller's installPackage() or installSplitPackage().

Uninstallation

Uninstallation functionality is provided by PackageUninstaller interface.

Kotlin
val result = packageUninstaller.uninstallPackage(packageName) {
    confirmationStrategy = ConfirmationStrategy.DEFERRED
    notification {
        title = "Notification title"
        contentText = "Notification text"
        icon = R.drawable.icon
    }
}
if (result) {
    println("Uninstall succeeded.")
}

Here DSL block is an extension on SessionOptions.Builder.

To obtain PackageUninstaller instance in Kotlin one can just treat it as a singleton object because its companion object implements PackageUninstaller interface. For example:

val result = PackageUninstaller.uninstallPackage(packageName)
val packageUninstallerInstance = PackageUninstaller

You can get if PackageUninstaller has an active session through a property:

val hasActiveSession: Boolean
Java
// you can provide your own SessionOptions instead of SessionOptions.DEFAULT
packageUninstaller.uninstallPackage(packageName, SessionOptions.DEFAULT, new PackageUninstaller.Callback() {
    @Override
    public void onFinished(boolean success) {}

    @Override
    public void onException(@NonNull Throwable exception) {}

    @Override
    public void onCanceled() {}
});

Callback interface methods are empty default methods, so you are not forced to always implement all of them.

As you may notice, callback instance is held on until session is completed, so don't reference any short-lived objects such as Views or Activities in callback implementation to prevent possible memory leaks. Usage directly from UI layer is hereby discouraged.

To obtain an instance of PackageUninstaller use static getInstance() method:

PackageUninstaller packageUninstaller = PackageUninstaller.getInstance();

You can get if PackageUninstaller has an active session through a getter method:

public boolean getHasActiveSession();

Also it's possible to cancel current uninstall session:

public void cancel();

Customization options

Install or uninstall session may be customized with SessionOptions. It allows to set notification data and different strategies for handling user's confirmation.

Default value can be retrieved from SessionOptions.DEFAULT static field.

A new instance may be constructed in a following way:

Kotlin
val sessionOptions = SessionOptions {
    confirmationStrategy = ConfirmationStrategy.DEFERRED
    notificationData = notificationDataInstance
    // It's also possible to use `notification` DSL function.
}
Java
SessionOptions sessionOptions = new SessionOptions.Builder()
    .setConfirmationStrategy(ConfirmationStrategy.DEFERRED)
    .setNotificationData(notificationDataInstance)
    .build();

ConfirmationStrategy

A strategy for handling user's confirmation of installation or uninstallation. Can be DEFERRED (used by default) or IMMEDIATE.

DEFERRED (default) — user will be shown a high-priority notification which will launch confirmation activity.

IMMEDIATE — user will be prompted to confirm installation or uninstallation right away. Suitable for launching session directly from the UI when app is in foreground.

Notification

It is possible to provide notification title, text and icon.

Note: any options for notification will be ignored if ConfirmationStrategy is set to IMMEDIATE.

If title/text is empty (they are by default), default value is used when notification is displayed. android.R.drawable.ic_dialog_alert is used as a default icon.

Default value can be retrieved from NotificationData.DEFAULT static field.

A new instance may be constructed in a following way:

Kotlin
val notificationData = NotificationData {
    title = "Title"
    contentText = "Text"
    icon = R.drawable.icon
}
Java
NotificationData notificationData = new NotificationData.Builder()
    .setTitle("Title")
    .setContentText("Text")
    .setIcon(R.drawable.icon)
    .build();

Testing

PackageInstaller and PackageUninstaller are interfaces, so you can provide your own fake implementation for tests. For example, you could create an implementation of PackageInstaller which will always return InstallResult.Failure with InstallFailureCause.Storage cause:

class FailingPackageInstaller : PackageInstaller {

    override val hasActiveSession = false
    override val progress = MutableSharedFlow<ProgressData>()

    private val result = InstallResult.Failure(
        InstallFailureCause.Storage("Insufficient storage.")
    )

    override suspend fun installSplitPackage(vararg apkFiles: ApkSource, options: SessionOptions) = result

    override fun installSplitPackage(vararg apkFiles: ApkSource, options: SessionOptions, callback: PackageInstaller.Callback) {
        callback.onFailure(result.cause)
    }

    override suspend fun installPackage(apkFile: ApkSource, options: SessionOptions) = result

    override fun installPackage(apkFile: ApkSource, options: SessionOptions, callback: PackageInstaller.Callback) {
        callback.onFailure(result.cause)
    }

    override fun cancel() {}
}

Sample app

There's a simple sample app available. It can install chosen APK file and uninstall an application selected from the installed apps list. Go here to see sources.

License

Copyright © 2021-2023 Ilya Fomichev

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.