Giter Club home page Giter Club logo

simpleinstaller's Introduction

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.

simpleinstaller's People

Contributors

solrudev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

twinscodeinc

simpleinstaller's Issues

Failed install MIUI

An error occurs when installing apk on Xiaomi devices. The installation process starts and after clicking "Install" an error occurs but there is no error description, the result returns "null"

installSplitPackage problems

I suspect this is more due to my inexperience with Kotlin with your code, but I've been able to get installPackage() to work, but not installSplitPackage(). I'm trying to take an array of File and convert them to an array of ApkSource, but even after I do that, I get a compiler error of: Type mismatch: inferred type is () -> Unit but SessionOptions was expected (located before the brace on the installSplitPackage() call).

val arr = mutableListOf<File>()
for (packageApk in packageApks) {
    val apkFile = FileUtils.getFile(packageApk)
    arr.add(apkFile)
}
var apks : Array<File> = arr.toTypedArray()
var apkSourceArray : Array<ApkSource> = apks.toApkSourceArray()

val result = PackageInstaller.installSplitPackage(*apkSourceArray) {
    notification {
        icon = R.drawable.ic_installer
    }
}
when (result) {
    is InstallResult.Success -> Log.i(TAG, getString(R.string.installed_successfully))
    is InstallResult.Failure -> {
        Log.i(TAG, result.toString())
        Log.i(TAG, getString(R.string.install_failed))
    }
}


Sample App

Can you please attach installable apk file of sample app here?

1st install works fine, subsequent installs fail

Installer.zip

I've attached an app that uses the file picker to pick apks from the Download directory. It recreates a problem that I'm having, namely that the first time I run installPackage() it works fine, and then fails to finish when run again. I've tried with both ConfirmationStrategy.DEFERRED and ConfirmationStrategy.IMMEDIATE. I have used the simpleinstaller app and that works, so I would appreciate any insights as to why my code doesn't.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.