diff --git a/CHANGELOG.md b/CHANGELOG.md index 590ccb2..574e6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# [1.4.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2023-11-27) + + +### Bug Fixes + +* Differentiate no package compatibility to any version compatibility ([762b7e3](https://github.com/ReVanced/revanced-library/commit/762b7e3bc01e2ca33dfcdbb1b5028d60ef6e0a48)) +* Sort the version maps by the most common version ([e4be6db](https://github.com/ReVanced/revanced-library/commit/e4be6dbccd86700ffafe7cd8395e845bbd3d5138)) + + +### Features + +* Allow getting most common compatible versions for all packages ([96845ba](https://github.com/ReVanced/revanced-library/commit/96845ba265e6dc208c7ac96f5e58734209cd1720)) + +# [1.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0-dev.1) (2023-11-27) + + +### Features + +* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](https://github.com/ReVanced/revanced-library/commit/c5f3536cbb6997766076595dc0b2b5d2e861ca73)) + # [1.3.0](https://github.com/ReVanced/revanced-library/compare/v1.2.0...v1.3.0) (2023-11-26) diff --git a/api/revanced-library.api b/api/revanced-library.api index 2d0cddb..e020e1e 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -63,6 +63,8 @@ public final class app/revanced/library/Options$Patch$Option { public final class app/revanced/library/PatchUtils { public static final field INSTANCE Lapp/revanced/library/PatchUtils; public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String; + public final fun getMostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map; + public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; } public abstract class app/revanced/library/adb/AdbManager { @@ -87,6 +89,7 @@ public final class app/revanced/library/adb/AdbManager$Companion { } public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception { + public fun ()V } public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception { diff --git a/gradle.properties b/gradle.properties index f621b40..fd63a01 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.3.0 +version = 1.4.0-dev.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55f19ed..8d21415 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ bcpkix-jdk18on = "1.76" jackson-module-kotlin = "2.14.3" jadb = "1.2.1" kotlin-reflect = "1.9.10" -kotlin-test = "1.9.10" +kotlin-test = "1.9.20" revanced-patcher = "19.0.0" binary-compatibility-validator = "0.13.2" diff --git a/src/main/kotlin/app/revanced/library/PatchUtils.kt b/src/main/kotlin/app/revanced/library/PatchUtils.kt index 716814c..1cc700b 100644 --- a/src/main/kotlin/app/revanced/library/PatchUtils.kt +++ b/src/main/kotlin/app/revanced/library/PatchUtils.kt @@ -1,6 +1,14 @@ package app.revanced.library import app.revanced.patcher.PatchSet +import app.revanced.patcher.patch.Patch + +typealias PackageName = String +typealias Version = String +typealias Count = Int + +typealias VersionMap = LinkedHashMap +typealias PackageNameMap = Map /** * Utility functions for working with patches. @@ -14,6 +22,13 @@ object PatchUtils { * @param packageName The name of the compatible package. * @return The most common version of. */ + @Deprecated( + "Use getMostCommonCompatibleVersions instead.", + ReplaceWith( + "getMostCommonCompatibleVersions(patches, setOf(packageName))" + + ".entries.firstOrNull()?.value?.keys?.firstOrNull()", + ), + ) fun getMostCommonCompatibleVersion( patches: PatchSet, packageName: String, @@ -28,4 +43,51 @@ object PatchUtils { .groupingBy { it } .eachCount() .maxByOrNull { it.value }?.key + + /** + * Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version. + * + * @param patches The set of patches to check. + * @param packageNames The names of the compatible packages to include. If null, all packages will be included. + * @param countUnusedPatches Whether to count patches that are not used. + * @return A map of package names to a map of versions to their count. + */ + fun getMostCommonCompatibleVersions( + patches: PatchSet, + packageNames: Set? = null, + countUnusedPatches: Boolean = false, + ): PackageNameMap = + buildMap { + fun filterWantedPackages(compatiblePackages: Iterable): Iterable { + val wantedPackages = packageNames?.toHashSet() ?: return compatiblePackages + return compatiblePackages.filter { it.name in wantedPackages } + } + + patches + .filter { it.use || countUnusedPatches } + .flatMap { it.compatiblePackages ?: emptyList() } + .let(::filterWantedPackages) + .forEach { compatiblePackage -> + if (compatiblePackage.versions?.isEmpty() == true) { + return@forEach + } + + val versionMap = getOrPut(compatiblePackage.name) { linkedMapOf() } + + compatiblePackage.versions?.let { versions -> + versions.forEach { version -> + versionMap[version] = versionMap.getOrDefault(version, 0) + 1 + } + } + } + + // Sort the version maps by the most common version. + forEach { (packageName, versionMap) -> + this[packageName] = + versionMap + .asIterable() + .sortedWith(compareByDescending { it.value }) + .associate { it.key to it.value } as VersionMap + } + } } diff --git a/src/main/kotlin/app/revanced/library/adb/Constants.kt b/src/main/kotlin/app/revanced/library/adb/Constants.kt index 94caabc..74641a0 100644 --- a/src/main/kotlin/app/revanced/library/adb/Constants.kt +++ b/src/main/kotlin/app/revanced/library/adb/Constants.kt @@ -21,21 +21,21 @@ internal object Constants { "chcon u:object_r:apk_data_file:s0 ${'$'}base_path" internal const val UMOUNT = - "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" + "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH" internal val MOUNT_SCRIPT = """ #!/system/bin/sh - MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin + MAGISKTMP="$( magisk --path )" || MAGISKTMP=/sbin MIRROR="${'$'}MAGISKTMP/.magisk/mirror" - until [ "${'$'}(getprop sys.boot_completed)" = 1 ]; do sleep 3; done + until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done until [ -d "/sdcard/Android" ]; do sleep 1; done base_path="$PATCHED_APK_PATH" - stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) + stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) chcon u:object_r:apk_data_file:s0 ${'$'}base_path mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path diff --git a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt index 9b8ada2..fc635cf 100644 --- a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt +++ b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt @@ -8,11 +8,106 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals internal object PatchUtilsTest { + private val patches = + arrayOf( + newPatch("some.package", setOf("a")), + newPatch("some.package", setOf("a", "b"), use = false), + newPatch("some.package", setOf("a", "b", "c"), use = false), + newPatch("some.other.package", setOf("b"), use = false), + newPatch("some.other.package", setOf("b", "c")), + newPatch("some.other.package", setOf("b", "c", "d")), + newPatch("some.other.other.package"), + newPatch("some.other.other.package", setOf("a")), + newPatch("some.other.other.package", setOf("b")), + newPatch("some.other.other.other.package", use = false), + newPatch("some.other.other.other.package", use = false), + ).toSet() + + @Test + fun `empty because package is incompatible with any version`() { + assertEqualsVersions( + expected = emptyMap(), + patches = setOf(newPatch("some.package", emptySet(), use = true)), + compatiblePackageNames = setOf("some.package"), + ) + } + + @Test + fun `empty list of versions because package is unconstrained to any version`() { + assertEqualsVersions( + expected = mapOf("some.package" to linkedMapOf()), + patches = setOf(newPatch("some.package")), + compatiblePackageNames = setOf("some.package"), + countUnusedPatches = true, + ) + } + + @Test + fun `empty because no known package was supplied`() { + assertEqualsVersions( + expected = emptyMap(), + patches, + compatiblePackageNames = setOf("unknown.package"), + ) + } + + @Test + fun `common versions correctly ordered for each package`() { + fun assertEqualsExpected(compatiblePackageNames: Set?) = + assertEqualsVersions( + expected = + mapOf( + "some.package" to linkedMapOf("a" to 3, "b" to 2, "c" to 1), + "some.other.package" to linkedMapOf("b" to 3, "c" to 2, "d" to 1), + "some.other.other.package" to linkedMapOf("a" to 1, "b" to 1), + "some.other.other.other.package" to linkedMapOf(), + ), + patches, + compatiblePackageNames, + countUnusedPatches = true, + ) + + assertEqualsExpected( + compatiblePackageNames = + setOf( + "some.package", + "some.other.package", + "some.other.other.package", + "some.other.other.other.package", + ), + ) + + assertEqualsExpected( + compatiblePackageNames = null, + ) + } + + @Test + fun `common versions correctly ordered for each package without counting unused patches`() { + assertEqualsVersions( + expected = + mapOf( + "some.package" to linkedMapOf("a" to 1), + "some.other.package" to linkedMapOf("b" to 2, "c" to 2, "d" to 1), + "some.other.other.package" to linkedMapOf("a" to 1, "b" to 1), + ), + patches, + compatiblePackageNames = + setOf( + "some.package", + "some.other.package", + "some.other.other.package", + "some.other.other.other.package", + ), + countUnusedPatches = false, + ) + } + @Test fun `return 'a' because it is the most common version`() { val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d") - .map { version -> newPatch("some.package", version) } + .map { version -> newPatch("some.package", setOf(version)) } .toSet() assertEqualsVersion("a", patches, "some.package") @@ -25,13 +120,13 @@ internal object PatchUtilsTest { @Test fun `return null because no patch is compatible with the supplied package name`() { - val patches = setOf(newPatch("some.package", "a")) + val patches = setOf(newPatch("some.package", setOf("a"))) assertEqualsVersion(null, patches, "other.package") } @Test - fun `return null because no patch compatible package is constrained to a version`() { + fun `return null because no compatible package is constrained to a version`() { val patches = setOf( newPatch("other.package"), @@ -41,15 +136,40 @@ internal object PatchUtilsTest { assertEqualsVersion(null, patches, "other.package") } + private fun assertEqualsVersions( + expected: PackageNameMap, + patches: PatchSet, + compatiblePackageNames: Set?, + countUnusedPatches: Boolean = false, + ) = assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersions(patches, compatiblePackageNames, countUnusedPatches), + ) + private fun assertEqualsVersion( expected: String?, patches: PatchSet, compatiblePackageName: String, - ) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName)) + ) { + // Test both the deprecated and the new method. + + @Suppress("DEPRECATION") + assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName), + ) + + assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersions(patches, setOf(compatiblePackageName)) + .entries.firstOrNull()?.value?.keys?.firstOrNull(), + ) + } private fun newPatch( packageName: String, - vararg versions: String, + versions: Set? = null, + use: Boolean = true, ) = object : BytecodePatch() { init { // Set the compatible packages field to the supplied package name and versions reflectively, @@ -57,9 +177,17 @@ internal object PatchUtilsTest { val compatiblePackagesField = Patch::class.java.getDeclaredField("compatiblePackages") compatiblePackagesField.isAccessible = true - compatiblePackagesField.set(this, setOf(CompatiblePackage(packageName, versions.toSet()))) + compatiblePackagesField.set(this, setOf(CompatiblePackage(packageName, versions?.toSet()))) + + val useField = Patch::class.java.getDeclaredField("use") + + useField.isAccessible = true + useField.set(this, use) } override fun execute(context: BytecodeContext) {} + + // Needed to make the patches unique. + override fun equals(other: Any?) = false } }