mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-09-10 08:27:11 +02:00
feat: integrate revanced patcher (#22)
This commit is contained in:
parent
2810e2e5ed
commit
437a9f1508
@ -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")
|
||||
|
@ -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 @@
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModel {
|
||||
InstallerScreenViewModel(
|
||||
input = it.get(),
|
||||
selectedPatches = it.get(),
|
||||
app = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<out Patch<Context>>
|
||||
internal typealias PatchList = List<PatchClass>
|
||||
|
||||
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<File>) {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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) }
|
@ -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
|
||||
}
|
@ -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())
|
@ -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<ZipEntry> = 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<ZipEntry> {
|
||||
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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<PatchClass>, val integrations: File?) {
|
||||
constructor(bundleJar: String, integrations: File?) : this(
|
||||
object : Iterable<PatchClass> {
|
||||
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()
|
||||
}
|
@ -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<List<PatchInfo>>(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)
|
||||
}
|
@ -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<String>?,
|
||||
val include: Boolean,
|
||||
val compatiblePackages: List<CompatiblePackage>?,
|
||||
val options: List<Option>?
|
||||
) : Parcelable {
|
||||
constructor(patch: PatchClass) : this(
|
||||
patch.patchName,
|
||||
patch.description,
|
||||
patch.dependencies?.map { it.java.patchName },
|
||||
patch.include,
|
||||
patch.compatiblePackages?.map { CompatiblePackage(it) },
|
||||
patch.options?.map { Option(it) })
|
||||
|
||||
fun compatibleWith(packageName: String) = compatiblePackages?.any { it.name == packageName } ?: true
|
||||
|
||||
fun supportsVersion(versionName: String) =
|
||||
compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } } ?: true
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class CompatiblePackage(val name: String, val versions: List<String>) : Parcelable {
|
||||
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Option(val title: String, val key: String, val description: String, val required: Boolean) : Parcelable {
|
||||
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required)
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package app.revanced.manager.compose.patcher.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.work.Data
|
||||
import androidx.work.workDataOf
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.patcher.Session
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
sealed class Progress {
|
||||
object Unpacking : Progress()
|
||||
object Merging : Progress()
|
||||
object PatchingStart : Progress()
|
||||
|
||||
data class PatchSuccess(val patchName: String) : Progress()
|
||||
|
||||
object Saving : Progress()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class StepStatus {
|
||||
WAITING,
|
||||
COMPLETED,
|
||||
FAILURE,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Step(val name: String, val status: StepStatus = StepStatus.WAITING)
|
||||
|
||||
@Serializable
|
||||
class StepGroup(@StringRes val name: Int, val steps: List<Step>, val status: StepStatus = StepStatus.WAITING)
|
||||
|
||||
class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
|
||||
val stepGroups = generateGroupsList(context, selectedPatches)
|
||||
|
||||
companion object {
|
||||
private const val PATCHES = 1
|
||||
private const val WORK_DATA_KEY = "progress"
|
||||
|
||||
/**
|
||||
* A map of [Session.Progress] to the corresponding position in [stepGroups]
|
||||
*/
|
||||
private val stepKeyMap = mapOf(
|
||||
Progress.Unpacking to StepKey(0, 0),
|
||||
Progress.Merging to StepKey(0, 1),
|
||||
Progress.PatchingStart to StepKey(PATCHES, 0),
|
||||
Progress.Saving to StepKey(2, 0),
|
||||
)
|
||||
|
||||
fun generateGroupsList(context: Context, selectedPatches: List<String>) = mutableListOf(
|
||||
StepGroup(
|
||||
R.string.patcher_step_group_prepare,
|
||||
listOf(
|
||||
Step(context.getString(R.string.patcher_step_unpack)),
|
||||
Step(context.getString(R.string.patcher_step_integrations))
|
||||
)
|
||||
),
|
||||
StepGroup(
|
||||
R.string.patcher_step_group_patching,
|
||||
selectedPatches.map { Step(it) }
|
||||
),
|
||||
StepGroup(
|
||||
R.string.patcher_step_group_saving,
|
||||
listOf(Step(context.getString(R.string.patcher_step_write_patched)))
|
||||
)
|
||||
)
|
||||
|
||||
fun groupsFromWorkData(workData: Data) = workData.getString(WORK_DATA_KEY)
|
||||
?.let { Json.decodeFromString<List<StepGroup>>(it) }
|
||||
}
|
||||
|
||||
fun groupsToWorkData() = workDataOf(WORK_DATA_KEY to Json.Default.encodeToString(stepGroups))
|
||||
|
||||
private var currentStep: StepKey? = null
|
||||
|
||||
private fun <T> MutableList<T>.mutateIndex(index: Int, callback: (T) -> T) = apply {
|
||||
this[index] = callback(this[index])
|
||||
}
|
||||
|
||||
private fun updateStepStatus(key: StepKey, newStatus: StepStatus) {
|
||||
var isLastStepOfGroup = false
|
||||
stepGroups.mutateIndex(key.groupIndex) { group ->
|
||||
isLastStepOfGroup = key.stepIndex == group.steps.size - 1
|
||||
val newGroupStatus = when {
|
||||
// This group failed if a step in it failed.
|
||||
newStatus == StepStatus.FAILURE -> StepStatus.FAILURE
|
||||
// All steps in the group succeeded.
|
||||
newStatus == StepStatus.COMPLETED && isLastStepOfGroup -> StepStatus.COMPLETED
|
||||
// Keep the old status.
|
||||
else -> group.status
|
||||
}
|
||||
|
||||
StepGroup(group.name, group.steps.toMutableList().mutateIndex(key.stepIndex) { step ->
|
||||
Step(step.name, newStatus)
|
||||
}, newGroupStatus)
|
||||
}
|
||||
|
||||
val isFinalStep = isLastStepOfGroup && key.groupIndex == stepGroups.size - 1
|
||||
|
||||
if (newStatus == StepStatus.COMPLETED) {
|
||||
// Move the cursor to the next step.
|
||||
currentStep = when {
|
||||
isFinalStep -> null // Final step has been completed.
|
||||
isLastStepOfGroup -> StepKey(key.groupIndex + 1, 0) // Move to the next group.
|
||||
else -> StepKey(key.groupIndex, key.stepIndex + 1) // Move to the next step of this group.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentStepStatus(newStatus: StepStatus) = currentStep?.let { updateStepStatus(it, newStatus) }
|
||||
|
||||
private data class StepKey(val groupIndex: Int, val stepIndex: Int)
|
||||
|
||||
fun handle(progress: Progress) {
|
||||
if (progress is Progress.PatchSuccess) {
|
||||
val patchStepKey = StepKey(
|
||||
PATCHES,
|
||||
stepGroups[PATCHES].steps.indexOfFirst { it.name == progress.patchName })
|
||||
|
||||
updateStepStatus(patchStepKey, StepStatus.COMPLETED)
|
||||
} else {
|
||||
currentStep?.let { updateStepStatus(it, StepStatus.COMPLETED) }
|
||||
|
||||
currentStep = stepKeyMap[progress]!!
|
||||
}
|
||||
}
|
||||
|
||||
fun failure() {
|
||||
// TODO: associate the exception with the step that just failed.
|
||||
setCurrentStepStatus(StepStatus.FAILURE)
|
||||
}
|
||||
|
||||
fun success() {
|
||||
setCurrentStepStatus(StepStatus.COMPLETED)
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package app.revanced.manager.compose.patcher.worker
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.revanced.manager.compose.patcher.data.repository.PatchesRepository
|
||||
import app.revanced.manager.compose.patcher.Session
|
||||
import app.revanced.manager.compose.patcher.aapt.Aapt
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
// TODO: setup wakelock + notification so android doesn't murder us.
|
||||
class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters),
|
||||
KoinComponent {
|
||||
private val patchesRepository: PatchesRepository by inject()
|
||||
|
||||
@Serializable
|
||||
data class Args(
|
||||
val input: String,
|
||||
val output: String,
|
||||
val selectedPatches: List<String>,
|
||||
val packageName: String,
|
||||
val packageVersion: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val ARGS_KEY = "args"
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (runAttemptCount > 0) {
|
||||
Log.d("revanced-worker", "Android requested retrying but retrying is disabled.")
|
||||
return Result.failure()
|
||||
}
|
||||
val aaptPath =
|
||||
Aapt.binary(applicationContext)?.absolutePath ?: throw FileNotFoundException("Could not resolve aapt.")
|
||||
|
||||
val frameworkPath =
|
||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!)
|
||||
val selected = args.selectedPatches.toSet()
|
||||
|
||||
val patchList = patchesRepository.loadPatchClassesFiltered(args.packageName)
|
||||
.filter { selected.contains(it.patchName) }
|
||||
|
||||
val progressManager = PatcherProgressManager(applicationContext, args.selectedPatches)
|
||||
|
||||
suspend fun updateProgress(progress: Progress) {
|
||||
progressManager.handle(progress)
|
||||
setProgress(progressManager.groupsToWorkData())
|
||||
}
|
||||
|
||||
updateProgress(Progress.Unpacking)
|
||||
|
||||
return try {
|
||||
Session(applicationContext.cacheDir.path, frameworkPath, aaptPath, File(args.input)) {
|
||||
updateProgress(it)
|
||||
}.use { session ->
|
||||
session.run(File(args.output), patchList, patchesRepository.getIntegrations())
|
||||
}
|
||||
|
||||
Log.i("revanced-worker", "Patching succeeded")
|
||||
progressManager.success()
|
||||
Result.success(progressManager.groupsToWorkData())
|
||||
} catch (e: Throwable) {
|
||||
Log.e("revanced-worker", "Got exception while patching", e)
|
||||
progressManager.failure()
|
||||
Result.failure(progressManager.groupsToWorkData())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package app.revanced.manager.compose.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import app.revanced.manager.compose.patcher.patch.PatchInfo
|
||||
|
||||
@Composable
|
||||
fun PatchItem(
|
||||
patch: PatchInfo,
|
||||
onOptionsDialog: () -> Unit,
|
||||
selected: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
supported: Boolean
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.let { if (!supported) it.alpha(0.5f) else it }
|
||||
.clickable(enabled = supported, onClick = onToggle),
|
||||
leadingContent = {
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = {
|
||||
onToggle()
|
||||
},
|
||||
enabled = supported
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(patch.name)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(patch.description ?: "")
|
||||
},
|
||||
trailingContent = {
|
||||
if (patch.options?.isNotEmpty() == true) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = supported) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package app.revanced.manager.compose.ui.destination
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed interface Destination: Parcelable {
|
||||
|
||||
@ -15,6 +17,8 @@ sealed interface Destination: Parcelable {
|
||||
object Settings: Destination
|
||||
|
||||
@Parcelize
|
||||
object PatchesSelector: Destination
|
||||
data class PatchesSelector(val input: PackageInfo): Destination
|
||||
|
||||
@Parcelize
|
||||
data class Installer(val input: PackageInfo, val selectedPatches: List<String>) : Destination
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@ -22,14 +24,22 @@ import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppIcon
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSelectorScreen(
|
||||
onAppClick: () -> Unit,
|
||||
onBackClick: () -> Unit
|
||||
onAppClick: (PackageInfo) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: AppSelectorViewModel = getViewModel()
|
||||
) {
|
||||
val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { apkUri ->
|
||||
vm.loadSelectedFile(apkUri!!).let(onAppClick)
|
||||
}
|
||||
|
||||
var filterText by rememberSaveable { mutableStateOf("") }
|
||||
var search by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@ -61,11 +71,11 @@ fun AppSelectorScreen(
|
||||
) { app ->
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onAppClick() },
|
||||
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
|
||||
leadingContent = { AppIcon(app.icon, null, 36) },
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = { Text((PM.testList[app.packageName]?: 0).let { if (it == 1) "$it " + stringResource(R.string.patch) else "$it " + stringResource(R.string.patches) }) }
|
||||
trailingContent = { Text("420 Patches") }
|
||||
)
|
||||
|
||||
}
|
||||
@ -106,7 +116,9 @@ fun AppSelectorScreen(
|
||||
item {
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { },
|
||||
modifier = Modifier.clickable {
|
||||
pickApkLauncher.launch("*/*")
|
||||
},
|
||||
leadingContent = { Box(Modifier.size(36.dp), Alignment.Center) { Icon(Icons.Default.Storage, null, modifier = Modifier.size(24.dp)) } },
|
||||
headlineContent = { Text(stringResource(R.string.select_from_storage)) }
|
||||
)
|
||||
@ -124,14 +136,12 @@ fun AppSelectorScreen(
|
||||
val app = list[index]
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onAppClick() },
|
||||
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
|
||||
leadingContent = { AppIcon(app.icon, null, 36) },
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = {
|
||||
Text(
|
||||
(PM.testList[app.packageName]?: 0).let { if (it == 1) "$it " + stringResource(R.string.patch) else "$it " + stringResource(R.string.patches) }
|
||||
)
|
||||
Text("420 Patches")
|
||||
}
|
||||
)
|
||||
|
||||
@ -146,7 +156,6 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
LoadingIndicator()
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppScaffold
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.compose.ui.component.AppScaffold
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel
|
||||
import app.revanced.manager.compose.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InstallerScreen(
|
||||
vm: InstallerScreenViewModel
|
||||
) {
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.installer),
|
||||
onBackClick = { },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
vm.stepGroups.forEach {
|
||||
Column {
|
||||
Text(
|
||||
text = "${stringResource(it.name)}: ${it.status}",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
it.steps.forEach {
|
||||
Text(
|
||||
text = "${it.name}: ${it.status}",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -16,15 +15,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
@ -34,72 +30,54 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.component.GroupHeader
|
||||
import app.revanced.manager.compose.ui.component.PatchItem
|
||||
import app.revanced.manager.compose.ui.viewmodel.PatchesSelectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import java.io.File
|
||||
|
||||
const val allowUnsupported = false
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
selectedApp: List<File>? = null,
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: PatchesSelectorViewModel = getViewModel()
|
||||
startPatching: (List<String>) -> Unit, onBackClick: () -> Unit, vm: PatchesSelectorViewModel
|
||||
) {
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var showOptionsDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showUnsupportedDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
if (showUnsupportedDialog)
|
||||
UnsupportedDialog(onDismissRequest = { showUnsupportedDialog = false })
|
||||
if (vm.showUnsupportedDialog) UnsupportedDialog(onDismissRequest = vm::dismissDialogs)
|
||||
|
||||
if (showOptionsDialog)
|
||||
OptionsDialog(
|
||||
onDismissRequest = { showOptionsDialog = false },
|
||||
onConfirm = {}
|
||||
)
|
||||
if (vm.showOptionsDialog) OptionsDialog(onDismissRequest = vm::dismissDialogs, onConfirm = {})
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.select_patches),
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch)) },
|
||||
icon = { Icon(Icons.Default.Build, null) },
|
||||
onClick = { /*TODO*/ })
|
||||
}
|
||||
) { paddingValues ->
|
||||
Scaffold(topBar = {
|
||||
AppTopBar(title = stringResource(R.string.select_patches), onBackClick = onBackClick, actions = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||
}
|
||||
})
|
||||
}, floatingActionButton = {
|
||||
ExtendedFloatingActionButton(text = { Text(stringResource(R.string.patch)) },
|
||||
icon = { Icon(Icons.Default.Build, null) },
|
||||
onClick = { startPatching(vm.selectedPatches) })
|
||||
}) { paddingValues ->
|
||||
Column(Modifier.fillMaxSize().padding(paddingValues)) {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
viewModel.bundles.forEachIndexed { index, bundle ->
|
||||
bundles.forEachIndexed { index, bundle ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
@ -111,68 +89,40 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
pageCount = viewModel.bundles.size,
|
||||
pageCount = bundles.size,
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
pageContent = { index ->
|
||||
|
||||
val patches = rememberSaveable { viewModel.bundles[index].patches }
|
||||
val bundle = bundles[index]
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(
|
||||
items = patches["supported"]!!
|
||||
items = bundle.supported
|
||||
) { patch ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
if (viewModel.selectedPatches.contains(patch))
|
||||
viewModel.selectedPatches.remove(patch)
|
||||
else
|
||||
viewModel.selectedPatches.add(patch)
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
onOptionsDialog = vm::openOptionsDialog,
|
||||
onToggle = {
|
||||
vm.togglePatch(patch)
|
||||
},
|
||||
leadingContent = {
|
||||
Checkbox(
|
||||
checked = viewModel.selectedPatches.contains(patch),
|
||||
onCheckedChange = {
|
||||
if (viewModel.selectedPatches.contains(patch))
|
||||
viewModel.selectedPatches.remove(patch)
|
||||
else
|
||||
viewModel.selectedPatches.add(patch)
|
||||
}
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(patch.name)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(patch.description)
|
||||
},
|
||||
trailingContent = {
|
||||
if (patch.options.isNotEmpty()) {
|
||||
IconButton(onClick = { showOptionsDialog = true }) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
selected = vm.isSelected(patch),
|
||||
supported = true
|
||||
)
|
||||
}
|
||||
|
||||
if (patches["unsupported"]!!.isNotEmpty()) {
|
||||
if (bundle.unsupported.isNotEmpty()) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp)
|
||||
.padding(end = 10.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp).padding(end = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
GroupHeader("Unsupported patches", Modifier.padding(0.dp))
|
||||
IconButton(onClick = { showUnsupportedDialog = true }) {
|
||||
GroupHeader(stringResource(R.string.unsupported_patches), Modifier.padding(0.dp))
|
||||
IconButton(onClick = vm::openUnsupportedDialog) {
|
||||
Icon(
|
||||
Icons.Outlined.HelpOutline,
|
||||
stringResource(R.string.help)
|
||||
Icons.Outlined.HelpOutline, stringResource(R.string.help)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -180,53 +130,23 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
|
||||
items(
|
||||
items = patches["unsupported"]!!,
|
||||
items = bundle.unsupported,
|
||||
// key = { it.name }
|
||||
) { patch ->
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.alpha(0.5f)
|
||||
.clickable(enabled = false) {
|
||||
if (viewModel.selectedPatches.contains(patch))
|
||||
viewModel.selectedPatches.remove(patch)
|
||||
else
|
||||
viewModel.selectedPatches.add(patch)
|
||||
},
|
||||
leadingContent = {
|
||||
Checkbox(
|
||||
checked = viewModel.selectedPatches.contains(patch),
|
||||
onCheckedChange = {
|
||||
if (viewModel.selectedPatches.contains(patch))
|
||||
viewModel.selectedPatches.remove(patch)
|
||||
else
|
||||
viewModel.selectedPatches.add(patch)
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
onOptionsDialog = vm::openOptionsDialog,
|
||||
onToggle = {
|
||||
vm.togglePatch(patch)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(patch.name)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(patch.description)
|
||||
},
|
||||
trailingContent = {
|
||||
if (patch.options.isNotEmpty()) {
|
||||
IconButton(onClick = { showOptionsDialog = true }, enabled = false) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
selected = vm.isSelected(patch),
|
||||
supported = allowUnsupported
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -236,10 +156,10 @@ fun UnsupportedDialog(
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val appVersion = "1.1.0"
|
||||
val supportedVersions = listOf("1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0")
|
||||
val supportedVersions =
|
||||
listOf("1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0", "1.1.1", "1.2.0")
|
||||
|
||||
AlertDialog(
|
||||
modifier = Modifier.padding(vertical = 45.dp),
|
||||
AlertDialog(modifier = Modifier.padding(vertical = 45.dp),
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
@ -247,29 +167,21 @@ fun UnsupportedDialog(
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.unsupported_app)) },
|
||||
text = { Text(stringResource(R.string.app_not_supported, appVersion, supportedVersions.joinToString(", "))) }
|
||||
)
|
||||
text = { Text(stringResource(R.string.app_not_supported, appVersion, supportedVersions.joinToString(", "))) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
onDismissRequest: () -> Unit, onConfirm: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
onConfirm()
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.options)) },
|
||||
text = {
|
||||
Text("You really thought these would exist?")
|
||||
AlertDialog(onDismissRequest = onDismissRequest, confirmButton = {
|
||||
Button(onClick = {
|
||||
onConfirm()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
)
|
||||
}, title = { Text(stringResource(R.string.options)) }, text = {
|
||||
Text("You really thought these would exist?")
|
||||
})
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.manager.compose.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
class AppSelectorViewModel(private val app: Application) : ViewModel() {
|
||||
fun loadSelectedFile(uri: Uri) =
|
||||
app.contentResolver.openInputStream(uri)!!.use { stream ->
|
||||
File(app.cacheDir, "input.apk").also {
|
||||
if (it.exists()) it.delete()
|
||||
Files.copy(stream, it.toPath())
|
||||
}.let { PM.getApkInfo(it, app) }
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package app.revanced.manager.compose.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.work.*
|
||||
import app.revanced.manager.compose.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.compose.patcher.worker.PatcherProgressManager
|
||||
import app.revanced.manager.compose.patcher.worker.StepGroup
|
||||
import app.revanced.manager.compose.service.InstallService
|
||||
import app.revanced.manager.compose.service.UninstallService
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class InstallerScreenViewModel(
|
||||
input: PackageInfo,
|
||||
selectedPatches: List<String>,
|
||||
private val app: Application
|
||||
) : ViewModel() {
|
||||
var stepGroups by mutableStateOf<List<StepGroup>>(PatcherProgressManager.generateGroupsList(app, selectedPatches))
|
||||
private set
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
// TODO: handle app installation as a step.
|
||||
var installStatus by mutableStateOf<Boolean?>(null)
|
||||
var pmStatus by mutableStateOf(-999)
|
||||
var extra by mutableStateOf("")
|
||||
|
||||
private val outputFile = File(app.cacheDir, "output.apk")
|
||||
|
||||
private val patcherWorker =
|
||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
|
||||
workDataOf(
|
||||
PatcherWorker.ARGS_KEY to
|
||||
Json.Default.encodeToString(
|
||||
PatcherWorker.Args(
|
||||
input.apk.path,
|
||||
outputFile.path,
|
||||
selectedPatches,
|
||||
input.packageName,
|
||||
input.packageName,
|
||||
)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
|
||||
private val liveData = workManager.getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData
|
||||
|
||||
private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status
|
||||
when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> workInfo.progress
|
||||
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData
|
||||
else -> null
|
||||
}?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it }
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
InstallService.APP_INSTALL_ACTION -> {
|
||||
pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||
extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
postInstallStatus()
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
|
||||
liveData.observeForever(observer)
|
||||
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
})
|
||||
}
|
||||
|
||||
fun installApk(apk: List<File>) {
|
||||
PM.installApp(apk, app)
|
||||
}
|
||||
|
||||
fun postInstallStatus() {
|
||||
installStatus = pmStatus == PackageInstaller.STATUS_SUCCESS
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
liveData.removeObserver(observer)
|
||||
app.unregisterReceiver(installBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorker.id)
|
||||
// logs.clear()
|
||||
}
|
||||
}
|
@ -1,85 +1,61 @@
|
||||
package app.revanced.manager.compose.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.compose.patcher.data.repository.PatchesRepository
|
||||
import app.revanced.manager.compose.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class PatchesSelectorViewModel: ViewModel() {
|
||||
private val patchesList = listOf(
|
||||
Patch("amogus-patch", "adds amogus to all apps, mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus mogus ",
|
||||
options = listOf(
|
||||
Option(
|
||||
"amogus"
|
||||
)
|
||||
),
|
||||
isSupported = true
|
||||
),
|
||||
Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = true
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(
|
||||
Option(
|
||||
"amogus"
|
||||
)
|
||||
),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),Patch("microg-support", "makes microg work",
|
||||
options = listOf(),
|
||||
isSupported = false
|
||||
),
|
||||
class PatchesSelectorViewModel(packageInfo: PackageInfo, patchesRepository: PatchesRepository) :
|
||||
ViewModel() {
|
||||
val bundlesFlow = patchesRepository.getPatchInformation().map { patches ->
|
||||
val supported = mutableListOf<PatchInfo>()
|
||||
val unsupported = mutableListOf<PatchInfo>()
|
||||
|
||||
).let { it + it + it + it + it }
|
||||
patches.filter { it.compatibleWith(packageInfo.packageName) }.forEach {
|
||||
val targetList = if (it.supportsVersion(packageInfo.packageName)) supported else unsupported
|
||||
|
||||
private val patchesLists = patchesList.groupBy { if (it.isSupported) "supported" else "unsupported" }
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
val bundles = listOf(
|
||||
Bundle(
|
||||
name = "offical",
|
||||
patches = patchesLists
|
||||
),
|
||||
Bundle(
|
||||
name = "extended",
|
||||
patches = patchesLists
|
||||
),
|
||||
Bundle(
|
||||
name = "balls",
|
||||
patches = patchesLists
|
||||
),
|
||||
)
|
||||
listOf(
|
||||
Bundle(
|
||||
name = "official",
|
||||
supported, unsupported
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var selectedPatches = mutableStateListOf<Patch>()
|
||||
val selectedPatches = mutableStateListOf<String>()
|
||||
|
||||
fun isSelected(patch: PatchInfo) = selectedPatches.contains(patch.name)
|
||||
fun togglePatch(patch: PatchInfo) {
|
||||
val name = patch.name
|
||||
if (isSelected(patch)) selectedPatches.remove(name) else selectedPatches.add(patch.name)
|
||||
}
|
||||
|
||||
data class Bundle(
|
||||
val name: String,
|
||||
val patches: Map<String, List<Patch>>
|
||||
val supported: List<PatchInfo>,
|
||||
val unsupported: List<PatchInfo>
|
||||
)
|
||||
|
||||
data class Patch(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val options: List<Option>,
|
||||
val isSupported: Boolean
|
||||
)
|
||||
var showOptionsDialog by mutableStateOf(false)
|
||||
private set
|
||||
var showUnsupportedDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
data class Option(
|
||||
val name: String
|
||||
)
|
||||
fun dismissDialogs() {
|
||||
showOptionsDialog = false
|
||||
showUnsupportedDialog = false
|
||||
}
|
||||
|
||||
fun openOptionsDialog() {
|
||||
showOptionsDialog = true
|
||||
}
|
||||
|
||||
fun openUnsupportedDialog() {
|
||||
showUnsupportedDialog = true
|
||||
}
|
||||
}
|
@ -6,120 +6,69 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.PackageInfoFlags
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import app.revanced.manager.compose.service.InstallService
|
||||
import app.revanced.manager.compose.service.UninstallService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
@Parcelize
|
||||
data class PackageInfo(val packageName: String, val version: String, val apk: File) : Parcelable {
|
||||
constructor(appInfo: PM.AppInfo) : this(appInfo.packageName, appInfo.versionName, appInfo.apk)
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@Suppress("DEPRECATION")
|
||||
object PM {
|
||||
|
||||
val testList = mapOf(
|
||||
"com.google.android.youtube" to 59,
|
||||
"com.android.vending" to 34,
|
||||
"com.backdrops.wallpapers" to 2,
|
||||
"com.termux" to 2,
|
||||
"com.notinstalled.app" to 1,
|
||||
"com.2notinstalled.app" to 1,
|
||||
"org.adaway" to 5,
|
||||
"com.activitymanager" to 1,
|
||||
"com.guoshi.httpcanary" to 1,
|
||||
"org.lsposed.lspatch" to 1,
|
||||
"app.revanced.manager.flutter" to 100,
|
||||
"com.reddit.frontpage" to 20
|
||||
)
|
||||
|
||||
val appList = mutableStateListOf<AppInfo>()
|
||||
val supportedAppList = mutableStateListOf<AppInfo>()
|
||||
|
||||
suspend fun loadApps(context: Context) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
testList.keys.map {
|
||||
try {
|
||||
val applicationInfo = packageManager.getApplicationInfo(it, 0)
|
||||
|
||||
AppInfo(
|
||||
it,
|
||||
applicationInfo.loadLabel(packageManager).toString(),
|
||||
applicationInfo.loadIcon(packageManager),
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
AppInfo(
|
||||
it,
|
||||
"Not installed"
|
||||
)
|
||||
}
|
||||
}.let { list ->
|
||||
list.sortedWith(
|
||||
compareByDescending<AppInfo> {
|
||||
testList[it.packageName]
|
||||
}.thenBy { it.label }.thenBy { it.packageName }
|
||||
)
|
||||
}.also {
|
||||
withContext(Dispatchers.Main) { supportedAppList.addAll(it) }
|
||||
}
|
||||
|
||||
val localAppList = mutableListOf<AppInfo>()
|
||||
|
||||
packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map {
|
||||
AppInfo(
|
||||
it.packageName,
|
||||
"0.69.420",
|
||||
it.loadLabel(packageManager).toString(),
|
||||
it.loadIcon(packageManager)
|
||||
it.loadIcon(packageManager),
|
||||
File("h")
|
||||
)
|
||||
}.also { localAppList.addAll(it) }
|
||||
|
||||
testList.keys.mapNotNull { packageName ->
|
||||
if (!localAppList.any { packageName == it.packageName }) {
|
||||
AppInfo(
|
||||
packageName,
|
||||
"Not installed"
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.also { localAppList.addAll(it) }
|
||||
|
||||
localAppList.sortWith(
|
||||
compareByDescending<AppInfo> {
|
||||
testList[it.packageName]
|
||||
}.thenBy { it.label }.thenBy { it.packageName }
|
||||
).also {
|
||||
withContext(Dispatchers.Main) { appList.addAll(localAppList) }
|
||||
}
|
||||
}.also { localAppList.addAll(it) }.also { supportedAppList.addAll(it) }
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val versionName: String,
|
||||
val label: String,
|
||||
val icon: @RawValue Drawable? = null
|
||||
val icon: @RawValue Drawable? = null,
|
||||
val apk: File,
|
||||
) : Parcelable
|
||||
|
||||
fun installApp(apk: File, context: Context) {
|
||||
fun installApp(apks: List<File>, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
val session =
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||
session.writeApk(apk)
|
||||
session.commit(context.installIntentSender)
|
||||
session.close()
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
apks.forEach { apk -> session.writeApk(apk) }
|
||||
session.commit(context.installIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
||||
}
|
||||
|
||||
fun getApkInfo(apk: File, context: Context) = context.packageManager.getPackageArchiveInfo(apk.path, 0)!!.let { PackageInfo(it.packageName, it.versionName, apk) }
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
|
BIN
app/src/main/jniLibs/arm64-v8a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/arm64-v8a/libaapt2.so
Normal file
Binary file not shown.
BIN
app/src/main/jniLibs/armeabi-v7a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/armeabi-v7a/libaapt2.so
Normal file
Binary file not shown.
BIN
app/src/main/jniLibs/x86/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/x86/libaapt2.so
Normal file
Binary file not shown.
BIN
app/src/main/jniLibs/x86_64/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/x86_64/libaapt2.so
Normal file
Binary file not shown.
@ -58,5 +58,15 @@
|
||||
<string name="no_sources_set">No sources set</string>
|
||||
<string name="no_patched_apps_found">No patched apps found</string>
|
||||
<string name="unsupported_app">Unsupported app</string>
|
||||
<string name="unsupported_patches">Unsupported patches</string>
|
||||
<string name="app_not_supported">Some of the patches do not support this app version (%s). The patches only support the following versions: %s.</string>
|
||||
|
||||
<string name="installer">Installer</string>
|
||||
|
||||
<string name="patcher_step_group_prepare">Preparation</string>
|
||||
<string name="patcher_step_unpack">Unpack Apk</string>
|
||||
<string name="patcher_step_integrations">Merge Integrations</string>
|
||||
<string name="patcher_step_group_patching">Patching</string>
|
||||
<string name="patcher_step_group_saving">Saving</string>
|
||||
<string name="patcher_step_write_patched">Write patched Apk</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user