mirror of
https://github.com/revanced/revanced-manager-compose
synced 2025-02-22 19:11:08 +01:00
feat: integrate revanced patcher (#22)
This commit is contained in:
parent
2810e2e5ed
commit
437a9f1508
@ -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")
|
||||||
|
@ -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>
|
@ -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
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
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
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
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?")
|
||||||
|
})
|
||||||
}
|
}
|
@ -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
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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) {
|
||||||
|
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_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>
|
Loading…
x
Reference in New Issue
Block a user