Skip to content

Getting Started

To obtain an instance of PackageInstaller or PackageUninstaller, use the getInstance(Context) method:

val packageInstaller = PackageInstaller.getInstance(context)
val packageUninstaller = PackageUninstaller.getInstance(context)
var packageInstaller = PackageInstaller.getInstance(context);
var packageUninstaller = PackageUninstaller.getInstance(context);

Simple session launch

Launching an install or uninstall session with default parameters and getting its result back is as easy as writing this:

try {
    when (val result = packageInstaller.createSession(apkUri).await()) {
        is SessionResult.Success -> println("Success")
        is SessionResult.Error -> println(result.cause.message)
    }
} catch (_: CancellationException) {
    println("Cancelled")
} catch (exception: Exception) {
    println(exception)
}

Session launches when await() is called.

var subscriptions = new DisposableSubscriptionContainer();
var session = packageInstaller.createSession(new InstallParameters.Builder(apkUri).build());
session.addStateListener(subscriptions, new Session.TerminalStateListener<>(session) {
    @Override
    public void onSuccess(@NonNull UUID sessionId) {
        System.out.println("Success");
    }

    @Override
    public void onFailure(@NonNull UUID sessionId, @NonNull InstallFailure failure) {
        if (failure instanceof Failure.Exceptional f) {
            System.out.println(f.getException());
        } else {
            System.out.println(failure.getCause().getMessage());
        }
    }

    @Override
    public void onCancelled(@NonNull UUID sessionId) {
        System.out.println("Cancelled");
    }
});

Session launches when TerminalStateListener is added to it.

It works as long as you don't care about UI lifecycle and unpredictable situations such as process death.

Handling UI lifecycle

If you're launching a session inside of a long-living service which is not expected to be killed (such as a foreground service), the previous example is good to go. However, when you are dealing with UI components such as Activities or Fragments, it's good practice to remove attached state listeners when appropriate:

When using ackpine-ktx artifact and calling Session.await(), the listener will be automatically detached when parent coroutine scope is cancelled. So if you're calling await() inside of a viewModelScope or lifecycleScope, it should be fine. Note that cancelling await() also cancels the session, this is done to respect coroutines' structured concurrency.

var subscriptions = new DisposableSubscriptionContainer();
var session = packageInstaller.createSession(...);
session.addStateListener(subscriptions, ...);

// when lifecycle is destroyed
subscriptions.clear();

Handling process death

Handling process death is not any different with Ackpine as with any other persisted state handling. You can save a session's ID and then re-retrieve the session from PackageInstaller:

savedStateHandle[SESSION_ID_KEY] = session.id

// after process restart
val id: UUID? = savedStateHandle[SESSION_ID_KEY]
if (id != null) {
    val result = packageInstaller.getSession(id)?.await()
    // or anything else you want to do with the session
}
savedStateHandle.set(SESSION_ID_KEY, session.getId());

// after process restart
UUID id = savedStateHandle.get(SESSION_ID_KEY);
if (id != null) {
    // using Guava
    Futures.addCallback(packageInstaller.getSessionAsync(id), new FutureCallback<>() {
        @Override
        public void onSuccess(@Nullable ProgressSession<InstallFailure> session) {
            if (session != null) {
                session.addStateListener(subscriptions, ...);
                // or anything else you want to do with the session
            }
        }

        @Override
        public void onFailure(@NonNull Throwable t) {
        }
    }, MoreExecutors.directExecutor());
}

Observing progress

Install sessions provide progress updates:

session.progress // Flow<Progress>
    .onEach { progress ->
        updateProgress(progress.progress, progress.max)
    }
    .launchIn(coroutineScope)
session.addProgressListener(subscriptions, (sessionId, progress) -> {
    updateProgress(progress.getProgress(), progress.getMax());
});

Error handling

Error causes are delivered as Failure objects through state listener or as a return value from await(). They're sealed hierarchies of typed errors, and you can match on their type. For example:

val failure = installError.cause
val error = when (failure) {
    is InstallFailure.Aborted -> "Aborted"
    is InstallFailure.Blocked -> "Blocked by ${failure.otherPackageName}"
    is InstallFailure.Conflict -> "Conflicting with ${failure.otherPackageName}"
    is InstallFailure.Exceptional -> failure.exception.message
    is InstallFailure.Generic -> "Generic failure"
    is InstallFailure.Incompatible -> "Incompatible"
    is InstallFailure.Invalid -> "Invalid"
    is InstallFailure.Storage -> "Storage path: ${failure.storagePath}"
    is InstallFailure.Timeout -> "Timeout"
}
var error = "";
if (failure instanceof InstallFailure.Aborted) {
    error = "Aborted";
} else if (failure instanceof InstallFailure.Blocked f) {
    error = "Blocked by " + f.getOtherPackageName();
} else if (failure instanceof InstallFailure.Conflict f) {
    error = "Conflicting with " + f.getOtherPackageName();
} else if (failure instanceof InstallFailure.Exceptional f) {
    error = f.getException().getMessage();
} else if (failure instanceof InstallFailure.Generic) {
    error = "Generic failure";
} else if (failure instanceof InstallFailure.Incompatible) {
    error = "Incompatible";
} else if (failure instanceof InstallFailure.Invalid) {
    error = "Invalid";
} else if (failure instanceof InstallFailure.Storage f) {
    error = "Storage path: " + f.getStoragePath();
} else if (failure instanceof InstallFailure.Timeout) {
    error = "Timeout";
}

When using await(), exceptions are never delivered as a Failure.Exceptional object. Instead, they are thrown.

Every example on this page is using PackageInstaller, but APIs for PackageUninstaller are absolutely the same except for progress updates.