From 437a9f150840b4b9457e4db595144b89aeefdddb Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 19 May 2023 20:49:32 +0200 Subject: [PATCH] feat: integrate revanced patcher (#22) --- app/build.gradle.kts | 15 +- app/src/main/AndroidManifest.xml | 13 ++ .../revanced/manager/compose/MainActivity.kt | 23 +- .../manager/compose/ManagerApplication.kt | 7 +- .../manager/compose/di/RepositoryModule.kt | 2 + .../manager/compose/di/ViewModelModule.kt | 18 +- .../manager/compose/di/WorkerModule.kt | 9 + .../manager/compose/network/api/ManagerAPI.kt | 12 +- .../manager/compose/patcher/Aligning.kt | 38 ++++ .../manager/compose/patcher/Session.kt | 100 +++++++++ .../manager/compose/patcher/aapt/Aapt.kt | 13 ++ .../compose/patcher/alignment/ZipAlign.kt | 11 + .../patcher/alignment/zip/Extensions.kt | 33 +++ .../compose/patcher/alignment/zip/ZipFile.kt | 177 +++++++++++++++ .../alignment/zip/structures/ZipEndRecord.kt | 74 ++++++ .../alignment/zip/structures/ZipEntry.kt | 189 ++++++++++++++++ .../compose/patcher/data/PatchBundle.kt | 40 ++++ .../data/repository/PatchesRepository.kt | 43 ++++ .../compose/patcher/patch/PatchInfo.kt | 46 ++++ .../patcher/worker/PatcherProgressManager.kt | 140 ++++++++++++ .../compose/patcher/worker/PatcherWorker.kt | 79 +++++++ .../manager/compose/ui/component/PatchItem.kt | 47 ++++ .../compose/ui/destination/Destination.kt | 6 +- .../compose/ui/screen/AppSelectorScreen.kt | 29 ++- .../compose/ui/screen/DashboardScreen.kt | 1 - .../compose/ui/screen/InstallerScreen.kt | 50 +++++ .../ui/screen/PatchesSelectorScreen.kt | 212 +++++------------- .../ui/viewmodel/AppSelectorViewModel.kt | 18 ++ .../ui/viewmodel/InstallerScreenViewModel.kt | 109 +++++++++ .../ui/viewmodel/PatchesSelectorViewModel.kt | 116 ++++------ .../app/revanced/manager/compose/util/PM.kt | 91 ++------ app/src/main/jniLibs/arm64-v8a/libaapt2.so | Bin 0 -> 6179616 bytes app/src/main/jniLibs/armeabi-v7a/libaapt2.so | Bin 0 -> 5303144 bytes app/src/main/jniLibs/x86/libaapt2.so | Bin 0 -> 6805364 bytes app/src/main/jniLibs/x86_64/libaapt2.so | Bin 0 -> 7088320 bytes app/src/main/res/values/strings.xml | 10 + 36 files changed, 1457 insertions(+), 314 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/compose/di/WorkerModule.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/Aligning.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/Session.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/aapt/Aapt.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/alignment/ZipAlign.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/Extensions.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/ZipFile.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEndRecord.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEntry.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/data/PatchBundle.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/data/repository/PatchesRepository.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchInfo.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherProgressManager.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherWorker.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/ui/component/PatchItem.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/ui/screen/InstallerScreen.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/ui/viewmodel/AppSelectorViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/compose/ui/viewmodel/InstallerScreenViewModel.kt create mode 100644 app/src/main/jniLibs/arm64-v8a/libaapt2.so create mode 100644 app/src/main/jniLibs/armeabi-v7a/libaapt2.so create mode 100644 app/src/main/jniLibs/x86/libaapt2.so create mode 100644 app/src/main/jniLibs/x86_64/libaapt2.so diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 079beda..8b81f4e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,6 +30,13 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + + packagingOptions { + resources { + excludes += "/prebuilt/**" + } + } + kotlinOptions { jvmTarget = "11" } @@ -43,10 +50,12 @@ dependencies { // AndroidX Core implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.activity:activity-compose:1.7.1") implementation("androidx.paging:paging-common-ktx:3.1.1") + implementation("androidx.work:work-runtime-ktx:2.8.1") // Compose implementation(platform("androidx.compose:compose-bom:2023.05.01")) @@ -70,11 +79,13 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") // ReVanced - implementation("app.revanced:revanced-patcher:7.0.0") + implementation("app.revanced:revanced-patcher:7.1.0") // Koin - implementation("io.insert-koin:koin-android:3.4.0") + val koinVersion = "3.4.0" + implementation("io.insert-koin:koin-android:$koinVersion") implementation("io.insert-koin:koin-androidx-compose:3.4.4") + implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion") // Compose Navigation implementation("dev.olshevski.navigation:reimagined:1.4.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4928e87..27c3deb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,8 @@ android:name=".ManagerApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" + android:extractNativeLibs="true" + android:largeHeap="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" @@ -40,5 +42,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/MainActivity.kt b/app/src/main/java/app/revanced/manager/compose/MainActivity.kt index beb96eb..4fa9469 100644 --- a/app/src/main/java/app/revanced/manager/compose/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/compose/MainActivity.kt @@ -12,6 +12,7 @@ import app.revanced.manager.compose.ui.screen.AppSelectorScreen import app.revanced.manager.compose.ui.screen.DashboardScreen import app.revanced.manager.compose.ui.screen.PatchesSelectorScreen import app.revanced.manager.compose.ui.screen.SettingsScreen +import app.revanced.manager.compose.ui.screen.InstallerScreen import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme import app.revanced.manager.compose.ui.theme.Theme import app.revanced.manager.compose.util.PM @@ -24,6 +25,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.koin.android.ext.android.inject +import org.koin.androidx.compose.getViewModel +import org.koin.core.parameter.parametersOf class MainActivity : ComponentActivity() { private val prefs: PreferencesManager by inject() @@ -53,7 +56,6 @@ class MainActivity : ComponentActivity() { controller = navController ) { destination -> when (destination) { - is Destination.Dashboard -> DashboardScreen( onSettingsClick = { navController.navigate(Destination.Settings) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) } @@ -64,14 +66,29 @@ class MainActivity : ComponentActivity() { ) is Destination.AppSelector -> AppSelectorScreen( - onAppClick = { navController.navigate(Destination.PatchesSelector) }, + onAppClick = { navController.navigate(Destination.PatchesSelector(it)) }, onBackClick = { navController.pop() } ) is Destination.PatchesSelector -> PatchesSelectorScreen( - onBackClick = { navController.pop() } + onBackClick = { navController.pop() }, + startPatching = { + navController.navigate( + Destination.Installer( + destination.input, + it + ) + ) + }, + vm = getViewModel { parametersOf(destination.input) } ) + is Destination.Installer -> InstallerScreen(getViewModel { + parametersOf( + destination.input, + destination.selectedPatches + ) + }) } } } diff --git a/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt index 2a44586..8db0d4b 100644 --- a/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt @@ -3,20 +3,23 @@ package app.revanced.manager.compose import android.app.Application import app.revanced.manager.compose.di.* import org.koin.android.ext.koin.androidContext +import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin -class ManagerApplication: Application() { +class ManagerApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@ManagerApplication) + workManagerFactory() modules( httpModule, preferencesModule, repositoryModule, serviceModule, - viewModelModule + workerModule, + viewModelModule, ) } } diff --git a/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt index 1479824..8750bbb 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt @@ -2,10 +2,12 @@ package app.revanced.manager.compose.di import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl import app.revanced.manager.compose.network.api.ManagerAPI +import app.revanced.manager.compose.patcher.data.repository.* import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val repositoryModule = module { singleOf(::ReVancedRepositoryImpl) singleOf(::ManagerAPI) + singleOf(::PatchesRepository) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt index cc30bd5..9066159 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt @@ -2,9 +2,23 @@ package app.revanced.manager.compose.di import app.revanced.manager.compose.ui.viewmodel.* import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val viewModelModule = module { - viewModelOf(::PatchesSelectorViewModel) + viewModel { + PatchesSelectorViewModel( + packageInfo = it.get(), + patchesRepository = get() + ) + } viewModelOf(::SettingsViewModel) -} \ No newline at end of file + viewModelOf(::AppSelectorViewModel) + viewModel { + InstallerScreenViewModel( + input = it.get(), + selectedPatches = it.get(), + app = get() + ) + } +} diff --git a/app/src/main/java/app/revanced/manager/compose/di/WorkerModule.kt b/app/src/main/java/app/revanced/manager/compose/di/WorkerModule.kt new file mode 100644 index 0000000..e7ce418 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/di/WorkerModule.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.compose.di + +import app.revanced.manager.compose.patcher.worker.PatcherWorker +import org.koin.androidx.workmanager.dsl.workerOf +import org.koin.dsl.module + +val workerModule = module { + workerOf(::PatcherWorker) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt index ee89d26..78056c4 100644 --- a/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt +++ b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt @@ -34,28 +34,36 @@ class ManagerAPI( downloadProgress = null } - suspend fun downloadPatchBundle() { + suspend fun downloadPatchBundle(): File? { try { val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() } .resolve("patchbundle.jar") downloadAsset(downloadUrl, patchesFile) + + return patchesFile } catch (e: Exception) { Log.e(tag, "Failed to download patch bundle", e) app.toast("Failed to download patch bundle") } + + return null } - suspend fun downloadIntegrations() { + suspend fun downloadIntegrations(): File? { try { val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() } .resolve("integrations.apk") downloadAsset(downloadUrl, integrationsFile) + + return integrationsFile } catch (e: Exception) { Log.e(tag, "Failed to download integrations", e) app.toast("Failed to download integrations") } + + return null } } diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/Aligning.kt b/app/src/main/java/app/revanced/manager/compose/patcher/Aligning.kt new file mode 100644 index 0000000..6d5bb94 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/Aligning.kt @@ -0,0 +1,38 @@ +package app.revanced.manager.compose.patcher + +import app.revanced.manager.compose.patcher.alignment.ZipAligner +import app.revanced.manager.compose.patcher.alignment.zip.ZipFile +import app.revanced.manager.compose.patcher.alignment.zip.structures.ZipEntry +import app.revanced.patcher.PatcherResult +import java.io.File + +// This is the same aligner used by the CLI. +// It will be removed eventually. +object Aligning { + fun align(result: PatcherResult, inputFile: File, outputFile: File) { + // logger.info("Aligning ${inputFile.name} to ${outputFile.name}") + + if (outputFile.exists()) outputFile.delete() + + ZipFile(outputFile).use { file -> + result.dexFiles.forEach { + file.addEntryCompressData( + ZipEntry.createWithName(it.name), + it.stream.readBytes() + ) + } + + result.resourceFile?.let { + file.copyEntriesFromFileAligned( + ZipFile(it), + ZipAligner::getEntryAlignment + ) + } + + file.copyEntriesFromFileAligned( + ZipFile(inputFile), + ZipAligner::getEntryAlignment + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/Session.kt b/app/src/main/java/app/revanced/manager/compose/patcher/Session.kt new file mode 100644 index 0000000..d84fc35 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/Session.kt @@ -0,0 +1,100 @@ +package app.revanced.manager.compose.patcher + +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions +import app.revanced.patcher.logging.Logger +import android.util.Log +import app.revanced.manager.compose.patcher.worker.Progress +import app.revanced.patcher.data.Context +import app.revanced.patcher.patch.Patch +import java.io.Closeable +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +internal typealias PatchClass = Class> +internal typealias PatchList = List + +class Session( + cacheDir: String, + frameworkDir: String, + aaptPath: String, + private val input: File, + private val onProgress: suspend (Progress) -> Unit = { } +) : Closeable { + class PatchFailedException(val patchName: String, cause: Throwable?) : Exception("Got exception while executing $patchName", cause) + + private val logger = LogcatLogger + private val temporary = File(cacheDir).resolve("manager").also { it.mkdirs() } + private val patcher = Patcher( + PatcherOptions( + inputFile = input, + resourceCacheDirectory = temporary.resolve("aapt-resources").path, + frameworkFolderLocation = frameworkDir, + aaptPath = aaptPath, + logger = logger, + ) + ) + + private suspend fun Patcher.applyPatchesVerbose() { + this.executePatches(true).forEach { (patch, result) -> + if (result.isSuccess) { + logger.info("$patch succeeded") + onProgress(Progress.PatchSuccess(patch)) + return@forEach + } + logger.error("$patch failed:") + result.exceptionOrNull()!!.printStackTrace() + + throw PatchFailedException(patch, result.exceptionOrNull()) + } + } + + suspend fun run(output: File, selectedPatches: PatchList, integrations: List) { + onProgress(Progress.Merging) + + with(patcher) { + logger.info("Merging integrations") + addIntegrations(integrations) {} + addPatches(selectedPatches) + + logger.info("Applying patches...") + onProgress(Progress.PatchingStart) + + applyPatchesVerbose() + } + + onProgress(Progress.Saving) + logger.info("Writing patched files...") + val result = patcher.save() + + val aligned = temporary.resolve("aligned.apk").also { Aligning.align(result, input, it) } + + logger.info("Patched apk saved to $aligned") + + Files.move(aligned.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + override fun close() { + temporary.delete() + } +} + +private object LogcatLogger : Logger { + private const val tag = "revanced-patcher" + override fun error(msg: String) { + Log.e(tag, msg) + } + + override fun warn(msg: String) { + Log.w(tag, msg) + } + + override fun info(msg: String) { + Log.i(tag, msg) + } + + override fun trace(msg: String) { + Log.v(tag, msg) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/compose/patcher/aapt/Aapt.kt new file mode 100644 index 0000000..20aa05f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/aapt/Aapt.kt @@ -0,0 +1,13 @@ +package app.revanced.manager.compose.patcher.aapt + +import android.content.Context +import java.io.File + +object Aapt { + fun binary(context: Context): File? { + return File(context.applicationInfo.nativeLibraryDir).resolveAapt() + } +} + +private fun File.resolveAapt() = + list { _, f -> !File(f).isDirectory && f.contains("aapt") }?.firstOrNull()?.let { resolve(it) } diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/alignment/ZipAlign.kt b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/ZipAlign.kt new file mode 100644 index 0000000..354fd1e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/ZipAlign.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.compose.patcher.alignment + +import app.revanced.manager.compose.patcher.alignment.zip.structures.ZipEntry + +internal object ZipAligner { + private const val DEFAULT_ALIGNMENT = 4 + private const val LIBRARY_ALIGNMENT = 4096 + + fun getEntryAlignment(entry: ZipEntry): Int? = + if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/Extensions.kt b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/Extensions.kt new file mode 100644 index 0000000..dfeaf8d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/Extensions.kt @@ -0,0 +1,33 @@ +package app.revanced.manager.compose.patcher.alignment.zip + +import java.io.DataInput +import java.io.DataOutput +import java.nio.ByteBuffer + +fun UInt.toLittleEndian() = + (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt() + +fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort() + +fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8) + or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt() + +fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort() + +fun ByteBuffer.getUShort() = this.getShort().toUShort() +fun ByteBuffer.getUInt() = this.getInt().toUInt() + +fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort()) +fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt()) + +fun DataInput.readUShort() = this.readShort().toUShort() +fun DataInput.readUInt() = this.readInt().toUInt() + +fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt()) +fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt()) + +fun DataInput.readUShortLE() = this.readUShort().toBigEndian() +fun DataInput.readUIntLE() = this.readUInt().toBigEndian() + +fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian()) +fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian()) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/ZipFile.kt b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/ZipFile.kt new file mode 100644 index 0000000..f828fbe --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/ZipFile.kt @@ -0,0 +1,177 @@ +package app.revanced.manager.compose.patcher.alignment.zip + +import app.revanced.manager.compose.patcher.alignment.zip.structures.ZipEndRecord +import app.revanced.manager.compose.patcher.alignment.zip.structures.ZipEntry + +import java.io.Closeable +import java.io.File +import java.io.RandomAccessFile +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.util.zip.CRC32 +import java.util.zip.Deflater + +class ZipFile(file: File) : Closeable { + var entries: MutableList = mutableListOf() + + private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") + private var CDNeedsRewrite = false + + private val compressionLevel = 5 + + init { + //if file isn't empty try to load entries + if (file.length() > 0) { + val endRecord = findEndRecord() + + if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) + throw IllegalArgumentException("Multi-file archives are not supported") + + entries = readEntries(endRecord).toMutableList() + } + + //seek back to start for writing + filePointer.seek(0) + } + + private fun findEndRecord(): ZipEndRecord { + //look from end to start since end record is at the end + for (i in filePointer.length() - 1 downTo 0) { + filePointer.seek(i) + //possible beginning of signature + if (filePointer.readByte() == 0x50.toByte()) { + //seek back to get the full int + filePointer.seek(i) + val possibleSignature = filePointer.readUIntLE() + if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { + filePointer.seek(i) + return ZipEndRecord.fromECD(filePointer) + } + } + } + + throw Exception("Couldn't find end record") + } + + private fun readEntries(endRecord: ZipEndRecord): List { + filePointer.seek(endRecord.centralDirectoryStartOffset.toLong()) + + val numberOfEntries = endRecord.diskEntries.toInt() + + return buildList(numberOfEntries) { + for (i in 1..numberOfEntries) { + add( + ZipEntry.fromCDE(filePointer).also + { + //for some reason the local extra field can be different from the central one + it.readLocalExtra( + filePointer.channel.map( + FileChannel.MapMode.READ_ONLY, + it.localHeaderOffset.toLong() + 28, + 2 + ) + ) + }) + } + } + } + + private fun writeCD() { + val CDStart = filePointer.channel.position().toUInt() + + entries.forEach { + filePointer.channel.write(it.toCDE()) + } + + val entriesCount = entries.size.toUShort() + + val endRecord = ZipEndRecord( + 0u, + 0u, + entriesCount, + entriesCount, + filePointer.channel.position().toUInt() - CDStart, + CDStart, + "" + ) + + filePointer.channel.write(endRecord.toECD()) + } + + private fun addEntry(entry: ZipEntry, data: ByteBuffer) { + CDNeedsRewrite = true + + entry.localHeaderOffset = filePointer.channel.position().toUInt() + + filePointer.channel.write(entry.toLFH()) + filePointer.channel.write(data) + + entries.add(entry) + } + + fun addEntryCompressData(entry: ZipEntry, data: ByteArray) { + val compressor = Deflater(compressionLevel, true) + compressor.setInput(data) + compressor.finish() + + val uncompressedSize = data.size + val compressedData = + ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger + + val compressedDataLength = compressor.deflate(compressedData) + val compressedBuffer = + ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray()) + + compressor.end() + + val crc = CRC32() + crc.update(data) + + entry.compression = 8u //deflate compression + entry.uncompressedSize = uncompressedSize.toUInt() + entry.compressedSize = compressedDataLength.toUInt() + entry.crc32 = crc.value.toUInt() + + addEntry(entry, compressedBuffer) + } + + private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { + alignment?.let { + //calculate where data would end up + val dataOffset = filePointer.filePointer + entry.LFHSize + + val mod = dataOffset % alignment + + //wrong alignment + if (mod != 0L) { + //add padding at end of extra field + entry.localExtraField = + entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) + } + } + + addEntry(entry, data) + } + + fun getDataForEntry(entry: ZipEntry): ByteBuffer { + return filePointer.channel.map( + FileChannel.MapMode.READ_ONLY, + entry.dataOffset.toLong(), + entry.compressedSize.toLong() + ) + } + + fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { + for (entry in file.entries) { + if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates + + val data = file.getDataForEntry(entry) + addEntryCopyData(entry, data, entryAlignment(entry)) + } + } + + override fun close() { + if (CDNeedsRewrite) writeCD() + filePointer.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEndRecord.kt b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEndRecord.kt new file mode 100644 index 0000000..08319db --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEndRecord.kt @@ -0,0 +1,74 @@ +package app.revanced.manager.compose.patcher.alignment.zip.structures + +import app.revanced.manager.compose.patcher.alignment.zip.* +import java.io.DataInput +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class ZipEndRecord( + val diskNumber: UShort, + val startingDiskNumber: UShort, + val diskEntries: UShort, + val totalEntries: UShort, + val centralDirectorySize: UInt, + val centralDirectoryStartOffset: UInt, + val fileComment: String, +) { + + companion object { + const val ECD_HEADER_SIZE = 22 + const val ECD_SIGNATURE = 0x06054b50u + + fun fromECD(input: DataInput): ZipEndRecord { + val signature = input.readUIntLE() + + if (signature != ECD_SIGNATURE) + throw IllegalArgumentException("Input doesn't start with end record signature") + + val diskNumber = input.readUShortLE() + val startingDiskNumber = input.readUShortLE() + val diskEntries = input.readUShortLE() + val totalEntries = input.readUShortLE() + val centralDirectorySize = input.readUIntLE() + val centralDirectoryStartOffset = input.readUIntLE() + val fileCommentLength = input.readUShortLE() + var fileComment = "" + + if (fileCommentLength > 0u) { + val fileCommentBytes = ByteArray(fileCommentLength.toInt()) + input.readFully(fileCommentBytes) + fileComment = fileCommentBytes.toString(Charsets.UTF_8) + } + + return ZipEndRecord( + diskNumber, + startingDiskNumber, + diskEntries, + totalEntries, + centralDirectorySize, + centralDirectoryStartOffset, + fileComment + ) + } + } + + fun toECD(): ByteBuffer { + val commentBytes = fileComment.toByteArray(Charsets.UTF_8) + + val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(ECD_SIGNATURE) + buffer.putUShort(diskNumber) + buffer.putUShort(startingDiskNumber) + buffer.putUShort(diskEntries) + buffer.putUShort(totalEntries) + buffer.putUInt(centralDirectorySize) + buffer.putUInt(centralDirectoryStartOffset) + buffer.putUShort(commentBytes.size.toUShort()) + + buffer.put(commentBytes) + + buffer.flip() + return buffer + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEntry.kt b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEntry.kt new file mode 100644 index 0000000..c99ae21 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/alignment/zip/structures/ZipEntry.kt @@ -0,0 +1,189 @@ +package app.revanced.manager.compose.patcher.alignment.zip.structures + +import app.revanced.manager.compose.patcher.alignment.zip.* +import java.io.DataInput +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class ZipEntry( + val version: UShort, + val versionNeeded: UShort, + val flags: UShort, + var compression: UShort, + val modificationTime: UShort, + val modificationDate: UShort, + var crc32: UInt, + var compressedSize: UInt, + var uncompressedSize: UInt, + val diskNumber: UShort, + val internalAttributes: UShort, + val externalAttributes: UInt, + var localHeaderOffset: UInt, + val fileName: String, + val extraField: ByteArray, + val fileComment: String, + var localExtraField: ByteArray = ByteArray(0), //separate for alignment +) { + val LFHSize: Int + get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size + + val dataOffset: UInt + get() = localHeaderOffset + LFHSize.toUInt() + + companion object { + const val CDE_HEADER_SIZE = 46 + const val CDE_SIGNATURE = 0x02014b50u + + const val LFH_HEADER_SIZE = 30 + const val LFH_SIGNATURE = 0x04034b50u + + fun createWithName(fileName: String): ZipEntry { + return ZipEntry( + 0x1403u, //made by unix, version 20 + 0u, + 0u, + 0u, + 0x0821u, //seems to be static time google uses, no idea + 0x0221u, //same as above + 0u, + 0u, + 0u, + 0u, + 0u, + 0u, + 0u, + fileName, + ByteArray(0), + "" + ) + } + + fun fromCDE(input: DataInput): ZipEntry { + val signature = input.readUIntLE() + + if (signature != CDE_SIGNATURE) + throw IllegalArgumentException("Input doesn't start with central directory entry signature") + + val version = input.readUShortLE() + val versionNeeded = input.readUShortLE() + var flags = input.readUShortLE() + val compression = input.readUShortLE() + val modificationTime = input.readUShortLE() + val modificationDate = input.readUShortLE() + val crc32 = input.readUIntLE() + val compressedSize = input.readUIntLE() + val uncompressedSize = input.readUIntLE() + val fileNameLength = input.readUShortLE() + var fileName = "" + val extraFieldLength = input.readUShortLE() + val extraField = ByteArray(extraFieldLength.toInt()) + val fileCommentLength = input.readUShortLE() + var fileComment = "" + val diskNumber = input.readUShortLE() + val internalAttributes = input.readUShortLE() + val externalAttributes = input.readUIntLE() + val localHeaderOffset = input.readUIntLE() + + val variableFieldsLength = + fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt() + + if (variableFieldsLength > 0) { + val fileNameBytes = ByteArray(fileNameLength.toInt()) + input.readFully(fileNameBytes) + fileName = fileNameBytes.toString(Charsets.UTF_8) + + input.readFully(extraField) + + val fileCommentBytes = ByteArray(fileCommentLength.toInt()) + input.readFully(fileCommentBytes) + fileComment = fileCommentBytes.toString(Charsets.UTF_8) + } + + flags = (flags and 0b1000u.inv() + .toUShort()) //disable data descriptor flag as they are not used + + return ZipEntry( + version, + versionNeeded, + flags, + compression, + modificationTime, + modificationDate, + crc32, + compressedSize, + uncompressedSize, + diskNumber, + internalAttributes, + externalAttributes, + localHeaderOffset, + fileName, + extraField, + fileComment, + ) + } + } + + fun readLocalExtra(buffer: ByteBuffer) { + buffer.order(ByteOrder.LITTLE_ENDIAN) + localExtraField = ByteArray(buffer.getUShort().toInt()) + } + + fun toLFH(): ByteBuffer { + val nameBytes = fileName.toByteArray(Charsets.UTF_8) + + val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) + .also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(LFH_SIGNATURE) + buffer.putUShort(versionNeeded) + buffer.putUShort(flags) + buffer.putUShort(compression) + buffer.putUShort(modificationTime) + buffer.putUShort(modificationDate) + buffer.putUInt(crc32) + buffer.putUInt(compressedSize) + buffer.putUInt(uncompressedSize) + buffer.putUShort(nameBytes.size.toUShort()) + buffer.putUShort(localExtraField.size.toUShort()) + + buffer.put(nameBytes) + buffer.put(localExtraField) + + buffer.flip() + return buffer + } + + fun toCDE(): ByteBuffer { + val nameBytes = fileName.toByteArray(Charsets.UTF_8) + val commentBytes = fileComment.toByteArray(Charsets.UTF_8) + + val buffer = + ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size) + .also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(CDE_SIGNATURE) + buffer.putUShort(version) + buffer.putUShort(versionNeeded) + buffer.putUShort(flags) + buffer.putUShort(compression) + buffer.putUShort(modificationTime) + buffer.putUShort(modificationDate) + buffer.putUInt(crc32) + buffer.putUInt(compressedSize) + buffer.putUInt(uncompressedSize) + buffer.putUShort(nameBytes.size.toUShort()) + buffer.putUShort(extraField.size.toUShort()) + buffer.putUShort(commentBytes.size.toUShort()) + buffer.putUShort(diskNumber) + buffer.putUShort(internalAttributes) + buffer.putUInt(externalAttributes) + buffer.putUInt(localHeaderOffset) + + buffer.put(nameBytes) + buffer.put(extraField) + buffer.put(commentBytes) + + buffer.flip() + return buffer + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/data/PatchBundle.kt b/app/src/main/java/app/revanced/manager/compose/patcher/data/PatchBundle.kt new file mode 100644 index 0000000..f7b511b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/data/PatchBundle.kt @@ -0,0 +1,40 @@ +package app.revanced.manager.compose.patcher.data + +import app.revanced.manager.compose.patcher.PatchClass +import app.revanced.patcher.Patcher +import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages +import app.revanced.patcher.util.patch.PatchBundle +import dalvik.system.PathClassLoader +import java.io.File + +class PatchBundle(private val loader: Iterable, val integrations: File?) { + constructor(bundleJar: String, integrations: File?) : this( + object : Iterable { + private val bundle = PatchBundle.Dex( + bundleJar, + PathClassLoader(bundleJar, Patcher::class.java.classLoader) + ) + + override fun iterator() = bundle.loadPatches().iterator() + }, + integrations + ) + + /** + * @return A list of patches that are compatible with this Apk. + */ + fun loadPatchesFiltered(packageName: String) = loader.filter { patch -> + val compatiblePackages = patch.compatiblePackages + ?: // The patch has no compatibility constraints, which means it is universal. + return@filter true + + if (!compatiblePackages.any { it.name == packageName }) { + // Patch is not compatible with this package. + return@filter false + } + + true + } + + fun loadAllPatches() = loader.toList() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/data/repository/PatchesRepository.kt b/app/src/main/java/app/revanced/manager/compose/patcher/data/repository/PatchesRepository.kt new file mode 100644 index 0000000..ef554ad --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/data/repository/PatchesRepository.kt @@ -0,0 +1,43 @@ +package app.revanced.manager.compose.patcher.data.repository + +import app.revanced.manager.compose.network.api.ManagerAPI +import app.revanced.manager.compose.patcher.data.PatchBundle +import app.revanced.manager.compose.patcher.patch.PatchInfo +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class PatchesRepository(private val managerAPI: ManagerAPI) { + private val patchInformation = MutableSharedFlow>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private var bundle: PatchBundle? = null + + private val scope = CoroutineScope(Job() + Dispatchers.IO) + + /** + * Load a new bundle and update state associated with it. + */ + private suspend fun loadNewBundle(new: PatchBundle) { + bundle = new + withContext(Dispatchers.Main) { + patchInformation.emit(new.loadAllPatches().map { PatchInfo(it) }) + } + } + + /** + * Get the [PatchBundle], loading it if needed. + */ + private suspend fun getBundle() = bundle ?: PatchBundle( + managerAPI.downloadPatchBundle()!!.absolutePath, + managerAPI.downloadIntegrations() + ).also { + loadNewBundle(it) + } + + suspend fun loadPatchClassesFiltered(packageName: String) = + getBundle().loadPatchesFiltered(packageName) + + fun getPatchInformation() = patchInformation.asSharedFlow().also { scope.launch { getBundle() } } + + suspend fun getIntegrations() = listOfNotNull(getBundle().integrations) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchInfo.kt new file mode 100644 index 0000000..e04adf4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchInfo.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.compose.patcher.patch + +import android.os.Parcelable +import app.revanced.manager.compose.patcher.PatchClass +import app.revanced.patcher.annotation.Package +import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages +import app.revanced.patcher.extensions.PatchExtensions.dependencies +import app.revanced.patcher.extensions.PatchExtensions.description +import app.revanced.patcher.extensions.PatchExtensions.include +import app.revanced.patcher.extensions.PatchExtensions.options +import app.revanced.patcher.extensions.PatchExtensions.patchName +import app.revanced.patcher.patch.PatchOption +import kotlinx.parcelize.Parcelize + +@Parcelize +data class PatchInfo( + val name: String, + val description: String?, + val dependencies: List?, + val include: Boolean, + val compatiblePackages: List?, + val options: List