diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 079bedab..8b81f4e0 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 4928e873..27c3deb2 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 beb96ebc..4fa94695 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 2a44586f..8db0d4b7 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 1479824e..8750bbb2 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 cc30bd56..90661592 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 00000000..e7ce4184
--- /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 ee89d26f..78056c4c 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 00000000..6d5bb940
--- /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 00000000..d84fc35c
--- /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 00000000..20aa05fa
--- /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 00000000..354fd1eb
--- /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 00000000..dfeaf8dc
--- /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 00000000..f828fbe9
--- /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 00000000..08319db4
--- /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 00000000..c99ae218
--- /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 00000000..f7b511b6
--- /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 00000000..ef554ad2
--- /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 00000000..e04adf4a
--- /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