feat: integrate revanced patcher (#22)

This commit is contained in:
Ax333l 2023-05-19 20:49:32 +02:00 committed by GitHub
parent f1656c6d1e
commit 40487923f9
36 changed files with 1457 additions and 314 deletions

View File

@ -30,6 +30,13 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
packagingOptions {
resources {
excludes += "/prebuilt/**"
}
}
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
@ -43,10 +50,12 @@ dependencies {
// AndroidX Core // AndroidX Core
implementation("androidx.core:core-ktx:1.10.1") 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.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-compose:1.7.1") implementation("androidx.activity:activity-compose:1.7.1")
implementation("androidx.paging:paging-common-ktx:3.1.1") implementation("androidx.paging:paging-common-ktx:3.1.1")
implementation("androidx.work:work-runtime-ktx:2.8.1")
// Compose // Compose
implementation(platform("androidx.compose:compose-bom:2023.05.01")) implementation(platform("androidx.compose:compose-bom:2023.05.01"))
@ -70,11 +79,13 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
// ReVanced // ReVanced
implementation("app.revanced:revanced-patcher:7.0.0") implementation("app.revanced:revanced-patcher:7.1.0")
// Koin // 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-compose:3.4.4")
implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
// Compose Navigation // Compose Navigation
implementation("dev.olshevski.navigation:reimagined:1.4.0") implementation("dev.olshevski.navigation:reimagined:1.4.0")

View File

@ -19,6 +19,8 @@
android:name=".ManagerApplication" android:name=".ManagerApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:extractNativeLibs="true"
android:largeHeap="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -40,5 +42,16 @@
<service android:name=".service.InstallService" /> <service android:name=".service.InstallService" />
<service android:name=".service.UninstallService" /> <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> </application>
</manifest> </manifest>

View File

@ -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.DashboardScreen
import app.revanced.manager.compose.ui.screen.PatchesSelectorScreen import app.revanced.manager.compose.ui.screen.PatchesSelectorScreen
import app.revanced.manager.compose.ui.screen.SettingsScreen 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.ReVancedManagerTheme
import app.revanced.manager.compose.ui.theme.Theme import app.revanced.manager.compose.ui.theme.Theme
import app.revanced.manager.compose.util.PM import app.revanced.manager.compose.util.PM
@ -24,6 +25,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val prefs: PreferencesManager by inject() private val prefs: PreferencesManager by inject()
@ -53,7 +56,6 @@ class MainActivity : ComponentActivity() {
controller = navController controller = navController
) { destination -> ) { destination ->
when (destination) { when (destination) {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) }, onSettingsClick = { navController.navigate(Destination.Settings) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) } onAppSelectorClick = { navController.navigate(Destination.AppSelector) }
@ -64,14 +66,29 @@ class MainActivity : ComponentActivity() {
) )
is Destination.AppSelector -> AppSelectorScreen( is Destination.AppSelector -> AppSelectorScreen(
onAppClick = { navController.navigate(Destination.PatchesSelector) }, onAppClick = { navController.navigate(Destination.PatchesSelector(it)) },
onBackClick = { navController.pop() } onBackClick = { navController.pop() }
) )
is Destination.PatchesSelector -> PatchesSelectorScreen( 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
)
})
} }
} }
} }

View File

@ -3,20 +3,23 @@ package app.revanced.manager.compose
import android.app.Application import android.app.Application
import app.revanced.manager.compose.di.* import app.revanced.manager.compose.di.*
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
class ManagerApplication: Application() { class ManagerApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startKoin { startKoin {
androidContext(this@ManagerApplication) androidContext(this@ManagerApplication)
workManagerFactory()
modules( modules(
httpModule, httpModule,
preferencesModule, preferencesModule,
repositoryModule, repositoryModule,
serviceModule, serviceModule,
viewModelModule workerModule,
viewModelModule,
) )
} }
} }

View File

@ -2,10 +2,12 @@ package app.revanced.manager.compose.di
import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.compose.network.api.ManagerAPI 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.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val repositoryModule = module { val repositoryModule = module {
singleOf(::ReVancedRepositoryImpl) singleOf(::ReVancedRepositoryImpl)
singleOf(::ManagerAPI) singleOf(::ManagerAPI)
singleOf(::PatchesRepository)
} }

View File

@ -2,9 +2,23 @@ package app.revanced.manager.compose.di
import app.revanced.manager.compose.ui.viewmodel.* import app.revanced.manager.compose.ui.viewmodel.*
import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
viewModelOf(::PatchesSelectorViewModel) viewModel {
PatchesSelectorViewModel(
packageInfo = it.get(),
patchesRepository = get()
)
}
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModel {
InstallerScreenViewModel(
input = it.get(),
selectedPatches = it.get(),
app = get()
)
}
} }

View File

@ -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)
}

View File

@ -34,28 +34,36 @@ class ManagerAPI(
downloadProgress = null downloadProgress = null
} }
suspend fun downloadPatchBundle() { suspend fun downloadPatchBundle(): File? {
try { try {
val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl
val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() } val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() }
.resolve("patchbundle.jar") .resolve("patchbundle.jar")
downloadAsset(downloadUrl, patchesFile) downloadAsset(downloadUrl, patchesFile)
return patchesFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Failed to download patch bundle", e) Log.e(tag, "Failed to download patch bundle", e)
app.toast("Failed to download patch bundle") app.toast("Failed to download patch bundle")
} }
return null
} }
suspend fun downloadIntegrations() { suspend fun downloadIntegrations(): File? {
try { try {
val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl
val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() } val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() }
.resolve("integrations.apk") .resolve("integrations.apk")
downloadAsset(downloadUrl, integrationsFile) downloadAsset(downloadUrl, integrationsFile)
return integrationsFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Failed to download integrations", e) Log.e(tag, "Failed to download integrations", e)
app.toast("Failed to download integrations") app.toast("Failed to download integrations")
} }
return null
} }
} }

View File

@ -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
)
}
}
}

View File

@ -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)
}
}

View File

@ -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) }

View File

@ -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
}

View File

@ -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())

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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())
}
}
}

View File

@ -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)
}
}
}
)
}

View File

@ -1,7 +1,9 @@
package app.revanced.manager.compose.ui.destination package app.revanced.manager.compose.ui.destination
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.compose.util.PackageInfo
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File
sealed interface Destination: Parcelable { sealed interface Destination: Parcelable {
@ -15,6 +17,8 @@ sealed interface Destination: Parcelable {
object Settings: Destination object Settings: Destination
@Parcelize @Parcelize
object PatchesSelector: Destination data class PatchesSelector(val input: PackageInfo): Destination
@Parcelize
data class Installer(val input: PackageInfo, val selectedPatches: List<String>) : Destination
} }

View File

@ -1,5 +1,7 @@
package app.revanced.manager.compose.ui.screen 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.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.AppIcon
import app.revanced.manager.compose.ui.component.AppTopBar import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.component.LoadingIndicator 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.PM
import app.revanced.manager.compose.util.PackageInfo
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSelectorScreen( fun AppSelectorScreen(
onAppClick: () -> Unit, onAppClick: (PackageInfo) -> Unit,
onBackClick: () -> Unit onBackClick: () -> Unit,
vm: AppSelectorViewModel = getViewModel()
) { ) {
val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { apkUri ->
vm.loadSelectedFile(apkUri!!).let(onAppClick)
}
var filterText by rememberSaveable { mutableStateOf("") } var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) } var search by rememberSaveable { mutableStateOf(false) }
@ -61,11 +71,11 @@ fun AppSelectorScreen(
) { app -> ) { app ->
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick() }, modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
leadingContent = { AppIcon(app.icon, null, 36) }, leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) }, headlineContent = { Text(app.label) },
supportingContent = { Text(app.packageName) }, 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 { item {
ListItem( 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)) } }, 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)) } headlineContent = { Text(stringResource(R.string.select_from_storage)) }
) )
@ -124,14 +136,12 @@ fun AppSelectorScreen(
val app = list[index] val app = list[index]
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick() }, modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
leadingContent = { AppIcon(app.icon, null, 36) }, leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) }, headlineContent = { Text(app.label) },
supportingContent = { Text(app.packageName) }, supportingContent = { Text(app.packageName) },
trailingContent = { trailingContent = {
Text( Text("420 Patches")
(PM.testList[app.packageName]?: 0).let { if (it == 1) "$it " + stringResource(R.string.patch) else "$it " + stringResource(R.string.patches) }
)
} }
) )
@ -146,7 +156,6 @@ fun AppSelectorScreen(
} }
} }
} }
} else { } else {
LoadingIndicator() LoadingIndicator()
} }

View File

@ -33,7 +33,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppScaffold
import app.revanced.manager.compose.ui.component.AppTopBar import app.revanced.manager.compose.ui.component.AppTopBar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@ -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
)
}
}
}
}
}
}

View File

@ -1,7 +1,6 @@
package app.revanced.manager.compose.ui.screen package app.revanced.manager.compose.ui.screen
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.filled.Build
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
@ -34,72 +30,54 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope 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.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.compose.R import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppTopBar import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.component.GroupHeader 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 app.revanced.manager.compose.ui.viewmodel.PatchesSelectorViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
import java.io.File const val allowUnsupported = false
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
selectedApp: List<File>? = null, startPatching: (List<String>) -> Unit, onBackClick: () -> Unit, vm: PatchesSelectorViewModel
onBackClick: () -> Unit,
viewModel: PatchesSelectorViewModel = getViewModel()
) { ) {
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var showOptionsDialog by rememberSaveable { mutableStateOf(false) } val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
var showUnsupportedDialog by rememberSaveable { mutableStateOf(false) }
if (showUnsupportedDialog) if (vm.showUnsupportedDialog) UnsupportedDialog(onDismissRequest = vm::dismissDialogs)
UnsupportedDialog(onDismissRequest = { showUnsupportedDialog = false })
if (showOptionsDialog) if (vm.showOptionsDialog) OptionsDialog(onDismissRequest = vm::dismissDialogs, onConfirm = {})
OptionsDialog(
onDismissRequest = { showOptionsDialog = false },
onConfirm = {}
)
Scaffold( Scaffold(topBar = {
topBar = { AppTopBar(title = stringResource(R.string.select_patches), onBackClick = onBackClick, actions = {
AppTopBar( IconButton(onClick = { }) {
title = stringResource(R.string.select_patches), Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
onBackClick = onBackClick, }
actions = { IconButton(onClick = { }) {
IconButton(onClick = { }) { Icon(Icons.Outlined.Search, stringResource(R.string.search))
Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help)) }
} })
IconButton(onClick = { }) { }, floatingActionButton = {
Icon(Icons.Outlined.Search, stringResource(R.string.search)) ExtendedFloatingActionButton(text = { Text(stringResource(R.string.patch)) },
} icon = { Icon(Icons.Default.Build, null) },
} onClick = { startPatching(vm.selectedPatches) })
) }) { paddingValues ->
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = { Icon(Icons.Default.Build, null) },
onClick = { /*TODO*/ })
}
) { paddingValues ->
Column(Modifier.fillMaxSize().padding(paddingValues)) { Column(Modifier.fillMaxSize().padding(paddingValues)) {
TabRow( TabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
viewModel.bundles.forEachIndexed { index, bundle -> bundles.forEachIndexed { index, bundle ->
Tab( Tab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
@ -111,68 +89,40 @@ fun PatchesSelectorScreen(
} }
HorizontalPager( HorizontalPager(
pageCount = viewModel.bundles.size, pageCount = bundles.size,
state = pagerState, state = pagerState,
userScrollEnabled = true, userScrollEnabled = true,
pageContent = { index -> pageContent = { index ->
val patches = rememberSaveable { viewModel.bundles[index].patches } val bundle = bundles[index]
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxSize()
.fillMaxSize()
) { ) {
items( items(
items = patches["supported"]!! items = bundle.supported
) { patch -> ) { patch ->
ListItem( PatchItem(
modifier = Modifier.clickable { patch = patch,
if (viewModel.selectedPatches.contains(patch)) onOptionsDialog = vm::openOptionsDialog,
viewModel.selectedPatches.remove(patch) onToggle = {
else vm.togglePatch(patch)
viewModel.selectedPatches.add(patch)
}, },
leadingContent = { selected = vm.isSelected(patch),
Checkbox( supported = true
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)
}
}
}
) )
} }
if (patches["unsupported"]!!.isNotEmpty()) { if (bundle.unsupported.isNotEmpty()) {
item { item {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp).padding(end = 10.dp),
.fillMaxWidth()
.padding(horizontal = 14.dp)
.padding(end = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
GroupHeader("Unsupported patches", Modifier.padding(0.dp)) GroupHeader(stringResource(R.string.unsupported_patches), Modifier.padding(0.dp))
IconButton(onClick = { showUnsupportedDialog = true }) { IconButton(onClick = vm::openUnsupportedDialog) {
Icon( Icon(
Icons.Outlined.HelpOutline, Icons.Outlined.HelpOutline, stringResource(R.string.help)
stringResource(R.string.help)
) )
} }
} }
@ -180,53 +130,23 @@ fun PatchesSelectorScreen(
} }
items( items(
items = patches["unsupported"]!!, items = bundle.unsupported,
// key = { it.name } // key = { it.name }
) { patch -> ) { patch ->
PatchItem(
ListItem( patch = patch,
modifier = Modifier onOptionsDialog = vm::openOptionsDialog,
.alpha(0.5f) onToggle = {
.clickable(enabled = false) { vm.togglePatch(patch)
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
)
}, },
headlineContent = { selected = vm.isSelected(patch),
Text(patch.name) supported = allowUnsupported
},
supportingContent = {
Text(patch.description)
},
trailingContent = {
if (patch.options.isNotEmpty()) {
IconButton(onClick = { showOptionsDialog = true }, enabled = false) {
Icon(Icons.Outlined.Settings, null)
}
}
}
) )
} }
} }
})
}
)
} }
} }
} }
@ -236,10 +156,10 @@ fun UnsupportedDialog(
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) { ) {
val appVersion = "1.1.0" 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( AlertDialog(modifier = Modifier.padding(vertical = 45.dp),
modifier = Modifier.padding(vertical = 45.dp),
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
@ -247,29 +167,21 @@ fun UnsupportedDialog(
} }
}, },
title = { Text(stringResource(R.string.unsupported_app)) }, 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 @Composable
fun OptionsDialog( fun OptionsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit, onConfirm: () -> Unit
onConfirm: () -> Unit
) { ) {
AlertDialog( AlertDialog(onDismissRequest = onDismissRequest, confirmButton = {
onDismissRequest = onDismissRequest, Button(onClick = {
confirmButton = { onConfirm()
Button(onClick = { onDismissRequest()
onConfirm() }) {
onDismissRequest() Text(stringResource(R.string.apply))
}
) {
Text(stringResource(R.string.apply))
}
},
title = { Text(stringResource(R.string.options)) },
text = {
Text("You really thought these would exist?")
} }
) }, title = { Text(stringResource(R.string.options)) }, text = {
Text("You really thought these would exist?")
})
} }

View File

@ -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) }
}
}

View File

@ -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()
}
}

View File

@ -1,85 +1,61 @@
package app.revanced.manager.compose.ui.viewmodel package app.revanced.manager.compose.ui.viewmodel
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel 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() { class PatchesSelectorViewModel(packageInfo: PackageInfo, patchesRepository: PatchesRepository) :
private val patchesList = listOf( ViewModel() {
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 ", val bundlesFlow = patchesRepository.getPatchInformation().map { patches ->
options = listOf( val supported = mutableListOf<PatchInfo>()
Option( val unsupported = mutableListOf<PatchInfo>()
"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
),
).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( listOf(
Bundle( Bundle(
name = "offical", name = "official",
patches = patchesLists supported, unsupported
), )
Bundle( )
name = "extended", }
patches = patchesLists
),
Bundle(
name = "balls",
patches = patchesLists
),
)
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( data class Bundle(
val name: String, val name: String,
val patches: Map<String, List<Patch>> val supported: List<PatchInfo>,
val unsupported: List<PatchInfo>
) )
data class Patch( var showOptionsDialog by mutableStateOf(false)
val name: String, private set
val description: String, var showUnsupportedDialog by mutableStateOf(false)
val options: List<Option>, private set
val isSupported: Boolean
)
data class Option( fun dismissDialogs() {
val name: String showOptionsDialog = false
) showUnsupportedDialog = false
}
fun openOptionsDialog() {
showOptionsDialog = true
}
fun openUnsupportedDialog() {
showUnsupportedDialog = true
}
} }

View File

@ -6,120 +6,69 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import app.revanced.manager.compose.service.InstallService import app.revanced.manager.compose.service.InstallService
import app.revanced.manager.compose.service.UninstallService import app.revanced.manager.compose.service.UninstallService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue import kotlinx.parcelize.RawValue
import java.io.File import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable 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") @SuppressLint("QueryPermissionsNeeded")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object PM { 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 appList = mutableStateListOf<AppInfo>()
val supportedAppList = mutableStateListOf<AppInfo>() val supportedAppList = mutableStateListOf<AppInfo>()
suspend fun loadApps(context: Context) { suspend fun loadApps(context: Context) {
val packageManager = context.packageManager 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>() val localAppList = mutableListOf<AppInfo>()
packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map { packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map {
AppInfo( AppInfo(
it.packageName, it.packageName,
"0.69.420",
it.loadLabel(packageManager).toString(), it.loadLabel(packageManager).toString(),
it.loadIcon(packageManager) it.loadIcon(packageManager),
File("h")
) )
}.also { localAppList.addAll(it) } }.also { localAppList.addAll(it) }.also { supportedAppList.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) }
}
} }
@Parcelize @Parcelize
data class AppInfo( data class AppInfo(
val packageName: String, val packageName: String,
val versionName: String,
val label: String, val label: String,
val icon: @RawValue Drawable? = null val icon: @RawValue Drawable? = null,
val apk: File,
) : Parcelable ) : Parcelable
fun installApp(apk: File, context: Context) { fun installApp(apks: List<File>, context: Context) {
val packageInstaller = context.packageManager.packageInstaller val packageInstaller = context.packageManager.packageInstaller
val session = packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
packageInstaller.openSession(packageInstaller.createSession(sessionParams)) apks.forEach { apk -> session.writeApk(apk) }
session.writeApk(apk) session.commit(context.installIntentSender)
session.commit(context.installIntentSender) }
session.close()
} }
fun uninstallPackage(pkg: String, context: Context) { fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender) 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) { private fun PackageInstaller.Session.writeApk(apk: File) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -58,5 +58,15 @@
<string name="no_sources_set">No sources set</string> <string name="no_sources_set">No sources set</string>
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="unsupported_app">Unsupported app</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="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> </resources>