Split APKs¶
ackpine-splits
artifact contains utilities for working with split APK files.
Reading zipped splits¶
ZippedApkSplits
class contains factory methods for lazy sequences of APK splits which are contained inside of a zipped file such as ZIP, APKS, APKM and XAPK.
val splits: CloseableSequence<Apk> = ZippedApkSplits.getApksForUri(zippedFileUri, context)
val splitsList = splits.toList()
CloseableSequence<Apk> splits = ZippedApkSplits.getApksForUri(zippedFileUri, context);
List<Apk> splitsList = new ArrayList<>();
for (var iterator = splits.iterator(); iterator.hasNext(); ) {
var apk = iterator.next();
splitsList.add(apk);
}
Attention
Iteration of these sequences is blocking due to I/O operations. Don't iterate them on UI thread!
As you can see, these sequences have a type of CloseableSequence
which implements AutoCloseable
. That means they can close all held resources, and effectively be cancelled externally at any moment with a close()
call.
Apk
has the following properties:
val uri: Uri
val name: String
val size: Long
val packageName: String
val versionCode: Long
val description: String
Note
If your application doesn't have direct access to files (via READ_EXTERNAL_STORAGE
permission), parsing and iteration of the sequences may be much slower or even fail on Android versions lower than 8.0 Oreo, because Ackpine may fall back to using ZipInputStream
for these operations.
Apk
has the following types: Base
for base APK, Feature
for a feature split, Libs
for an APK split containing native libraries, ScreenDensity
for an APK split containing graphic resources tailored to specific screen density, Localization
for an APK split containing localized resources and Other
for an unknown APK split. They also have their specific properties. Refer to API documentation for details.
Working with splits¶
Sequences transformations¶
ApkSplits
class contains utilities for transforming Apk
sequences. In Kotlin they appear as extensions of Sequence<Apk>
.
For sequences of APKs, the following operations are available:
validate()
operation validates a split package and throwsSplitPackageException
if it's not valid when the sequence is iterated, while also closing all opened I/O resources. This operation cooperates with cancellation if upstream sequence supports it.
Example:
val splits: CloseableSequence<Apk> = ZippedApkSplits
.getApksForUri(zippedFileUri, context)
.validate()
val splitsList = try {
splits.toList()
} catch (exception: SplitPackageException) {
println(exception)
emptyList()
}
CloseableSequence<Apk> splits = ZippedApkSplits.getApksForUri(zippedFileUri, context);
CloseableSequence<Apk> validatedSplits = ApkSplits.validate(splits);
List<Apk> splitsList = new ArrayList<>();
try {
for (var iterator = validatedSplits.iterator(); iterator.hasNext(); ) {
var apk = iterator.next();
splitsList.add(apk);
}
} catch (SplitPackageException exception) {
System.out.println(exception);
splitsList = Collections.emptyList();
}
SplitPackage
API¶
For manipulating split packages, you can use SplitPackage
API.
ackpine-splits-ktx
module contains Kotlin-idiomatic extensions which are used in the examples below.
First, you create a SplitPackage.Provider
from a sequence of APKs, e.g. obtained from ZippedApkSplits
:
val splits = sequence.toSplitPackage()
var splits = SplitPackage.from(sequence);
Then you can apply different operations to it, and when you're done, materialize it into a SplitPackage
object:
val sortedSplits = splits.sortedByCompatibility(context)
val splitPackage = sortedSplits.get() // <- suspending, cancellable
// print SplitPackage entries for libs APKs
println(splitPackage.libs)
var sortedSplits = splits.sortedByCompatibility(context);
var splitPackageFuture = sortedSplits.getAsync(); // cancellable
// using Guava
Futures.addCallback(splitPackageFuture, new FutureCallback<>() {
@Override
public void onSuccess(@NonNull SplitPackage splitPackage) {
// print SplitPackage entries for libs APKs
System.out.println(splitPackage.getLibs());
}
@Override
public void onFailure(@NonNull Throwable t) {
}
}, MoreExecutors.directExecutor());
The get()
is cancellable if split package source supports cancellation (such as CloseableSequence
).
libs
in the previous example is a List<SplitPackage.Entry<Apk.Libs>>
.
Each entry in APK lists inside of SplitPackage
(such as libs
, localization
etc.) has isPreferred
and apk
properties:
isPreferred
— indicates whether the APK is the most preferred for the device among all splits of the same type. By default it istrue
. When an operation which checks compatibility is applied, this flag is updated accordingly;apk
—Apk
object.
SplitPackage
can be flattened to a plain list of entries by calling toList()
. Also you can filter out all entries where isPreferred=false
with filterPreferred()
:
val splitsList = splitPackage.toList()
val compatibleSplits = splitPackage.filterPreferred()
var splitsList = splitPackage.toList();
var compatibleSplits = splitPackage.filterPreferred();
List of available SplitPackage.Provider
operations:
-
sortedByCompatibility(Context)
operation returns a provider that gives out APK splits sorted according to their compatibility with the device. The most preferred APK splits will appear first. If exact device's screen density, ABI or locale doesn't appear in the splits, nearest matching split is chosen as a preferred one. -
filterCompatible(Context)
operation filters out the splits which are not the most preferred for the device. It acts the same as applyingsortedByCompatibility(context)
to the provider and callingfilterPreferred()
for the resultingSplitPackage
.
Full example of a pipeline:
val splits = ZippedApkSplits.getApksForUri(zippedFileUri, context)
.validate()
.toSplitPackage()
.sortedByCompatibility(context)
val sortedSplits = try {
splits.get() // <- suspending, cancellable
} catch (exception: SplitPackageException) {
println(exception)
SplitPackage.empty().get()
}
println(sortedSplits.libs) // prints SplitPackage entries for libs APKs,
// ordered by their compatibility with the device
val splitsToInstall = sortedSplits.filterPreferred()
sortedSplits
.toList()
.filterNot { entry -> entry.isPreferred }
.map { entry -> entry.apk }
.forEach(::println) // prints incompatible APKs
Sequence<Apk> zippedApkSplits = ZippedApkSplits.getApksForUri(uri, context);
Sequence<Apk> validatedSplits = ApkSplits.validate(zippedApkSplits);
SplitPackage.Provider splits = SplitPackage
.from(validatedSplits)
.sortedByCompatibility(context);
// using Guava
Futures.addCallback(splits.getAsync(), new FutureCallback<>() {
@Override
public void onSuccess(@NonNull SplitPackage sortedSplits) {
// prints SplitPackage entries for libs APKs,
// ordered by their compatibility with the device
System.out.println(sortedSplits.getLibs());
var splitsToInstall = sortedSplits.filterPreferred();
for (var entry : sortedSplits.toList()) {
if (!entry.isPreferred()) {
System.out.println(entry.getApk()); // prints incompatible APKs
}
}
}
@Override
public void onFailure(@NonNull Throwable exception) {
if (exception instanceof SplitPackageException) {
System.out.println(exception);
}
}
}, MoreExecutors.directExecutor());
Creating APK splits from separate files¶
You can parse an APK file from a File
or Uri
using static Apk
factories:
val apkFromFile: Apk? = Apk.fromFile(file, context)
val apkFromUri: Apk? = Apk.fromUri(uri, context, cancellationSignal)
Apk apkFromFile = Apk.fromFile(file, context);
Apk apkFromUri = Apk.fromUri(uri, context, cancellationSignal);