Skip to content

Commit

Permalink
Merge pull request #57 from solrudev/develop
Browse files Browse the repository at this point in the history
0.5.3
  • Loading branch information
solrudev authored Apr 25, 2024
2 parents 5dcf2ec + 2e9eec1 commit dd6ebb1
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google()

```kotlin
dependencies {
val ackpineVersion = "0.5.2"
val ackpineVersion = "0.5.3"
implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion")

// optional - Kotlin extensions and Coroutines support
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Ilya Fomichev
* Copyright (C) 2023-2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -209,7 +209,7 @@ public class InstallParameters private constructor(
/**
* Sets [InstallParameters.installerType], maintaining the following invariants:
* * When on API level < 21, [InstallerType.INTENT_BASED] is always set regardless of the provided value;
* * When on API level >= 21 and [apks] contain more than one entry, [InstallerType.SESSION_BASED] is always
* * When on API level >= 21 and [apks] contains more than one entry, [InstallerType.SESSION_BASED] is always
* set regardless of the provided value.
*/
public fun setInstallerType(installerType: InstallerType): Builder = apply {
Expand Down
1 change: 1 addition & 0 deletions ackpine-splits/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ dependencies {
api(androidx.annotation)
implementation(projects.ackpineRuntime)
implementation(androidx.core.ktx)
implementation(libs.apache.commons.compress)
implementation(libs.apksig)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Ilya Fomichev
* Copyright (C) 2023-2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,17 +26,20 @@ import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import ru.solrudev.ackpine.helpers.entries
import ru.solrudev.ackpine.helpers.toFile
import ru.solrudev.ackpine.plugin.AckpinePlugin
import ru.solrudev.ackpine.plugin.AckpinePluginRegistry
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.InputStream
Expand Down Expand Up @@ -202,23 +205,57 @@ public class ZippedFileProvider : ContentProvider() {
val zipFile = ZipFile(file)
return try {
val zipEntry = zipFile.getEntry(uri.encodedQuery)
ZipEntryStream(zipFile, zipFile.getInputStream(zipEntry), zipEntry.size)
ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile)
} catch (t: Throwable) {
zipFile.close()
throw t
}
}
// fall back to ZipInputStream if file is not readable directly
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return try {
openZipEntryStreamUsingFileChannel(zipFileUri, uri.encodedQuery)
} catch (exception: Exception) {
exception.printStackTrace()
openZipEntryStreamUsingZipInputStream(zipFileUri, uri.encodedQuery, signal)
}
}
return openZipEntryStreamUsingZipInputStream(zipFileUri, uri.encodedQuery, signal)
}

@RequiresApi(Build.VERSION_CODES.O)
private fun openZipEntryStreamUsingFileChannel(zipFileUri: Uri, zipEntryName: String?): ZipEntryStream {
val fd = context?.contentResolver?.openFileDescriptor(zipFileUri, "r")
?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri")
val fileInputStream = FileInputStream(fd.fileDescriptor)
val zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
return try {
val zipEntry = zipFile.getEntry(zipEntryName)
ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd)
} catch (t: Throwable) {
fd.close()
fileInputStream.close()
zipFile.close()
throw t
}
}

private fun openZipEntryStreamUsingZipInputStream(
zipFileUri: Uri,
zipEntryName: String?,
signal: CancellationSignal?
): ZipEntryStream {
val zipStream = ZipInputStream(context?.contentResolver?.openInputStream(zipFileUri))
val zipEntry = try {
zipStream.entries()
.onEach { signal?.throwIfCanceled() }
.first { it.name == uri.encodedQuery }
.first { it.name == zipEntryName }
} catch (t: Throwable) {
zipStream.close()
throw t
}
return ZipEntryStream(zipFile = null, zipStream, zipEntry.size)
return ZipEntryStream(zipStream, zipEntry.size)
}

private fun zipFileUri(uri: Uri): Uri {
Expand Down Expand Up @@ -327,9 +364,9 @@ private object ZippedFileProviderPlugin : AckpinePlugin {
}

private class ZipEntryStream(
private val zipFile: ZipFile?,
private val inputStream: InputStream,
val size: Long
val size: Long,
private vararg val resources: AutoCloseable
) : InputStream() {

override fun read(): Int = inputStream.read()
Expand All @@ -342,7 +379,9 @@ private class ZipEntryStream(
override fun skip(n: Long): Long = inputStream.skip(n)

override fun close() {
zipFile?.close()
for (resource in resources) {
resource.close()
}
inputStream.close()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Ilya Fomichev
* Copyright (C) 2023-2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,9 +18,12 @@ package ru.solrudev.ackpine.splits

import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import ru.solrudev.ackpine.helpers.entries
import ru.solrudev.ackpine.helpers.toFile
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream

Expand Down Expand Up @@ -69,12 +72,46 @@ public object ZippedApkSplits {
yieldAll(getApksForFile(file))
return@sequence
}
// fall back to ZipInputStream if file is not readable directly
ZipInputStream(applicationContext.contentResolver.openInputStream(uri)).use { zipStream ->
zipStream.entries()
.mapNotNull { zipEntry -> Apk.fromZipEntry(uri.toString(), zipEntry, zipStream) }
.forEach { yield(it) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
yieldAllUsingFileChannel(applicationContext, uri)
} catch (exception: Exception) {
exception.printStackTrace()
yieldAllUsingZipInputStream(applicationContext, uri)
}
return@sequence
}
yieldAllUsingZipInputStream(applicationContext, uri)
}.constrainOnce()
}

@RequiresApi(Build.VERSION_CODES.O)
private suspend inline fun SequenceScope<Apk>.yieldAllUsingFileChannel(applicationContext: Context, uri: Uri) {
applicationContext.contentResolver.openFileDescriptor(uri, "r").use { fd ->
fd ?: throw NullPointerException("ParcelFileDescriptor was null: $uri")
FileInputStream(fd.fileDescriptor).use { fileInputStream ->
org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
.use { zipFile ->
zipFile.entries
.asSequence()
.mapNotNull { zipEntry ->
zipFile.getInputStream(zipEntry).use { entryStream ->
Apk.fromZipEntry(uri.toString(), zipEntry, entryStream)
}
}
.forEach { yield(it) }
}
}
}
}

private suspend inline fun SequenceScope<Apk>.yieldAllUsingZipInputStream(applicationContext: Context, uri: Uri) {
ZipInputStream(applicationContext.contentResolver.openInputStream(uri)).use { zipStream ->
zipStream.entries()
.mapNotNull { zipEntry -> Apk.fromZipEntry(uri.toString(), zipEntry, zipStream) }
.forEach { yield(it) }
}
}
}
15 changes: 15 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
Change Log
==========

Version 0.5.3 (2024-04-25)
--------------------------

### Dependencies

- Updated Android Gradle Plugin to 8.3.2.
- Updated `apksig` to 8.3.2.
- Added Apache Commons Compress dependency to `ackpine-splits` module.

### Bug fixes and improvements

- Use `FileChannel` to read zipped APKs on Android Oreo+ if possible. This drastically improves performance when direct access through `java.io` APIs is not available and allows to process problematic ZIP files (such as XAPK files).
- Don't crash if exception occurs while iterating APK sequence in sample apps, and display the exception message instead.
- Share one thread pool across ViewModels in Java sample.

Version 0.5.2 (2024-03-30)
--------------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ Available for install sessions. Ackpine supports two different package installer
`InstallParameters` builder will maintain the following invariants when configuring the installer type:

- When on API level < 21, `INTENT_BASED` is always set regardless of the provided value;
- When on API level >= 21 and `InstallParameters.Builder.apks` contain more than one entry, `SESSION_BASED` is always set regardless of the provided value.
- When on API level >= 21 and `InstallParameters.Builder.apks` contains more than one entry, `SESSION_BASED` is always set regardless of the provided value.

By default, the value of installer type on API level < 21 is `INTENT_BASED`, and on API level >= 21 is `SESSION_BASED`.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google()

```kotlin
dependencies {
val ackpineVersion = "0.5.2"
val ackpineVersion = "0.5.3"
implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion")

// optional - Kotlin extensions and Coroutines support
Expand Down
2 changes: 1 addition & 1 deletion docs/split_apks.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ val description: String
```

!!! Note
If your application doesn't have direct access to files (via `MANAGE_EXTERNAL_STORAGE` or `READ_EXTERNAL_STORAGE` permissions), parsing and iteration of the sequences will be much slower, because Ackpine will fall back to using `ZipInputStream` for these operations.
If your application doesn't have direct access to files (via `MANAGE_EXTERNAL_STORAGE` or `READ_EXTERNAL_STORAGE` permissions), parsing and iteration of the sequences may be much slower, 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](api/ackpine-splits/index.html) for details.

Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
android-gradleplugin = "8.3.1"
android-gradleplugin = "8.3.2"
kotlin = "1.9.23"
kotlin-ksp = "1.9.23-1.0.19"

Expand All @@ -10,6 +10,7 @@ plugin-agp = { module = "com.android.tools.build:gradle", version.ref = "android
plugin-kotlin-android = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" }
plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version = "0.14.0" }
apache-commons-compress = { module = "org.apache.commons:commons-compress", version = "1.26.1" }
apksig = { module = "com.android.tools.build:apksig", version.ref = "android-gradleplugin" }
listenablefuture = { module = "com.google.guava:listenablefuture", version = "1.0" }
guava = { module = "com.google.guava:guava", version = "33.1.0-android" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 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.
*/

package ru.solrudev.ackpine.sample;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
public static ExecutorService INSTANCE = Executors.newFixedThreadPool(8);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Ilya Fomichev
* Copyright (C) 2023-2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,7 +38,6 @@
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import kotlin.sequences.Sequence;
import ru.solrudev.ackpine.DisposableSubscriptionContainer;
Expand All @@ -52,6 +51,7 @@
import ru.solrudev.ackpine.installer.PackageInstaller;
import ru.solrudev.ackpine.installer.parameters.InstallParameters;
import ru.solrudev.ackpine.sample.R;
import ru.solrudev.ackpine.sample.ThreadPool;
import ru.solrudev.ackpine.session.Failure;
import ru.solrudev.ackpine.session.ProgressSession;
import ru.solrudev.ackpine.session.Session;
Expand Down Expand Up @@ -181,6 +181,10 @@ private List<Uri> mapApkSequenceToUri(@NonNull Sequence<Apk> apks) {
e.getExpected(), e.getActual(), e.getName()));
}
return Collections.emptyList();
} catch (Exception exception) {
final var message = exception.getMessage() != null ? exception.getMessage() : "";
error.postValue(NotificationString.raw(message));
return Collections.emptyList();
}
}

Expand Down Expand Up @@ -221,7 +225,7 @@ public void onFailure(@NonNull UUID sessionId, @NonNull InstallFailure failure)
final var packageInstaller = PackageInstaller.getInstance(application);
final var savedStateHandle = createSavedStateHandle(creationExtras);
final var sessionsRepository = new SessionDataRepositoryImpl(savedStateHandle);
final var executor = Executors.newFixedThreadPool(8);
final var executor = ThreadPool.INSTANCE;
return new InstallViewModel(packageInstaller, sessionsRepository, executor);
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Ilya Fomichev
* Copyright (C) 2023-2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,9 +39,9 @@
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import ru.solrudev.ackpine.DisposableSubscriptionContainer;
import ru.solrudev.ackpine.sample.ThreadPool;
import ru.solrudev.ackpine.session.Failure;
import ru.solrudev.ackpine.session.Session;
import ru.solrudev.ackpine.session.parameters.Confirmation;
Expand Down Expand Up @@ -186,7 +186,7 @@ public void onFailure(@NonNull UUID sessionId, @NonNull UninstallFailure failure
assert application != null;
final var packageUninstaller = PackageUninstaller.getInstance(application);
final var savedStateHandle = createSavedStateHandle(creationExtras);
final var executor = Executors.newFixedThreadPool(8);
final var executor = ThreadPool.INSTANCE;
return new UninstallViewModel(packageUninstaller, savedStateHandle, executor);
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class InstallViewModel(
}
error.value = errorString
return emptyList()
} catch (exception: Exception) {
error.value = NotificationString.raw(exception.message.orEmpty())
return emptyList()
}
}

Expand Down
2 changes: 1 addition & 1 deletion version.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MAJOR_VERSION=0
MINOR_VERSION=5
PATCH_VERSION=2
PATCH_VERSION=3
SUFFIX=
SNAPSHOT=false

0 comments on commit dd6ebb1

Please sign in to comment.