From fec3a03739d3b289d2b21d3d9caba7aeeea9541c Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 19 Dec 2024 23:46:12 +0500 Subject: [PATCH 01/22] extract ZipEntryStream and related code from ZippedFileProvider and use it for faster parsing in Apk.fromUri() --- .../ru/solrudev/ackpine/ZippedFileProvider.kt | 111 +------------- .../helpers => io}/NonClosingInputStream.kt | 4 +- .../ru/solrudev/ackpine/io/ToByteBuffer.kt | 28 ++++ .../ru/solrudev/ackpine/io/ZipEntryStream.kt | 143 ++++++++++++++++++ .../kotlin/ru/solrudev/ackpine/splits/Apk.kt | 27 ++-- .../splits/parsing/AndroidManifestParser.kt | 25 +-- 6 files changed, 197 insertions(+), 141 deletions(-) rename ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/{splits/helpers => io}/NonClosingInputStream.kt (95%) create mode 100644 ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ToByteBuffer.kt create mode 100644 ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt index b313450df..5c6ee4c34 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt @@ -26,28 +26,22 @@ 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.closeWithException -import ru.solrudev.ackpine.helpers.entries -import ru.solrudev.ackpine.helpers.toFile +import ru.solrudev.ackpine.io.ZipEntryStream 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.IOException import java.io.InputStream import java.io.OutputStream import java.util.concurrent.Executor -import java.util.zip.ZipFile -import java.util.zip.ZipInputStream /** * [ContentProvider] which allows to open files inside of ZIP archives. @@ -201,71 +195,9 @@ public class ZippedFileProvider : ContentProvider() { private fun openZipEntryStream(uri: Uri, signal: CancellationSignal?): ZipEntryStream { val zipFileUri = zipFileUri(uri) - val file = context?.let { context -> zipFileUri.toFile(context, signal) } - if (file?.canRead() == true) { - val zipFile = ZipFile(file) - return try { - val zipEntry = zipFile.getEntry(uri.encodedQuery) - ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile) - } catch (throwable: Throwable) { - zipFile.closeWithException(throwable) - throw throwable - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return openZipEntryStreamApi26(zipFileUri, uri.encodedQuery, signal) - } - return openZipEntryStreamUsingZipInputStream(zipFileUri, uri.encodedQuery, signal) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun openZipEntryStreamApi26( - zipFileUri: Uri, - zipEntryName: String?, - signal: CancellationSignal? - ): ZipEntryStream { - var fd: ParcelFileDescriptor? = null - var fileInputStream: FileInputStream? = null - val zipFile: org.apache.commons.compress.archivers.zip.ZipFile - try { - fd = context?.contentResolver?.openFileDescriptor(zipFileUri, "r", signal) - ?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri") - fileInputStream = FileInputStream(fd.fileDescriptor) - zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder() - .setSeekableByteChannel(fileInputStream.channel) - .get() - } catch (throwable: Throwable) { - fd?.closeWithException(throwable) - fileInputStream?.closeWithException(throwable) - throwable.printStackTrace() - return openZipEntryStreamUsingZipInputStream(zipFileUri, zipEntryName, signal) - } - try { - val zipEntry = zipFile.getEntry(zipEntryName) - return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd) - } catch (throwable: Throwable) { - fd.closeWithException(throwable) - fileInputStream.closeWithException(throwable) - zipFile.closeWithException(throwable) - throw throwable - } - } - - 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 == zipEntryName } - } catch (throwable: Throwable) { - zipStream.closeWithException(throwable) - throw throwable - } - return ZipEntryStream(zipStream, zipEntry.size) + val zipEntryName = uri.encodedQuery + return ZipEntryStream.open(zipFileUri, zipEntryName.orEmpty(), context!!, signal) + ?: throw IOException("Zip entry $zipEntryName not found at $uri") } private fun zipFileUri(uri: Uri): Uri { @@ -371,37 +303,4 @@ private object ZippedFileProviderPlugin : AckpinePlugin { override fun setExecutor(executor: Executor) { this.executor = executor } -} - -private class ZipEntryStream( - private val inputStream: InputStream, - val size: Long, - private vararg var resources: AutoCloseable -) : InputStream() { - - override fun read(): Int = inputStream.read() - override fun available(): Int = inputStream.available() - override fun markSupported(): Boolean = inputStream.markSupported() - override fun mark(readlimit: Int) = inputStream.mark(readlimit) - override fun read(b: ByteArray?): Int = inputStream.read(b) - override fun read(b: ByteArray?, off: Int, len: Int): Int = inputStream.read(b, off, len) - override fun reset() = inputStream.reset() - override fun skip(n: Long): Long = inputStream.skip(n) - - override fun close() { - for (resource in resources) { - runCatching { resource.close() } - } - resources = emptyArray() - inputStream.close() - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ZipEntryStream) return false - return inputStream == other.inputStream - } - - override fun hashCode(): Int = inputStream.hashCode() - override fun toString(): String = inputStream.toString() } \ No newline at end of file diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/NonClosingInputStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/NonClosingInputStream.kt similarity index 95% rename from ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/NonClosingInputStream.kt rename to ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/NonClosingInputStream.kt index 114304902..55fbf88c1 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/NonClosingInputStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/NonClosingInputStream.kt @@ -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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package ru.solrudev.ackpine.splits.helpers +package ru.solrudev.ackpine.io import androidx.annotation.RestrictTo import java.io.InputStream diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ToByteBuffer.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ToByteBuffer.kt new file mode 100644 index 000000000..ba4a3606f --- /dev/null +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ToByteBuffer.kt @@ -0,0 +1,28 @@ +/* + * 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.io + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.nio.ByteBuffer + +@JvmSynthetic +internal fun InputStream.toByteBuffer(): ByteBuffer { + val buffer = ByteArrayOutputStream() + copyTo(buffer) + return ByteBuffer.wrap(buffer.toByteArray()) +} \ No newline at end of file diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt new file mode 100644 index 000000000..92020d821 --- /dev/null +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -0,0 +1,143 @@ +/* + * 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.io + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import androidx.annotation.RequiresApi +import ru.solrudev.ackpine.helpers.closeWithException +import ru.solrudev.ackpine.helpers.entries +import ru.solrudev.ackpine.helpers.toFile +import java.io.FileInputStream +import java.io.InputStream +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream + +internal class ZipEntryStream private constructor( + private val inputStream: InputStream, + val size: Long, + private vararg var resources: AutoCloseable +) : InputStream() { + + override fun read(): Int = inputStream.read() + override fun available(): Int = inputStream.available() + override fun markSupported(): Boolean = inputStream.markSupported() + override fun mark(readlimit: Int) = inputStream.mark(readlimit) + override fun read(b: ByteArray?): Int = inputStream.read(b) + override fun read(b: ByteArray?, off: Int, len: Int): Int = inputStream.read(b, off, len) + override fun reset() = inputStream.reset() + override fun skip(n: Long): Long = inputStream.skip(n) + + override fun close() { + for (resource in resources) { + runCatching { resource.close() } + } + resources = emptyArray() + inputStream.close() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ZipEntryStream) return false + return inputStream == other.inputStream + } + + override fun hashCode(): Int = inputStream.hashCode() + override fun toString(): String = inputStream.toString() + + internal companion object { + + @JvmSynthetic + internal fun open( + uri: Uri, + zipEntryName: String, + context: Context, + signal: CancellationSignal? + ): ZipEntryStream? { + val file = uri.toFile(context, signal) + if (file.canRead()) { + val zipFile = ZipFile(file) + return try { + val zipEntry = zipFile.getEntry(uri.encodedQuery) ?: return null + ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile) + } catch (throwable: Throwable) { + zipFile.closeWithException(throwable) + throw throwable + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return openZipEntryStreamApi26(uri, zipEntryName, context, signal) + } + return openZipEntryStreamUsingZipInputStream(uri, zipEntryName, context, signal) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun openZipEntryStreamApi26( + zipFileUri: Uri, + zipEntryName: String, + context: Context, + signal: CancellationSignal? + ): ZipEntryStream? { + var fd: ParcelFileDescriptor? = null + var fileInputStream: FileInputStream? = null + val zipFile: org.apache.commons.compress.archivers.zip.ZipFile + try { + fd = context.contentResolver.openFileDescriptor(zipFileUri, "r", signal) + ?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri") + fileInputStream = FileInputStream(fd.fileDescriptor) + zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder() + .setSeekableByteChannel(fileInputStream.channel) + .get() + } catch (throwable: Throwable) { + fd?.closeWithException(throwable) + fileInputStream?.closeWithException(throwable) + throwable.printStackTrace() + return openZipEntryStreamUsingZipInputStream(zipFileUri, zipEntryName, context, signal) + } + try { + val zipEntry = zipFile.getEntry(zipEntryName) ?: return null + return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd) + } catch (throwable: Throwable) { + fd.closeWithException(throwable) + fileInputStream.closeWithException(throwable) + zipFile.closeWithException(throwable) + throw throwable + } + } + + private fun openZipEntryStreamUsingZipInputStream( + zipFileUri: Uri, + zipEntryName: String, + context: Context, + signal: CancellationSignal? + ): ZipEntryStream? { + val zipStream = ZipInputStream(context.contentResolver.openInputStream(zipFileUri)) + val zipEntry = try { + zipStream.entries() + .onEach { signal?.throwIfCanceled() } + .firstOrNull { it.name == zipEntryName } ?: return null + } catch (throwable: Throwable) { + zipStream.closeWithException(throwable) + throw throwable + } + return ZipEntryStream(zipStream, zipEntry.size) + } + } +} \ No newline at end of file diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt index d9969d96d..02d3bc427 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt @@ -21,15 +21,18 @@ import android.net.Uri import androidx.core.content.FileProvider import ru.solrudev.ackpine.AckpineFileProvider import ru.solrudev.ackpine.ZippedFileProvider +import ru.solrudev.ackpine.helpers.entries import ru.solrudev.ackpine.helpers.toFile +import ru.solrudev.ackpine.io.NonClosingInputStream.Companion.nonClosing +import ru.solrudev.ackpine.io.ZipEntryStream +import ru.solrudev.ackpine.io.toByteBuffer import ru.solrudev.ackpine.splits.Dpi.Companion.dpi -import ru.solrudev.ackpine.splits.helpers.NonClosingInputStream.Companion.nonClosing import ru.solrudev.ackpine.splits.helpers.deviceLocales import ru.solrudev.ackpine.splits.helpers.displayNameAndSize import ru.solrudev.ackpine.splits.helpers.isApk import ru.solrudev.ackpine.splits.helpers.localeFromSplitName +import ru.solrudev.ackpine.splits.parsing.ANDROID_MANIFEST_FILE_NAME import ru.solrudev.ackpine.splits.parsing.AndroidManifest -import ru.solrudev.ackpine.splits.parsing.androidManifest import java.io.File import java.io.InputStream import java.nio.ByteBuffer @@ -271,12 +274,12 @@ public sealed class Apk( } val (displayName, size) = uri.displayNameAndSize(context) val name = displayName.substringAfterLast('/').substringBeforeLast('.') - context.contentResolver.openInputStream(uri).use { inputStream -> - inputStream ?: return null - val androidManifest = ZipInputStream(inputStream.nonClosing()).use { it.androidManifest() } - ?: return null - return createApkSplit(androidManifest, name, uri, size) - } + ZipEntryStream + .open(uri, ANDROID_MANIFEST_FILE_NAME, context, signal = null) + .use { entryStream -> + entryStream ?: return null + return createApkSplit(entryStream.toByteBuffer(), name, uri, size) + } } @JvmSynthetic @@ -286,7 +289,10 @@ public sealed class Apk( } val uri = ZippedFileProvider.getUriForZipEntry(zipPath, zipEntry.name) val name = zipEntry.name.substringAfterLast('/').substringBeforeLast('.') - val androidManifest = ZipInputStream(inputStream.nonClosing()).use { it.androidManifest() } ?: return null + val androidManifest = ZipInputStream(inputStream.nonClosing()).use { zipInputStream -> + zipInputStream.entries().firstOrNull { it.name == ANDROID_MANIFEST_FILE_NAME } ?: return null + zipInputStream.toByteBuffer() + } return createApkSplit(androidManifest, name, uri, zipEntry.size) } @@ -296,7 +302,8 @@ public sealed class Apk( } val name = file.name.substringAfterLast('/').substringBeforeLast('.') ZipFile(file).use { zipFile -> - val androidManifest = zipFile.androidManifest() ?: return null + val zipEntry = zipFile.getEntry(ANDROID_MANIFEST_FILE_NAME) ?: return null + val androidManifest = zipFile.getInputStream(zipEntry).use(InputStream::toByteBuffer) return createApkSplit(androidManifest, name, uri, file.length()) } } diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/parsing/AndroidManifestParser.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/parsing/AndroidManifestParser.kt index b7df626ee..b4d0be683 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/parsing/AndroidManifestParser.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/parsing/AndroidManifestParser.kt @@ -17,13 +17,10 @@ package ru.solrudev.ackpine.splits.parsing import com.android.apksig.internal.apk.AndroidBinXmlParser -import ru.solrudev.ackpine.helpers.entries -import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -import java.util.zip.ZipFile -import java.util.zip.ZipInputStream -private const val ANDROID_MANIFEST_FILE_NAME = "AndroidManifest.xml" +@get:JvmSynthetic +internal const val ANDROID_MANIFEST_FILE_NAME = "AndroidManifest.xml" @JvmSynthetic internal fun AndroidManifest(androidManifest: ByteBuffer): AndroidManifest? { @@ -56,22 +53,4 @@ internal fun AndroidManifest(androidManifest: ByteBuffer): AndroidManifest? { return null } return AndroidManifest(manifest) -} - -@JvmSynthetic -internal fun ZipInputStream.androidManifest(): ByteBuffer? { - entries().firstOrNull { it.name == ANDROID_MANIFEST_FILE_NAME } ?: return null - val buffer = ByteArrayOutputStream() - copyTo(buffer) - return ByteBuffer.wrap(buffer.toByteArray()) -} - -@JvmSynthetic -internal fun ZipFile.androidManifest(): ByteBuffer? { - val androidManifestZipEntry = getEntry(ANDROID_MANIFEST_FILE_NAME) ?: return null - getInputStream(androidManifestZipEntry).use { entryStream -> - val buffer = ByteArrayOutputStream() - entryStream.copyTo(buffer) - return ByteBuffer.wrap(buffer.toByteArray()) - } } \ No newline at end of file From 95f6df62bd87080f4032b1f4c92161c414040711 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 19 Dec 2024 23:49:56 +0500 Subject: [PATCH 02/22] move createApkSplit() call out of use blocks --- .../main/kotlin/ru/solrudev/ackpine/splits/Apk.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt index 02d3bc427..84e74f68b 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt @@ -274,12 +274,10 @@ public sealed class Apk( } val (displayName, size) = uri.displayNameAndSize(context) val name = displayName.substringAfterLast('/').substringBeforeLast('.') - ZipEntryStream + val androidManifest = ZipEntryStream .open(uri, ANDROID_MANIFEST_FILE_NAME, context, signal = null) - .use { entryStream -> - entryStream ?: return null - return createApkSplit(entryStream.toByteBuffer(), name, uri, size) - } + ?.use(InputStream::toByteBuffer) ?: return null + return createApkSplit(androidManifest, name, uri, size) } @JvmSynthetic @@ -301,11 +299,11 @@ public sealed class Apk( return null } val name = file.name.substringAfterLast('/').substringBeforeLast('.') - ZipFile(file).use { zipFile -> + val androidManifest = ZipFile(file).use { zipFile -> val zipEntry = zipFile.getEntry(ANDROID_MANIFEST_FILE_NAME) ?: return null - val androidManifest = zipFile.getInputStream(zipEntry).use(InputStream::toByteBuffer) - return createApkSplit(androidManifest, name, uri, file.length()) + zipFile.getInputStream(zipEntry).toByteBuffer() } + return createApkSplit(androidManifest, name, uri, file.length()) } private fun createApkSplit(manifestByteBuffer: ByteBuffer, name: String, uri: Uri, size: Long): Apk? { From 79583a75db07ace9fd69acc698efcff6fa63271f Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 20 Dec 2024 00:15:21 +0500 Subject: [PATCH 03/22] declare readResolve() for empty ResolvableString --- .../kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 3de15217b..875577c4f 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -162,6 +162,7 @@ public sealed interface ResolvableString : Serializable { private data object Empty : ResolvableString { private const val serialVersionUID = 5194188194930148316L override fun resolve(context: Context): String = "" + private fun readResolve(): Any = Empty } private data class Raw(val value: String) : ResolvableString { From b89780bcd3e815ee4b0762c61b1758a93ddd4465 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 20 Dec 2024 18:01:10 +0500 Subject: [PATCH 04/22] use less specific type --- .../src/main/kotlin/ru/solrudev/ackpine/splits/ApkSplits.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ApkSplits.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ApkSplits.kt index ba2caf7db..135de0870 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ApkSplits.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ApkSplits.kt @@ -240,7 +240,7 @@ public object ApkSplits { ?.let { yield(ApkCompatibility(isPreferred = true, it)) } private suspend inline fun SequenceScope.yieldRemaining( - groupedSplits: MutableCollection> + groupedSplits: Collection> ) where T : Apk.ConfigSplit, T : Apk { for (apk in groupedSplits.flatten()) { yield(ApkCompatibility(isPreferred = false, apk)) From 48adee586787a908a81cb4cc4acd1fc3078e2fc5 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Sat, 21 Dec 2024 12:25:57 +0500 Subject: [PATCH 05/22] fix using wrong zip entry name when opening using ZipFile in ZipEntryStream.open extract openZipEntryStreamUsingZipFile() --- .../ru/solrudev/ackpine/io/ZipEntryStream.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt index 92020d821..b58f94bd5 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi import ru.solrudev.ackpine.helpers.closeWithException import ru.solrudev.ackpine.helpers.entries import ru.solrudev.ackpine.helpers.toFile +import java.io.File import java.io.FileInputStream import java.io.InputStream import java.util.zip.ZipFile @@ -72,20 +73,22 @@ internal class ZipEntryStream private constructor( signal: CancellationSignal? ): ZipEntryStream? { val file = uri.toFile(context, signal) - if (file.canRead()) { - val zipFile = ZipFile(file) - return try { - val zipEntry = zipFile.getEntry(uri.encodedQuery) ?: return null - ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile) - } catch (throwable: Throwable) { - zipFile.closeWithException(throwable) - throw throwable - } + return when { + file.canRead() -> openZipEntryStreamUsingZipFile(file, zipEntryName) + Build.VERSION.SDK_INT >= 26 -> openZipEntryStreamApi26(uri, zipEntryName, context, signal) + else -> openZipEntryStreamUsingZipInputStream(uri, zipEntryName, context, signal) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return openZipEntryStreamApi26(uri, zipEntryName, context, signal) + } + + private fun openZipEntryStreamUsingZipFile(file: File, zipEntryName: String): ZipEntryStream? { + val zipFile = ZipFile(file) + try { + val zipEntry = zipFile.getEntry(zipEntryName) ?: return null + return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile) + } catch (throwable: Throwable) { + zipFile.closeWithException(throwable) + throw throwable } - return openZipEntryStreamUsingZipInputStream(uri, zipEntryName, context, signal) } @RequiresApi(Build.VERSION_CODES.O) From e4d0a8eab7228075bb7995d7ca2dff5b80a3ff7f Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Sat, 21 Dec 2024 12:27:11 +0500 Subject: [PATCH 06/22] remove unnecessary api level checks --- .../kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt index 51fd2173d..736c6e74a 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt @@ -19,7 +19,6 @@ package ru.solrudev.ackpine.helpers import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.os.Build import android.os.CancellationSignal import android.os.Environment import android.os.Process @@ -33,11 +32,7 @@ internal fun Uri.toFile(context: Context, signal: CancellationSignal? = null): F return File(requireNotNull(path) { "Uri path is null: $this" }) } try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - context.contentResolver.openFileDescriptor(this, "r", signal) - } else { - context.contentResolver.openFileDescriptor(this, "r") - }.use { fileDescriptor -> + context.contentResolver.openFileDescriptor(this, "r", signal).use { fileDescriptor -> if (fileDescriptor == null) { throw NullPointerException("ParcelFileDescriptor was null: $this") } @@ -60,7 +55,7 @@ internal fun Uri.toFile(context: Context, signal: CancellationSignal? = null): F } private fun tryFileFromExternalDocumentUri(context: Context, uri: Uri): File? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || !DocumentsContract.isDocumentUri(context, uri)) { + if (!DocumentsContract.isDocumentUri(context, uri)) { return null } if (uri.authority != "com.android.externalstorage.documents") { From 161eb4f478c64ee0246b4a031d767d26b9509c3d Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Sat, 21 Dec 2024 12:30:01 +0500 Subject: [PATCH 07/22] change Uri.toFile() signature --- .../ru/solrudev/ackpine/helpers/UriHelpers.kt | 14 +++++++------- .../ru/solrudev/ackpine/io/ZipEntryStream.kt | 4 ++-- .../main/kotlin/ru/solrudev/ackpine/splits/Apk.kt | 4 ++-- .../ru/solrudev/ackpine/splits/ZippedApkSplits.kt | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt index 736c6e74a..990550271 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/helpers/UriHelpers.kt @@ -27,14 +27,14 @@ import java.io.File import java.io.FileNotFoundException @JvmSynthetic -internal fun Uri.toFile(context: Context, signal: CancellationSignal? = null): File { - if (scheme == ContentResolver.SCHEME_FILE) { - return File(requireNotNull(path) { "Uri path is null: $this" }) +internal fun Context.getFileFromUri(uri: Uri, signal: CancellationSignal? = null): File { + if (uri.scheme == ContentResolver.SCHEME_FILE) { + return File(requireNotNull(uri.path) { "Uri path is null: $uri" }) } try { - context.contentResolver.openFileDescriptor(this, "r", signal).use { fileDescriptor -> + contentResolver.openFileDescriptor(uri, "r", signal).use { fileDescriptor -> if (fileDescriptor == null) { - throw NullPointerException("ParcelFileDescriptor was null: $this") + throw NullPointerException("ParcelFileDescriptor was null: $uri") } val path = "/proc/${Process.myPid()}/fd/${fileDescriptor.fd}" val canonicalPath = File(path).canonicalPath.let { canonicalPath -> @@ -45,7 +45,7 @@ internal fun Uri.toFile(context: Context, signal: CancellationSignal? = null): F } } if (canonicalPath == path) { - return tryFileFromExternalDocumentUri(context, this) ?: File("") + return tryGetFileFromExternalDocumentUri(this, uri) ?: File("") } return File(canonicalPath) } @@ -54,7 +54,7 @@ internal fun Uri.toFile(context: Context, signal: CancellationSignal? = null): F } } -private fun tryFileFromExternalDocumentUri(context: Context, uri: Uri): File? { +private fun tryGetFileFromExternalDocumentUri(context: Context, uri: Uri): File? { if (!DocumentsContract.isDocumentUri(context, uri)) { return null } diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt index b58f94bd5..662cadff1 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -24,7 +24,7 @@ import android.os.ParcelFileDescriptor import androidx.annotation.RequiresApi import ru.solrudev.ackpine.helpers.closeWithException import ru.solrudev.ackpine.helpers.entries -import ru.solrudev.ackpine.helpers.toFile +import ru.solrudev.ackpine.helpers.getFileFromUri import java.io.File import java.io.FileInputStream import java.io.InputStream @@ -72,7 +72,7 @@ internal class ZipEntryStream private constructor( context: Context, signal: CancellationSignal? ): ZipEntryStream? { - val file = uri.toFile(context, signal) + val file = context.getFileFromUri(uri, signal) return when { file.canRead() -> openZipEntryStreamUsingZipFile(file, zipEntryName) Build.VERSION.SDK_INT >= 26 -> openZipEntryStreamApi26(uri, zipEntryName, context, signal) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt index 84e74f68b..293d096b1 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt @@ -22,7 +22,7 @@ import androidx.core.content.FileProvider import ru.solrudev.ackpine.AckpineFileProvider import ru.solrudev.ackpine.ZippedFileProvider import ru.solrudev.ackpine.helpers.entries -import ru.solrudev.ackpine.helpers.toFile +import ru.solrudev.ackpine.helpers.getFileFromUri import ru.solrudev.ackpine.io.NonClosingInputStream.Companion.nonClosing import ru.solrudev.ackpine.io.ZipEntryStream import ru.solrudev.ackpine.io.toByteBuffer @@ -265,7 +265,7 @@ public sealed class Apk( */ @JvmStatic public fun fromUri(uri: Uri, context: Context): Apk? { - val file = uri.toFile(context) + val file = context.getFileFromUri(uri) if (file.canRead()) { return fromFile(file, uri) } diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt index df8234797..60d176c4c 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt @@ -23,7 +23,7 @@ import android.os.ParcelFileDescriptor import androidx.annotation.RequiresApi import ru.solrudev.ackpine.helpers.closeWithException import ru.solrudev.ackpine.helpers.entries -import ru.solrudev.ackpine.helpers.toFile +import ru.solrudev.ackpine.helpers.getFileFromUri import java.io.File import java.io.FileInputStream import java.util.zip.ZipFile @@ -60,7 +60,7 @@ public object ZippedApkSplits { public fun getApksForUri(uri: Uri, context: Context): Sequence { val applicationContext = context.applicationContext // avoid capturing context into closure return closeableSequence { - val file = uri.toFile(applicationContext) + val file = applicationContext.getFileFromUri(uri) when { file.canRead() -> yieldAllUsingFile(file) Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> yieldAllApi26(applicationContext, uri) From a660cb1ba4d8dd67931d68e832109dd1b017c61c Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Mon, 23 Dec 2024 16:46:01 +0500 Subject: [PATCH 08/22] naming --- .../ru/solrudev/ackpine/io/ZipEntryStream.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt index 662cadff1..1527b36ec 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -74,13 +74,13 @@ internal class ZipEntryStream private constructor( ): ZipEntryStream? { val file = context.getFileFromUri(uri, signal) return when { - file.canRead() -> openZipEntryStreamUsingZipFile(file, zipEntryName) - Build.VERSION.SDK_INT >= 26 -> openZipEntryStreamApi26(uri, zipEntryName, context, signal) - else -> openZipEntryStreamUsingZipInputStream(uri, zipEntryName, context, signal) + file.canRead() -> openUsingZipFile(file, zipEntryName) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> openApi26(uri, zipEntryName, context, signal) + else -> openUsingZipInputStream(uri, zipEntryName, context, signal) } } - private fun openZipEntryStreamUsingZipFile(file: File, zipEntryName: String): ZipEntryStream? { + private fun openUsingZipFile(file: File, zipEntryName: String): ZipEntryStream? { val zipFile = ZipFile(file) try { val zipEntry = zipFile.getEntry(zipEntryName) ?: return null @@ -92,8 +92,8 @@ internal class ZipEntryStream private constructor( } @RequiresApi(Build.VERSION_CODES.O) - private fun openZipEntryStreamApi26( - zipFileUri: Uri, + private fun openApi26( + uri: Uri, zipEntryName: String, context: Context, signal: CancellationSignal? @@ -102,8 +102,8 @@ internal class ZipEntryStream private constructor( var fileInputStream: FileInputStream? = null val zipFile: org.apache.commons.compress.archivers.zip.ZipFile try { - fd = context.contentResolver.openFileDescriptor(zipFileUri, "r", signal) - ?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri") + fd = context.contentResolver.openFileDescriptor(uri, "r", signal) + ?: throw NullPointerException("ParcelFileDescriptor was null: $uri") fileInputStream = FileInputStream(fd.fileDescriptor) zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder() .setSeekableByteChannel(fileInputStream.channel) @@ -112,7 +112,7 @@ internal class ZipEntryStream private constructor( fd?.closeWithException(throwable) fileInputStream?.closeWithException(throwable) throwable.printStackTrace() - return openZipEntryStreamUsingZipInputStream(zipFileUri, zipEntryName, context, signal) + return openUsingZipInputStream(uri, zipEntryName, context, signal) } try { val zipEntry = zipFile.getEntry(zipEntryName) ?: return null @@ -125,13 +125,13 @@ internal class ZipEntryStream private constructor( } } - private fun openZipEntryStreamUsingZipInputStream( - zipFileUri: Uri, + private fun openUsingZipInputStream( + uri: Uri, zipEntryName: String, context: Context, signal: CancellationSignal? ): ZipEntryStream? { - val zipStream = ZipInputStream(context.contentResolver.openInputStream(zipFileUri)) + val zipStream = ZipInputStream(context.contentResolver.openInputStream(uri)) val zipEntry = try { zipStream.entries() .onEach { signal?.throwIfCanceled() } From 7807d871b20e0be8bf851783ba784420b72af8ca Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Mon, 23 Dec 2024 18:01:17 +0500 Subject: [PATCH 09/22] close resources if zip entry was not found --- .../ru/solrudev/ackpine/io/ZipEntryStream.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt index 1527b36ec..5e7ca94f6 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -83,7 +83,11 @@ internal class ZipEntryStream private constructor( private fun openUsingZipFile(file: File, zipEntryName: String): ZipEntryStream? { val zipFile = ZipFile(file) try { - val zipEntry = zipFile.getEntry(zipEntryName) ?: return null + val zipEntry = zipFile.getEntry(zipEntryName) + if (zipEntry == null) { + zipFile.close() + return null + } return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile) } catch (throwable: Throwable) { zipFile.closeWithException(throwable) @@ -115,7 +119,13 @@ internal class ZipEntryStream private constructor( return openUsingZipInputStream(uri, zipEntryName, context, signal) } try { - val zipEntry = zipFile.getEntry(zipEntryName) ?: return null + val zipEntry = zipFile.getEntry(zipEntryName) + if (zipEntry == null) { + fd.close() + fileInputStream.close() + zipFile.close() + return null + } return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd) } catch (throwable: Throwable) { fd.closeWithException(throwable) @@ -135,11 +145,15 @@ internal class ZipEntryStream private constructor( val zipEntry = try { zipStream.entries() .onEach { signal?.throwIfCanceled() } - .firstOrNull { it.name == zipEntryName } ?: return null + .firstOrNull { it.name == zipEntryName } } catch (throwable: Throwable) { zipStream.closeWithException(throwable) throw throwable } + if (zipEntry == null) { + zipStream.close() + return null + } return ZipEntryStream(zipStream, zipEntry.size) } } From 9c072d08cca077b571da9d7ef20c513ee6a202d6 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 10:30:29 +0500 Subject: [PATCH 10/22] add ZippedFileProvider.getUriForZipEntry() overloads for Uri and File --- ackpine-splits/api/ackpine-splits.api | 4 ++++ .../ru/solrudev/ackpine/ZippedFileProvider.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ackpine-splits/api/ackpine-splits.api b/ackpine-splits/api/ackpine-splits.api index 6c9f524f1..623b8f7e5 100644 --- a/ackpine-splits/api/ackpine-splits.api +++ b/ackpine-splits/api/ackpine-splits.api @@ -4,6 +4,8 @@ public final class ru/solrudev/ackpine/ZippedFileProvider : android/content/Cont public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I public fun getType (Landroid/net/Uri;)Ljava/lang/String; + public static final fun getUriForZipEntry (Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri; + public static final fun getUriForZipEntry (Ljava/io/File;Ljava/lang/String;)Landroid/net/Uri; public static final fun getUriForZipEntry (Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri; public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri; public static final fun isZippedFileProviderUri (Landroid/net/Uri;)Z @@ -21,6 +23,8 @@ public final class ru/solrudev/ackpine/ZippedFileProvider : android/content/Cont } public final class ru/solrudev/ackpine/ZippedFileProvider$Companion { + public final fun getUriForZipEntry (Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri; + public final fun getUriForZipEntry (Ljava/io/File;Ljava/lang/String;)Landroid/net/Uri; public final fun getUriForZipEntry (Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri; public final fun isZippedFileProviderUri (Landroid/net/Uri;)Z } diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt index 5c6ee4c34..ab5a872aa 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/ZippedFileProvider.kt @@ -292,6 +292,28 @@ public class ZippedFileProvider : ContentProvider() { .encodedQuery(zipEntryName) .build() } + + /** + * Creates an [Uri] for a ZIP entry. + * + * @param file a ZIP file containing [zip entry][zipEntryName]. + * @param zipEntryName name of the ZIP entry inside of the ZIP archive. + */ + @JvmStatic + public fun getUriForZipEntry(file: File, zipEntryName: String): Uri { + return getUriForZipEntry(file.absolutePath, zipEntryName) + } + + /** + * Creates an [Uri] for a ZIP entry. + * + * @param uri [Uri] pointing to a ZIP file containing [zip entry][zipEntryName]. + * @param zipEntryName name of the ZIP entry inside of the ZIP archive. + */ + @JvmStatic + public fun getUriForZipEntry(uri: Uri, zipEntryName: String): Uri { + return getUriForZipEntry(uri.toString(), zipEntryName) + } } } From 59f49f9797f70c43ea09719e6742ba477943514b Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 11:09:17 +0500 Subject: [PATCH 11/22] remove unnecessary try-catch --- .../ackpine/splits/ZippedApkSplits.kt | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt index 60d176c4c..fa11e885a 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/ZippedApkSplits.kt @@ -104,23 +104,17 @@ public object ZippedApkSplits { yieldAllUsingZipInputStream(context, uri) return } - try { - zipFile.entries - .asSequence() - .filterNot { isClosed } - .mapNotNull { zipEntry -> - zipFile.getInputStream(zipEntry).use { entryStream -> - entryStream.use() - Apk.fromZipEntry(uri.toString(), zipEntry, entryStream) - } + zipFile.entries + .asSequence() + .filterNot { isClosed } + .mapNotNull { zipEntry -> + zipFile.getInputStream(zipEntry).use { entryStream -> + entryStream.use() + Apk.fromZipEntry(uri.toString(), zipEntry, entryStream) } - .forEach { yield(it) } - } catch (throwable: Throwable) { - fd.closeWithException(throwable) - fileInputStream.closeWithException(throwable) - zipFile.closeWithException(throwable) - throw throwable - } + } + .forEach { yield(it) } + } private suspend inline fun CloseableSequenceScope.yieldAllUsingZipInputStream(context: Context, uri: Uri) { From b653404e5a2e98a763ae897032fcca4d68cf253d Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 12:30:51 +0500 Subject: [PATCH 12/22] make signal an optional parameter --- .../src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt | 2 +- .../src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt index 5e7ca94f6..c760e5e0b 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/io/ZipEntryStream.kt @@ -70,7 +70,7 @@ internal class ZipEntryStream private constructor( uri: Uri, zipEntryName: String, context: Context, - signal: CancellationSignal? + signal: CancellationSignal? = null ): ZipEntryStream? { val file = context.getFileFromUri(uri, signal) return when { diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt index 293d096b1..033538951 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/Apk.kt @@ -275,7 +275,7 @@ public sealed class Apk( val (displayName, size) = uri.displayNameAndSize(context) val name = displayName.substringAfterLast('/').substringBeforeLast('.') val androidManifest = ZipEntryStream - .open(uri, ANDROID_MANIFEST_FILE_NAME, context, signal = null) + .open(uri, ANDROID_MANIFEST_FILE_NAME, context) ?.use(InputStream::toByteBuffer) ?: return null return createApkSplit(androidManifest, name, uri, size) } From 0212a0837d08e4eface4b3c0886d43ad10cefec3 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 14:48:34 +0500 Subject: [PATCH 13/22] update agp and apksig to 8.7.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 123432926..afb09bbc4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -android-gradleplugin = "8.6.1" +android-gradleplugin = "8.7.3" kotlin = "2.0.21" kotlin-ksp = "2.0.21-1.0.26" From de61415c1b18db2052867810bef5581426bfae77 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 14:48:46 +0500 Subject: [PATCH 14/22] update ksp to 2.0.21-1.0.28 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index afb09bbc4..2304e9884 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] android-gradleplugin = "8.7.3" kotlin = "2.0.21" -kotlin-ksp = "2.0.21-1.0.26" +kotlin-ksp = "2.0.21-1.0.28" [libraries] materialcomponents = { module = "com.google.android.material:material", version = "1.12.0" } From e92d5e3841160986b18ebada6e79e8bda2ec3644 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 14:51:04 +0500 Subject: [PATCH 15/22] update binary-compatibility-validator to 0.17.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2304e9884..e6eee652d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ plugin-gradleMavenPublish = { module = "com.vanniktech:gradle-maven-publish-plug plugin-agp = { module = "com.android.tools.build:gradle", version.ref = "android-gradleplugin" } plugin-kotlin-android = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "2.0.0" } -plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version = "0.16.3" } +plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version = "0.17.0" } apache-commons-compress = { module = "org.apache.commons:commons-compress", version = "1.27.1" } apksig = { module = "com.android.tools.build:apksig", version.ref = "android-gradleplugin" } listenablefuture = { module = "com.google.guava:listenablefuture", version = "1.0" } From 70ccaaf1ad01d1d8769ba0679951617c63b7d090 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 14:51:20 +0500 Subject: [PATCH 16/22] update guava to 33.4.0-android --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6eee652d..abd5e5df6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-c apache-commons-compress = { module = "org.apache.commons:commons-compress", version = "1.27.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.3.1-android" } +guava = { module = "com.google.guava:guava", version = "33.4.0-android" } viewbindingpropertydelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version = "1.5.9" } okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" } From c057c3c7d893a2d6be501e09dc5070a8df8122f7 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 14:51:35 +0500 Subject: [PATCH 17/22] update androidx.navigation to 2.8.5 --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index cd20c08af..c26a7953e 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,6 +1,6 @@ [versions] lifecycle = "2.8.7" -navigation = "2.8.3" +navigation = "2.8.5" room = "2.6.1" concurrent = "1.2.0" From d275a22e656bc6ea679dd37127b35029c095ea36 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 15:33:37 +0500 Subject: [PATCH 18/22] update compileSdk and targetSdk to 35 update build tools to 35.0.0 --- .../src/main/kotlin/ru/solrudev/ackpine/gradle/Constants.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/Constants.kt b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/Constants.kt index a626e302c..5f91495d0 100644 --- a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/Constants.kt +++ b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/Constants.kt @@ -22,13 +22,13 @@ public object Constants { public const val PACKAGE_NAME: String = "ru.solrudev.ackpine" public const val JDK_VERSION: Int = 17 public const val MIN_SDK: Int = 16 - public const val COMPILE_SDK: Int = 34 - public const val BUILD_TOOLS_VERSION: String = "34.0.0" + public const val COMPILE_SDK: Int = 35 + public const val BUILD_TOOLS_VERSION: String = "35.0.0" } public object SampleConstants { public const val PACKAGE_NAME: String = "ru.solrudev.ackpine.sample" public const val MIN_SDK: Int = 21 - public const val TARGET_SDK: Int = 34 + public const val TARGET_SDK: Int = 35 public val JAVA_VERSION: JavaVersion = JavaVersion.VERSION_17 } \ No newline at end of file From 38defbd5c293268ecb5807d337023dd17f2d7eb8 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 17:02:16 +0500 Subject: [PATCH 19/22] handle insets in sample apps --- gradle/libs.versions.toml | 1 + sample-api34/build.gradle.kts | 1 + .../ackpine/sample/updater/MainActivity.kt | 16 +++++++++++ sample-java/build.gradle.kts | 1 + .../solrudev/ackpine/sample/MainActivity.java | 22 +++++++++++++++ .../sample/install/InstallFragment.java | 4 ++- sample-ktx/build.gradle.kts | 1 + .../solrudev/ackpine/sample/MainActivity.kt | 28 +++++++++++++++++++ .../ackpine/sample/install/InstallFragment.kt | 6 +++- 9 files changed, 78 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abd5e5df6..a2e6521b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ listenablefuture = { module = "com.google.guava:listenablefuture", version = "1. guava = { module = "com.google.guava:guava", version = "33.4.0-android" } viewbindingpropertydelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version = "1.5.9" } okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" } +insetter = { module = "dev.chrisbanes.insetter:insetter", version = "0.6.1" } [plugins] android-application = { id = "com.android.application", version.ref = "android-gradleplugin" } diff --git a/sample-api34/build.gradle.kts b/sample-api34/build.gradle.kts index ba3cb983b..5068648a9 100644 --- a/sample-api34/build.gradle.kts +++ b/sample-api34/build.gradle.kts @@ -38,4 +38,5 @@ dependencies { implementation(libs.materialcomponents) implementation(libs.okhttp) implementation(libs.viewbindingpropertydelegate) + implementation(libs.insetter) } \ No newline at end of file diff --git a/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt b/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt index 34682b348..d38cfff80 100644 --- a/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt +++ b/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt @@ -18,12 +18,14 @@ package ru.solrudev.ackpine.sample.updater import android.animation.LayoutTransition import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding +import dev.chrisbanes.insetter.applyInsetter import kotlinx.coroutines.launch import ru.solrudev.ackpine.AssetFileProvider import ru.solrudev.ackpine.resources.ResolvableString @@ -38,6 +40,8 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) + enableEdgeToEdge() + applyInsets() setSupportActionBar(binding.toolbarMain) with(binding.cardMainInstall) { imageViewInstallIcon.setImageURI( @@ -60,6 +64,18 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { } } + private fun applyInsets() = binding.containerMain.applyInsetter { + type(statusBars = true) { + padding() + } + type(navigationBars = true) { + padding(horizontal = true) + } + type(displayCutout = true) { + padding(horizontal = true, top = true) + } + } + private fun setProgress(progressData: Progress) = with(binding.cardMainInstall) { val progress = progressData.progress val max = progressData.max diff --git a/sample-java/build.gradle.kts b/sample-java/build.gradle.kts index 583bf4ea5..61053b668 100644 --- a/sample-java/build.gradle.kts +++ b/sample-java/build.gradle.kts @@ -35,4 +35,5 @@ dependencies { implementation(androidx.swiperefreshlayout) implementation(libs.materialcomponents) implementation(libs.guava) + implementation(libs.insetter) } \ No newline at end of file diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/MainActivity.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/MainActivity.java index 916af74e2..757828bab 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/MainActivity.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/MainActivity.java @@ -21,14 +21,18 @@ import android.content.Intent; import android.os.Bundle; +import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowInsetsCompat; import androidx.navigation.NavController; import androidx.navigation.NavOptions; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; +import dev.chrisbanes.insetter.Insetter; +import dev.chrisbanes.insetter.Side; import ru.solrudev.ackpine.sample.databinding.NavHostBinding; import ru.solrudev.ackpine.sample.install.InstallFragment; @@ -44,6 +48,8 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = NavHostBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + EdgeToEdge.enable(this); + applyInsets(); final NavController navController = getNavController(); NavigationUI.setupWithNavController(binding.toolbarNavHost, navController, appBarConfiguration); NavigationUI.setupWithNavController(binding.bottomNavigationViewNavHost, navController); @@ -58,6 +64,22 @@ protected void onNewIntent(@NonNull Intent intent) { maybeHandleInstallUri(intent); } + private void applyInsets() { + Insetter.builder() + .padding(WindowInsetsCompat.Type.statusBars()) + .padding(WindowInsetsCompat.Type.navigationBars(), Side.LEFT | Side.RIGHT) + .padding(WindowInsetsCompat.Type.displayCutout(), Side.LEFT | Side.RIGHT | Side.TOP) + .applyToView(binding.appBarLayoutNavHost); + Insetter.builder() + .padding(WindowInsetsCompat.Type.navigationBars(), Side.LEFT | Side.RIGHT) + .padding(WindowInsetsCompat.Type.displayCutout(), Side.LEFT | Side.RIGHT) + .applyToView(binding.contentNavHost); + Insetter.builder() + .padding(WindowInsetsCompat.Type.navigationBars(), Side.LEFT | Side.RIGHT | Side.BOTTOM) + .padding(WindowInsetsCompat.Type.displayCutout(), Side.LEFT | Side.RIGHT | Side.BOTTOM) + .applyToView(binding.bottomNavigationViewNavHost); + } + private void maybeHandleInstallUri(@NonNull Intent intent) { final var uri = intent.getData(); if (ACTION_VIEW.equals(intent.getAction()) && uri != null) { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java index 463d68dd5..19f6edf21 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java @@ -144,7 +144,9 @@ private void resetUriToInstall() { private void observeViewModel() { viewModel.getError().observe(getViewLifecycleOwner(), error -> { if (!error.isEmpty()) { - Snackbar.make(requireView(), error.resolve(requireContext()), Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.content_nav_host), + error.resolve(requireContext()), + Snackbar.LENGTH_LONG) .setAnchorView(binding.fabInstall) .show(); viewModel.clearError(); diff --git a/sample-ktx/build.gradle.kts b/sample-ktx/build.gradle.kts index d4d4b32cb..b92073c10 100644 --- a/sample-ktx/build.gradle.kts +++ b/sample-ktx/build.gradle.kts @@ -35,4 +35,5 @@ dependencies { implementation(androidx.swiperefreshlayout) implementation(libs.materialcomponents) implementation(libs.viewbindingpropertydelegate) + implementation(libs.insetter) } \ No newline at end of file diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/MainActivity.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/MainActivity.kt index fdf33fd1e..d7167a9ff 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/MainActivity.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/MainActivity.kt @@ -19,6 +19,7 @@ package ru.solrudev.ackpine.sample import android.content.Intent import android.content.Intent.ACTION_VIEW import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.navigation.NavController @@ -27,6 +28,7 @@ import androidx.navigation.navOptions import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import by.kirich1409.viewbindingdelegate.viewBinding +import dev.chrisbanes.insetter.applyInsetter import ru.solrudev.ackpine.sample.databinding.NavHostBinding import ru.solrudev.ackpine.sample.install.InstallFragment @@ -41,6 +43,8 @@ class MainActivity : AppCompatActivity(R.layout.nav_host) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) + enableEdgeToEdge() + applyInsets() val navController = navController binding.toolbarNavHost.setupWithNavController(navController, appBarConfiguration) binding.bottomNavigationViewNavHost.setupWithNavController(navController) @@ -54,6 +58,30 @@ class MainActivity : AppCompatActivity(R.layout.nav_host) { maybeHandleInstallUri(intent) } + private fun applyInsets() = with(binding) { + appBarLayoutNavHost.applyInsetter { + type(statusBars = true) { + padding() + } + type(navigationBars = true) { + padding(horizontal = true) + } + type(displayCutout = true) { + padding(horizontal = true, top = true) + } + } + contentNavHost.applyInsetter { + type(navigationBars = true, displayCutout = true) { + padding(horizontal = true) + } + } + bottomNavigationViewNavHost.applyInsetter { + type(navigationBars = true, displayCutout = true) { + padding(horizontal = true, bottom = true) + } + } + } + private fun maybeHandleInstallUri(intent: Intent) { val uri = intent.data if (intent.action == ACTION_VIEW && uri != null) { diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt index 23944505e..d264f9b6a 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt @@ -105,7 +105,11 @@ class InstallFragment : Fragment(R.layout.fragment_install) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> if (!uiState.error.isEmpty) { - Snackbar.make(requireView(), uiState.error.resolve(requireContext()), Snackbar.LENGTH_LONG) + Snackbar.make( + requireActivity().findViewById(R.id.content_nav_host), + uiState.error.resolve(requireContext()), + Snackbar.LENGTH_LONG + ) .setAnchorView(binding.fabInstall) .show() viewModel.clearError() From 83e3f5a4c9ab4b925082d50117572121f83bb5cc Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 17:37:23 +0500 Subject: [PATCH 20/22] fix insets in sample-api34 --- .../ru/solrudev/ackpine/sample/updater/MainActivity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt b/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt index d38cfff80..f2888d6fc 100644 --- a/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt +++ b/sample-api34/src/main/kotlin/ru/solrudev/ackpine/sample/updater/MainActivity.kt @@ -65,15 +65,12 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { } private fun applyInsets() = binding.containerMain.applyInsetter { - type(statusBars = true) { + type(statusBars = true, displayCutout = true) { padding() } type(navigationBars = true) { padding(horizontal = true) } - type(displayCutout = true) { - padding(horizontal = true, top = true) - } } private fun setProgress(progressData: Progress) = with(binding.cardMainInstall) { From f55f4c58edca822dcea83aa388ae03ef20acbf12 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 24 Dec 2024 17:39:09 +0500 Subject: [PATCH 21/22] increment version --- README.md | 2 +- docs/index.md | 2 +- version.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 056c0f8b7..2842fd2fd 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google() ```kotlin dependencies { - val ackpineVersion = "0.9.2" + val ackpineVersion = "0.9.3" implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion") // optional - Kotlin extensions and Coroutines support diff --git a/docs/index.md b/docs/index.md index be1d55a9b..e8d17fb3c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google() ```kotlin dependencies { - val ackpineVersion = "0.9.2" + val ackpineVersion = "0.9.3" implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion") // optional - Kotlin extensions and Coroutines support diff --git a/version.properties b/version.properties index afffc4c58..ad7d74d6c 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ MAJOR_VERSION=0 MINOR_VERSION=9 -PATCH_VERSION=2 +PATCH_VERSION=3 SUFFIX= SNAPSHOT=false \ No newline at end of file From 03bb7209ffba564d6662d25176c6e23fbb50fb86 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 25 Dec 2024 17:42:41 +0500 Subject: [PATCH 22/22] add version 0.9.3 to changelog --- docs/changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index a557d029e..c9739ccc1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,30 @@ Change Log ========== +Version 0.9.3 (2024-12-25) +-------------------------- + +### Dependencies + +- Updated KSP to 2.0.21-1.0.28. +- Updated Android Gradle Plugin to 8.7.3. +- Updated `apksig` to 8.7.3. +- Updated `binary-compatibility-validator` to 0.17.0. +- Updated `androidx.navigation` to 2.8.5 (sample apps dependency). +- Updated Guava to 33.4.0-android (sample apps dependency). + +### Bug fixes and improvements + +- Raise `compileSdk` to 35. +- Use random access when parsing APK on API level 26+ in `Apk.fromUri()`. This greatly improves performance for large APKs. +- Add `ZippedFileProvider.getUriForZipEntry()` overloads for `File` and `Uri`. +- Raise `targetSdk` for sample apps to 35. +- Proper support for edge-to-edge display in sample apps. + +### Public API changes + +- Added `getUriForZipEntry(File, String)` and `getUriForZipEntry(Uri, String)` to `ZippedFileProvider.Companion` and as static `ZippedFileProvider` methods. + Version 0.9.2 (2024-12-19) --------------------------