mirror of
https://github.com/revanced/revanced-manager-compose.git
synced 2025-04-30 06:14:25 +02:00
feat: patch bundle sources system (#24)
This commit is contained in:
parent
3de4d84484
commit
91c11da363
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.8.21"
|
kotlin("plugin.serialization") version "1.8.21"
|
||||||
}
|
}
|
||||||
@ -37,6 +38,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
@ -78,6 +83,14 @@ dependencies {
|
|||||||
// KotlinX
|
// KotlinX
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
||||||
|
|
||||||
|
// Room
|
||||||
|
val roomVersion = "2.5.1"
|
||||||
|
implementation("androidx.room:room-runtime:$roomVersion")
|
||||||
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:$roomVersion")
|
||||||
|
ksp("androidx.room:room-compiler:$roomVersion")
|
||||||
|
|
||||||
|
|
||||||
// ReVanced
|
// ReVanced
|
||||||
implementation("app.revanced:revanced-patcher:7.1.0")
|
implementation("app.revanced:revanced-patcher:7.1.0")
|
||||||
|
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "b40f3b048880f3f3c9361f6d1c4aaea5",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `location` TEXT NOT NULL, `version` TEXT NOT NULL, `integrations_version` TEXT NOT NULL, PRIMARY KEY(`uid`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "location",
|
||||||
|
"columnName": "location",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionInfo.patches",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "versionInfo.integrations",
|
||||||
|
"columnName": "integrations_version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sources_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_sources_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b40f3b048880f3f3c9361f6d1c4aaea5')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
|||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.compose.domain.manager.PreferencesManager
|
import app.revanced.manager.compose.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.compose.domain.repository.BundleRepository
|
||||||
import app.revanced.manager.compose.ui.destination.Destination
|
import app.revanced.manager.compose.ui.destination.Destination
|
||||||
import app.revanced.manager.compose.ui.screen.*
|
import app.revanced.manager.compose.ui.screen.*
|
||||||
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
|
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
|
||||||
@ -22,6 +23,7 @@ import org.koin.core.parameter.parametersOf
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
|
private val bundleRepository: BundleRepository by inject()
|
||||||
private val mainScope = MainScope()
|
private val mainScope = MainScope()
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@ -30,6 +32,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
|
bundleRepository.onAppStart(this@MainActivity)
|
||||||
|
|
||||||
val context = this
|
val context = this
|
||||||
mainScope.launch(Dispatchers.IO) {
|
mainScope.launch(Dispatchers.IO) {
|
||||||
PM.loadApps(context)
|
PM.loadApps(context)
|
||||||
|
@ -18,8 +18,10 @@ class ManagerApplication : Application() {
|
|||||||
preferencesModule,
|
preferencesModule,
|
||||||
repositoryModule,
|
repositoryModule,
|
||||||
serviceModule,
|
serviceModule,
|
||||||
|
managerModule,
|
||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
|
databaseModule,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.manager.compose.data.room
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceEntity
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceDao
|
||||||
|
|
||||||
|
@Database(entities = [SourceEntity::class], version = 1)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun sourceDao(): SourceDao
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.manager.compose.data.room
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceLocation
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun locationFromString(value: String) = when(value) {
|
||||||
|
SourceLocation.Local.SENTINEL -> SourceLocation.Local
|
||||||
|
else -> SourceLocation.Remote(Url(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun locationToString(location: SourceLocation) = location.toString()
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.compose.data.room.sources
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SourceDao {
|
||||||
|
@Query("SELECT * FROM $sourcesTableName")
|
||||||
|
suspend fun all(): List<SourceEntity>
|
||||||
|
|
||||||
|
@Query("SELECT version, integrations_version FROM $sourcesTableName WHERE uid = :uid")
|
||||||
|
suspend fun getVersionById(uid: Int): VersionInfo
|
||||||
|
|
||||||
|
@Query("UPDATE $sourcesTableName SET version=:patches, integrations_version=:integrations WHERE uid=:uid")
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String, integrations: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM $sourcesTableName")
|
||||||
|
suspend fun purge()
|
||||||
|
|
||||||
|
@Query("DELETE FROM $sourcesTableName WHERE uid=:uid")
|
||||||
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun add(source: SourceEntity)
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package app.revanced.manager.compose.data.room.sources
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
const val sourcesTableName = "sources"
|
||||||
|
|
||||||
|
sealed class SourceLocation {
|
||||||
|
object Local : SourceLocation() {
|
||||||
|
const val SENTINEL = "local"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Remote(val url: Url) : SourceLocation() {
|
||||||
|
override fun toString() = url.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VersionInfo(
|
||||||
|
@ColumnInfo(name = "version") val patches: String,
|
||||||
|
@ColumnInfo(name = "integrations_version") val integrations: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(tableName = sourcesTableName, indices = [Index(value = ["name"], unique = true)])
|
||||||
|
data class SourceEntity(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "name") val name: String,
|
||||||
|
@Embedded val versionInfo: VersionInfo,
|
||||||
|
@ColumnInfo(name = "location") val location: SourceLocation,
|
||||||
|
)
|
@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.manager.compose.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import app.revanced.manager.compose.data.room.AppDatabase
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val databaseModule = module {
|
||||||
|
fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build()
|
||||||
|
|
||||||
|
single {
|
||||||
|
provideAppDatabase(androidContext())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.manager.compose.di
|
||||||
|
|
||||||
|
import app.revanced.manager.compose.domain.repository.SourceRepository
|
||||||
|
import app.revanced.manager.compose.patcher.SignerService
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val managerModule = module {
|
||||||
|
singleOf(::SignerService)
|
||||||
|
}
|
@ -2,12 +2,16 @@ package app.revanced.manager.compose.di
|
|||||||
|
|
||||||
import app.revanced.manager.compose.domain.repository.ReVancedRepository
|
import app.revanced.manager.compose.domain.repository.ReVancedRepository
|
||||||
import app.revanced.manager.compose.network.api.ManagerAPI
|
import app.revanced.manager.compose.network.api.ManagerAPI
|
||||||
import app.revanced.manager.compose.patcher.data.repository.PatchesRepository
|
import app.revanced.manager.compose.domain.repository.BundleRepository
|
||||||
|
import app.revanced.manager.compose.domain.repository.SourcePersistenceRepository
|
||||||
|
import app.revanced.manager.compose.domain.repository.SourceRepository
|
||||||
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(::ReVancedRepository)
|
singleOf(::ReVancedRepository)
|
||||||
singleOf(::ManagerAPI)
|
singleOf(::ManagerAPI)
|
||||||
singleOf(::PatchesRepository)
|
singleOf(::BundleRepository)
|
||||||
|
singleOf(::SourcePersistenceRepository)
|
||||||
|
singleOf(::SourceRepository)
|
||||||
}
|
}
|
@ -2,7 +2,6 @@ package app.revanced.manager.compose.di
|
|||||||
|
|
||||||
import app.revanced.manager.compose.network.service.HttpService
|
import app.revanced.manager.compose.network.service.HttpService
|
||||||
import app.revanced.manager.compose.network.service.ReVancedService
|
import app.revanced.manager.compose.network.service.ReVancedService
|
||||||
import app.revanced.manager.compose.patcher.SignerService
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
@ -17,5 +16,4 @@ val serviceModule = module {
|
|||||||
|
|
||||||
single { provideReVancedService(get()) }
|
single { provideReVancedService(get()) }
|
||||||
singleOf(::HttpService)
|
singleOf(::HttpService)
|
||||||
singleOf(::SignerService)
|
|
||||||
}
|
}
|
@ -1,10 +1,6 @@
|
|||||||
package app.revanced.manager.compose.di
|
package app.revanced.manager.compose.di
|
||||||
|
|
||||||
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
|
import app.revanced.manager.compose.ui.viewmodel.*
|
||||||
import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel
|
|
||||||
import app.revanced.manager.compose.ui.viewmodel.PatchesSelectorViewModel
|
|
||||||
import app.revanced.manager.compose.ui.viewmodel.SettingsViewModel
|
|
||||||
import app.revanced.manager.compose.ui.viewmodel.UpdateSettingsViewModel
|
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@ -13,11 +9,12 @@ val viewModelModule = module {
|
|||||||
viewModel {
|
viewModel {
|
||||||
PatchesSelectorViewModel(
|
PatchesSelectorViewModel(
|
||||||
packageInfo = it.get(),
|
packageInfo = it.get(),
|
||||||
patchesRepository = get()
|
bundleRepository = get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
|
viewModelOf(::SourcesScreenViewModel)
|
||||||
viewModel {
|
viewModel {
|
||||||
InstallerScreenViewModel(
|
InstallerScreenViewModel(
|
||||||
input = it.get(),
|
input = it.get(),
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package app.revanced.manager.compose.domain.repository
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import app.revanced.manager.compose.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.compose.util.launchAndRepeatWithViewLifecycle
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class BundleRepository(private val sourceRepository: SourceRepository) {
|
||||||
|
/**
|
||||||
|
* A [Flow] that emits whenever the sources change.
|
||||||
|
*
|
||||||
|
* The outer flow emits whenever the sources configuration changes.
|
||||||
|
* The inner flow emits whenever one of the bundles update.
|
||||||
|
*/
|
||||||
|
private val sourceUpdates = sourceRepository.sources.map { sources ->
|
||||||
|
sources.map { (name, source) ->
|
||||||
|
source.bundle.map { bundle ->
|
||||||
|
name to bundle
|
||||||
|
}
|
||||||
|
}.merge().buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _bundles = MutableStateFlow<Map<String, PatchBundle>>(emptyMap())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Flow] that gives you all loaded [PatchBundle]s.
|
||||||
|
* This is only synced when the app is in the foreground.
|
||||||
|
*/
|
||||||
|
val bundles = _bundles.asStateFlow()
|
||||||
|
|
||||||
|
fun onAppStart(lifecycleOwner: LifecycleOwner) {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
sourceRepository.loadSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleOwner.launchAndRepeatWithViewLifecycle {
|
||||||
|
sourceUpdates.collect { events ->
|
||||||
|
val map = HashMap<String, PatchBundle>()
|
||||||
|
_bundles.emit(map)
|
||||||
|
|
||||||
|
events.collect { (name, new) ->
|
||||||
|
map[name] = new
|
||||||
|
_bundles.emit(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package app.revanced.manager.compose.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.compose.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceEntity
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceLocation
|
||||||
|
import app.revanced.manager.compose.data.room.sources.VersionInfo
|
||||||
|
import app.revanced.manager.compose.util.apiURL
|
||||||
|
import kotlin.random.Random
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
class SourcePersistenceRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.sourceDao()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
|
||||||
|
val defaultSource = SourceEntity(
|
||||||
|
uid = generateUid(),
|
||||||
|
name = "Official",
|
||||||
|
versionInfo = VersionInfo("", ""),
|
||||||
|
location = SourceLocation.Remote(Url(apiURL))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadConfiguration(): List<SourceEntity> {
|
||||||
|
val all = dao.all()
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
dao.add(defaultSource)
|
||||||
|
return listOf(defaultSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clear() = dao.purge()
|
||||||
|
|
||||||
|
suspend fun create(name: String, location: SourceLocation): Int {
|
||||||
|
val uid = generateUid()
|
||||||
|
dao.add(
|
||||||
|
SourceEntity(
|
||||||
|
uid = uid,
|
||||||
|
name = name,
|
||||||
|
versionInfo = VersionInfo("", ""),
|
||||||
|
location = location,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||||
|
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
|
||||||
|
dao.updateVersion(uid, patches, integrations)
|
||||||
|
|
||||||
|
suspend fun getVersion(id: Int) = dao.getVersionById(id).let { it.patches to it.integrations }
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package app.revanced.manager.compose.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceEntity
|
||||||
|
import app.revanced.manager.compose.data.room.sources.SourceLocation
|
||||||
|
import app.revanced.manager.compose.domain.sources.RemoteSource
|
||||||
|
import app.revanced.manager.compose.domain.sources.LocalSource
|
||||||
|
import app.revanced.manager.compose.domain.sources.Source
|
||||||
|
import app.revanced.manager.compose.util.tag
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) {
|
||||||
|
private val sourcesDir = app.dataDir.resolve("sources").also { it.mkdirs() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the directory of the [Source] with the specified [uid], creating it if needed.
|
||||||
|
*/
|
||||||
|
private fun directoryOf(uid: Int) = sourcesDir.resolve(uid.toString()).also { it.mkdirs() }
|
||||||
|
|
||||||
|
private fun SourceEntity.load(dir: File) = when (location) {
|
||||||
|
is SourceLocation.Local -> LocalSource(uid, dir)
|
||||||
|
is SourceLocation.Remote -> RemoteSource(uid, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSources() = withContext(Dispatchers.Default) {
|
||||||
|
val sourcesConfig = persistenceRepo.loadConfiguration().onEach {
|
||||||
|
Log.d(tag, "Source: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = sourcesConfig.associate {
|
||||||
|
val dir = directoryOf(it.uid)
|
||||||
|
val source = it.load(dir)
|
||||||
|
|
||||||
|
it.name to source
|
||||||
|
}
|
||||||
|
|
||||||
|
_sources.emit(sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resetConfig() = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.clear()
|
||||||
|
_sources.emit(emptyMap())
|
||||||
|
sourcesDir.apply {
|
||||||
|
delete()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(source: Source) = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.delete(source.id)
|
||||||
|
directoryOf(source.id).delete()
|
||||||
|
|
||||||
|
_sources.update {
|
||||||
|
it.filterValues { value ->
|
||||||
|
value.id != source.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSource(name: String, source: Source) =
|
||||||
|
_sources.update { it.toMutableMap().apply { put(name, source) } }
|
||||||
|
|
||||||
|
suspend fun createLocalSource(name: String, patches: InputStream, integrations: InputStream?) {
|
||||||
|
val id = persistenceRepo.create(name, SourceLocation.Local)
|
||||||
|
val source = LocalSource(id, directoryOf(id))
|
||||||
|
|
||||||
|
addSource(name, source)
|
||||||
|
|
||||||
|
source.replace(patches, integrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRemoteSource(name: String, apiUrl: Url) {
|
||||||
|
val id = persistenceRepo.create(name, SourceLocation.Remote(apiUrl))
|
||||||
|
addSource(name, RemoteSource(id, directoryOf(id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _sources: MutableStateFlow<Map<String, Source>> = MutableStateFlow(emptyMap())
|
||||||
|
val sources = _sources.asStateFlow()
|
||||||
|
|
||||||
|
suspend fun redownloadRemoteSources() =
|
||||||
|
sources.value.values.filterIsInstance<RemoteSource>().forEach { it.downloadLatest() }
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package app.revanced.manager.compose.domain.sources
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
|
||||||
|
class LocalSource(id: Int, directory: File) : Source(id, directory) {
|
||||||
|
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
patches?.let {
|
||||||
|
Files.copy(it, patchesJar.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
integrations?.let {
|
||||||
|
Files.copy(it, this@LocalSource.integrations.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_bundle.emit(loadBundle { throw it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.manager.compose.domain.sources
|
||||||
|
|
||||||
|
import app.revanced.manager.compose.network.api.ManagerAPI
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class RemoteSource(id: Int, directory: File) : Source(id, directory) {
|
||||||
|
private val api: ManagerAPI by inject()
|
||||||
|
suspend fun downloadLatest() = withContext(Dispatchers.IO) {
|
||||||
|
api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) ->
|
||||||
|
saveVersion(patchesVer, integrationsVer)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_bundle.emit(loadBundle { err -> throw err })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update() = withContext(Dispatchers.IO) {
|
||||||
|
val currentVersion = getVersion()
|
||||||
|
if (!hasInstalled() || currentVersion != api.getLatestBundleVersion()) {
|
||||||
|
downloadLatest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.manager.compose.domain.sources
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.compose.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.compose.domain.repository.SourcePersistenceRepository
|
||||||
|
import app.revanced.manager.compose.util.tag
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchBundle] source.
|
||||||
|
*/
|
||||||
|
sealed class Source(val id: Int, directory: File) : KoinComponent {
|
||||||
|
private val configRepository: SourcePersistenceRepository by inject()
|
||||||
|
protected companion object {
|
||||||
|
/**
|
||||||
|
* A placeholder [PatchBundle].
|
||||||
|
*/
|
||||||
|
val emptyPatchBundle = PatchBundle(emptyList(), null)
|
||||||
|
fun logError(err: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load bundle", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val patchesJar = directory.resolve("patches.jar")
|
||||||
|
protected val integrations = directory.resolve("integrations.apk")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the bundle has been downloaded to local storage.
|
||||||
|
*/
|
||||||
|
fun hasInstalled() = patchesJar.exists()
|
||||||
|
|
||||||
|
protected suspend fun getVersion() = configRepository.getVersion(id)
|
||||||
|
protected suspend fun saveVersion(patches: String, integrations: String) =
|
||||||
|
configRepository.updateVersion(id, patches, integrations)
|
||||||
|
|
||||||
|
// TODO: Communicate failure states better.
|
||||||
|
protected fun loadBundle(onFail: (Throwable) -> Unit = ::logError) = if (!hasInstalled()) emptyPatchBundle
|
||||||
|
else try {
|
||||||
|
PatchBundle(patchesJar, integrations.takeIf { it.exists() })
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
onFail(err)
|
||||||
|
emptyPatchBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val _bundle = MutableStateFlow(loadBundle())
|
||||||
|
val bundle = _bundle.asStateFlow()
|
||||||
|
}
|
@ -7,11 +7,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import app.revanced.manager.compose.domain.repository.ReVancedRepository
|
import app.revanced.manager.compose.domain.repository.ReVancedRepository
|
||||||
import app.revanced.manager.compose.util.ghIntegrations
|
import app.revanced.manager.compose.util.*
|
||||||
import app.revanced.manager.compose.util.ghManager
|
|
||||||
import app.revanced.manager.compose.util.ghPatches
|
|
||||||
import app.revanced.manager.compose.util.tag
|
|
||||||
import app.revanced.manager.compose.util.toast
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@ -40,36 +36,19 @@ class ManagerAPI(
|
|||||||
downloadProgress = null
|
downloadProgress = null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadPatchBundle(): File? {
|
private suspend fun patchesAsset() = revancedRepository.findAsset(ghPatches, ".jar")
|
||||||
try {
|
private suspend fun integrationsAsset() = revancedRepository.findAsset(ghIntegrations, ".apk")
|
||||||
val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl
|
|
||||||
val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() }
|
|
||||||
.resolve("patchbundle.jar")
|
|
||||||
downloadAsset(downloadUrl, patchesFile)
|
|
||||||
|
|
||||||
return patchesFile
|
suspend fun getLatestBundleVersion() = patchesAsset().version to integrationsAsset().version
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to download patch bundle", e)
|
|
||||||
app.toast("Failed to download patch bundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
suspend fun downloadBundle(patchBundle: File, integrations: File): Pair<String, String> {
|
||||||
}
|
val patchBundleAsset = patchesAsset()
|
||||||
|
val integrationsAsset = integrationsAsset()
|
||||||
|
|
||||||
suspend fun downloadIntegrations(): File? {
|
downloadAsset(patchBundleAsset.downloadUrl, patchBundle)
|
||||||
try {
|
downloadAsset(integrationsAsset.downloadUrl, integrations)
|
||||||
val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl
|
|
||||||
val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() }
|
|
||||||
.resolve("integrations.apk")
|
|
||||||
downloadAsset(downloadUrl, integrationsFile)
|
|
||||||
|
|
||||||
return integrationsFile
|
return patchBundleAsset.version to integrationsAsset.version
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to download integrations", e)
|
|
||||||
app.toast("Failed to download integrations")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadManager(): File? {
|
suspend fun downloadManager(): File? {
|
||||||
@ -87,4 +66,5 @@ class ManagerAPI(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MissingAssetException : Exception()
|
class MissingAssetException : Exception()
|
@ -5,7 +5,7 @@ import app.revanced.manager.compose.network.dto.Assets
|
|||||||
import app.revanced.manager.compose.network.dto.ReVancedReleases
|
import app.revanced.manager.compose.network.dto.ReVancedReleases
|
||||||
import app.revanced.manager.compose.network.dto.ReVancedRepositories
|
import app.revanced.manager.compose.network.dto.ReVancedRepositories
|
||||||
import app.revanced.manager.compose.network.utils.APIResponse
|
import app.revanced.manager.compose.network.utils.APIResponse
|
||||||
import app.revanced.manager.compose.network.utils.getOrNull
|
import app.revanced.manager.compose.network.utils.getOrThrow
|
||||||
import app.revanced.manager.compose.util.apiURL
|
import app.revanced.manager.compose.util.apiURL
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -31,10 +31,12 @@ class ReVancedService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findAsset(repo: String, file: String): Assets {
|
suspend fun findAsset(repo: String, file: String): Assets {
|
||||||
val releases = getAssets().getOrNull() ?: throw Exception("Cannot retrieve assets")
|
val releases = getAssets().getOrThrow()
|
||||||
|
|
||||||
val asset = releases.tools.find { asset ->
|
val asset = releases.tools.find { asset ->
|
||||||
(asset.name.contains(file) && asset.repository.contains(repo))
|
(asset.name.contains(file) && asset.repository.contains(repo))
|
||||||
} ?: throw MissingAssetException()
|
} ?: throw MissingAssetException()
|
||||||
|
|
||||||
return Assets(asset.repository, asset.version, asset.timestamp, asset.name,asset.size, asset.downloadUrl, asset.content_type)
|
return Assets(asset.repository, asset.version, asset.timestamp, asset.name,asset.size, asset.downloadUrl, asset.content_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ sealed interface APIResponse<T> {
|
|||||||
data class Failure<T>(val error: APIFailure) : APIResponse<T>
|
data class Failure<T>(val error: APIFailure) : APIResponse<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
|
class APIError(code: HttpStatusCode, body: String?) : Exception("HTTP Code $code, Body: $body")
|
||||||
|
|
||||||
class APIFailure(error: Throwable, body: String?) : Error(body, error)
|
class APIFailure(error: Throwable, body: String?) : Exception(body ?: error.message, error)
|
||||||
|
|
||||||
inline fun <T, R> APIResponse<T>.fold(
|
inline fun <T, R> APIResponse<T>.fold(
|
||||||
success: (T) -> R,
|
success: (T) -> R,
|
||||||
@ -32,7 +32,7 @@ inline fun <T, R> APIResponse<T>.fold(
|
|||||||
|
|
||||||
inline fun <T, R> APIResponse<T>.fold(
|
inline fun <T, R> APIResponse<T>.fold(
|
||||||
success: (T) -> R,
|
success: (T) -> R,
|
||||||
fail: (Error) -> R,
|
fail: (Exception) -> R,
|
||||||
): R {
|
): R {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is APIResponse.Success -> success(data)
|
is APIResponse.Success -> success(data)
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
package app.revanced.manager.compose.patcher.data.repository
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
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) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the [PatchBundle] if needed.
|
|
||||||
*/
|
|
||||||
private suspend fun loadBundle() = bundle ?: PatchBundle(
|
|
||||||
managerAPI.downloadPatchBundle()!!.absolutePath,
|
|
||||||
managerAPI.downloadIntegrations()
|
|
||||||
).also {
|
|
||||||
loadNewBundle(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadPatchClassesFiltered(packageName: String) =
|
|
||||||
loadBundle().loadPatchesFiltered(packageName)
|
|
||||||
|
|
||||||
fun getPatchInformation() = patchInformation.asSharedFlow().also {
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
loadBundle()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("revanced-manager", "Failed to download bundle", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getIntegrations() = listOfNotNull(loadBundle().integrations)
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.compose.patcher.data
|
package app.revanced.manager.compose.patcher.patch
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import app.revanced.manager.compose.patcher.PatchClass
|
import app.revanced.manager.compose.patcher.PatchClass
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||||
@ -8,17 +9,21 @@ import dalvik.system.PathClassLoader
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) {
|
class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) {
|
||||||
constructor(bundleJar: String, integrations: File?) : this(
|
constructor(bundleJar: File, integrations: File?) : this(
|
||||||
object : Iterable<PatchClass> {
|
object : Iterable<PatchClass> {
|
||||||
private val bundle = PatchBundle.Dex(
|
private val bundle = PatchBundle.Dex(
|
||||||
bundleJar,
|
bundleJar.absolutePath,
|
||||||
PathClassLoader(bundleJar, Patcher::class.java.classLoader)
|
PathClassLoader(bundleJar.absolutePath, Patcher::class.java.classLoader)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun iterator() = bundle.loadPatches().iterator()
|
override fun iterator() = bundle.loadPatches().iterator()
|
||||||
},
|
},
|
||||||
integrations
|
integrations
|
||||||
)
|
) {
|
||||||
|
Log.d("revanced-manager", "Loaded patch bundle: $bundleJar")
|
||||||
|
}
|
||||||
|
|
||||||
|
val patches = loadAllPatches().map(::PatchInfo)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return A list of patches that are compatible with this Apk.
|
* @return A list of patches that are compatible with this Apk.
|
@ -4,12 +4,13 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import app.revanced.manager.compose.domain.repository.BundleRepository
|
||||||
import app.revanced.manager.compose.patcher.Session
|
import app.revanced.manager.compose.patcher.Session
|
||||||
import app.revanced.manager.compose.patcher.aapt.Aapt
|
import app.revanced.manager.compose.patcher.aapt.Aapt
|
||||||
import app.revanced.manager.compose.patcher.data.repository.PatchesRepository
|
import app.revanced.manager.compose.util.PatchesSelection
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -19,13 +20,13 @@ import java.io.FileNotFoundException
|
|||||||
// TODO: setup wakelock + notification so android doesn't murder us.
|
// TODO: setup wakelock + notification so android doesn't murder us.
|
||||||
class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters),
|
class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters),
|
||||||
KoinComponent {
|
KoinComponent {
|
||||||
private val patchesRepository: PatchesRepository by inject()
|
private val bundleRepository: BundleRepository by inject()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Args(
|
data class Args(
|
||||||
val input: String,
|
val input: String,
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: List<String>,
|
val selectedPatches: PatchesSelection,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val packageVersion: String
|
val packageVersion: String
|
||||||
)
|
)
|
||||||
@ -46,12 +47,17 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW
|
|||||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||||
|
|
||||||
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!)
|
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!)
|
||||||
val selected = args.selectedPatches.toSet()
|
|
||||||
|
|
||||||
val patchList = patchesRepository.loadPatchClassesFiltered(args.packageName)
|
val bundles = bundleRepository.bundles.value
|
||||||
.filter { selected.contains(it.patchName) }
|
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||||
|
|
||||||
val progressManager = PatcherProgressManager(applicationContext, args.selectedPatches)
|
val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->
|
||||||
|
bundles[bundleName]?.loadPatchesFiltered(args.packageName)?.filter { selected.contains(it.patchName) }
|
||||||
|
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressManager =
|
||||||
|
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { (_, selected) -> selected })
|
||||||
|
|
||||||
suspend fun updateProgress(progress: Progress) {
|
suspend fun updateProgress(progress: Progress) {
|
||||||
progressManager.handle(progress)
|
progressManager.handle(progress)
|
||||||
@ -64,7 +70,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW
|
|||||||
Session(applicationContext.cacheDir.path, frameworkPath, aaptPath, File(args.input)) {
|
Session(applicationContext.cacheDir.path, frameworkPath, aaptPath, File(args.input)) {
|
||||||
updateProgress(it)
|
updateProgress(it)
|
||||||
}.use { session ->
|
}.use { session ->
|
||||||
session.run(File(args.output), patchList, patchesRepository.getIntegrations())
|
session.run(File(args.output), patchList, integrations)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("revanced-worker", "Patching succeeded")
|
Log.i("revanced-worker", "Patching succeeded")
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced.manager.compose.ui.component
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileSelector(mime: String, onSelect: (Uri) -> Unit, content: @Composable () -> Unit) {
|
||||||
|
val activityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
|
uri?.let(onSelect)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
activityLauncher.launch(mime)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.manager.compose.ui.component.sources
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.compose.ui.component.FileSelector
|
||||||
|
import app.revanced.manager.compose.util.APK_MIMETYPE
|
||||||
|
import app.revanced.manager.compose.util.JAR_MIMETYPE
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LocalBundleSelectors(onPatchesSelection: (Uri) -> Unit, onIntegrationsSelection: (Uri) -> Unit) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
FileSelector(
|
||||||
|
mime = JAR_MIMETYPE,
|
||||||
|
onSelect = onPatchesSelection
|
||||||
|
) {
|
||||||
|
Text("Patches")
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSelector(
|
||||||
|
mime = APK_MIMETYPE,
|
||||||
|
onSelect = onIntegrationsSelection
|
||||||
|
) {
|
||||||
|
Text("Integrations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package app.revanced.manager.compose.ui.component.sources
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import app.revanced.manager.compose.R
|
||||||
|
import app.revanced.manager.compose.util.parseUrlOrNull
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewSourceDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onRemoteSubmit: (String, Url) -> Unit,
|
||||||
|
onLocalSubmit: (String, Uri, Uri?) -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnBackPress = true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Column {
|
||||||
|
IconButton(onClick = onDismissRequest) {
|
||||||
|
Icon(Icons.Filled.Cancel, stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
var isLocal by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
|
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
|
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
var name by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
val inputsAreValid by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val nameSize = name.length
|
||||||
|
|
||||||
|
nameSize in 4..19 && if (isLocal) patchBundle != null else {
|
||||||
|
remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isLocal) {
|
||||||
|
integrations = null
|
||||||
|
patchBundle = null
|
||||||
|
remoteUrl = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = if (isLocal) "Local" else "Remote")
|
||||||
|
Switch(checked = isLocal, onCheckedChange = { isLocal = it })
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = {
|
||||||
|
Text("Name")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
|
LocalBundleSelectors(
|
||||||
|
onPatchesSelection = { patchBundle = it },
|
||||||
|
onIntegrationsSelection = { integrations = it },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TextField(
|
||||||
|
value = remoteUrl,
|
||||||
|
onValueChange = { remoteUrl = it },
|
||||||
|
label = {
|
||||||
|
Text("API Url")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (isLocal) {
|
||||||
|
onLocalSubmit(name, patchBundle!!, integrations)
|
||||||
|
} else {
|
||||||
|
onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = inputsAreValid
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,146 @@
|
|||||||
|
package app.revanced.manager.compose.ui.component.sources
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.compose.R
|
||||||
|
import app.revanced.manager.compose.domain.sources.LocalSource
|
||||||
|
import app.revanced.manager.compose.domain.sources.RemoteSource
|
||||||
|
import app.revanced.manager.compose.domain.sources.Source
|
||||||
|
import app.revanced.manager.compose.ui.viewmodel.SourcesScreenViewModel
|
||||||
|
import app.revanced.manager.compose.util.uiSafe
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SourceItem(name: String, source: Source, onDelete: () -> Unit) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var sheetActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val bundle by source.bundle.collectAsStateWithLifecycle()
|
||||||
|
val patchCount = bundle.patches.size
|
||||||
|
val padding = PaddingValues(16.dp, 0.dp)
|
||||||
|
|
||||||
|
if (sheetActive) {
|
||||||
|
val modalSheetState = rememberModalBottomSheetState(
|
||||||
|
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetState = modalSheetState,
|
||||||
|
onDismissRequest = { sheetActive = false }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
when (source) {
|
||||||
|
is RemoteSource -> RemoteSourceItem(source)
|
||||||
|
is LocalSource -> LocalSourceItem(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
modalSheetState.hide()
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Delete this source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(64.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
sheetActive = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RemoteSourceItem(source: RemoteSource) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val androidContext = LocalContext.current
|
||||||
|
Text(text = "(api url here)")
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
uiSafe(androidContext, R.string.source_download_fail, SourcesScreenViewModel.failLogMsg) {
|
||||||
|
source.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = "Check for updates")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LocalSourceItem(source: LocalSource) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val androidContext = LocalContext.current
|
||||||
|
val resolver = remember { androidContext.contentResolver!! }
|
||||||
|
|
||||||
|
fun loadAndReplace(uri: Uri, @StringRes toastMsg: Int, errorLogMsg: String, callback: suspend (InputStream) -> Unit) = coroutineScope.launch {
|
||||||
|
uiSafe(androidContext, toastMsg, errorLogMsg) {
|
||||||
|
resolver.openInputStream(uri)!!.use {
|
||||||
|
callback(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalBundleSelectors(
|
||||||
|
onPatchesSelection = { uri ->
|
||||||
|
loadAndReplace(uri, R.string.source_replace_fail, "Failed to replace patch bundle") {
|
||||||
|
source.replace(it, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIntegrationsSelection = { uri ->
|
||||||
|
loadAndReplace(uri, R.string.source_replace_integrations_fail, "Failed to replace integrations") {
|
||||||
|
source.replace(null, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -2,6 +2,7 @@ package app.revanced.manager.compose.ui.destination
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.compose.util.PackageInfo
|
import app.revanced.manager.compose.util.PackageInfo
|
||||||
|
import app.revanced.manager.compose.util.PatchesSelection
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
sealed interface Destination : Parcelable {
|
sealed interface Destination : Parcelable {
|
||||||
@ -19,5 +20,5 @@ sealed interface Destination : Parcelable {
|
|||||||
data class PatchesSelector(val input: PackageInfo) : Destination
|
data class PatchesSelector(val input: PackageInfo) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Installer(val input: PackageInfo, val selectedPatches: List<String>) : Destination
|
data class Installer(val input: PackageInfo, val selectedPatches: PatchesSelection) : Destination
|
||||||
}
|
}
|
@ -26,6 +26,7 @@ import app.revanced.manager.compose.patcher.patch.PatchInfo
|
|||||||
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.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.compose.ui.viewmodel.PatchesSelectorViewModel
|
||||||
|
import app.revanced.manager.compose.util.PatchesSelection
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
const val allowUnsupported = false
|
const val allowUnsupported = false
|
||||||
@ -33,7 +34,7 @@ const val allowUnsupported = false
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
startPatching: (List<String>) -> Unit, onBackClick: () -> Unit, vm: PatchesSelectorViewModel
|
startPatching: (PatchesSelection) -> Unit, onBackClick: () -> Unit, vm: PatchesSelectorViewModel
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@ -56,7 +57,7 @@ fun PatchesSelectorScreen(
|
|||||||
}, floatingActionButton = {
|
}, floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(text = { Text(stringResource(R.string.patch)) },
|
ExtendedFloatingActionButton(text = { Text(stringResource(R.string.patch)) },
|
||||||
icon = { Icon(Icons.Default.Build, null) },
|
icon = { Icon(Icons.Default.Build, null) },
|
||||||
onClick = { startPatching(vm.selectedPatches.toList()) })
|
onClick = { startPatching(vm.generateSelection()) })
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
Column(Modifier.fillMaxSize().padding(paddingValues)) {
|
Column(Modifier.fillMaxSize().padding(paddingValues)) {
|
||||||
TabRow(
|
TabRow(
|
||||||
@ -80,26 +81,26 @@ fun PatchesSelectorScreen(
|
|||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
pageContent = { index ->
|
pageContent = { index ->
|
||||||
|
|
||||||
val bundle = bundles[index]
|
val (bundleName, supportedPatches, unsupportedPatches) = bundles[index]
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = bundle.supported
|
items = supportedPatches
|
||||||
) { patch ->
|
) { patch ->
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch = patch,
|
patch = patch,
|
||||||
onOptionsDialog = vm::openOptionsDialog,
|
onOptionsDialog = vm::openOptionsDialog,
|
||||||
onToggle = {
|
onToggle = {
|
||||||
vm.togglePatch(patch)
|
vm.togglePatch(bundleName, patch)
|
||||||
},
|
},
|
||||||
selected = vm.isSelected(patch),
|
selected = vm.isSelected(bundleName, patch),
|
||||||
supported = true
|
supported = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bundle.unsupported.isNotEmpty()) {
|
if (unsupportedPatches.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp).padding(end = 10.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp).padding(end = 10.dp),
|
||||||
@ -116,16 +117,16 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = bundle.unsupported,
|
items = unsupportedPatches,
|
||||||
// key = { it.name }
|
// key = { it.name }
|
||||||
) { patch ->
|
) { patch ->
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch = patch,
|
patch = patch,
|
||||||
onOptionsDialog = vm::openOptionsDialog,
|
onOptionsDialog = vm::openOptionsDialog,
|
||||||
onToggle = {
|
onToggle = {
|
||||||
vm.togglePatch(patch)
|
vm.togglePatch(bundleName, patch)
|
||||||
},
|
},
|
||||||
selected = vm.isSelected(patch),
|
selected = vm.isSelected(bundleName, patch),
|
||||||
supported = allowUnsupported
|
supported = allowUnsupported
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,66 @@
|
|||||||
package app.revanced.manager.compose.ui.screen
|
package app.revanced.manager.compose.ui.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.compose.R
|
import app.revanced.manager.compose.R
|
||||||
|
import app.revanced.manager.compose.ui.component.sources.NewSourceDialog
|
||||||
|
import app.revanced.manager.compose.ui.component.sources.SourceItem
|
||||||
|
import app.revanced.manager.compose.ui.viewmodel.SourcesScreenViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen() {
|
fun SourcesScreen(vm: SourcesScreenViewModel = getViewModel()) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
var showNewSourceDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
Text(
|
val scope = rememberCoroutineScope()
|
||||||
text = stringResource(R.string.no_sources_set),
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
val sources by vm.sources.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
if (showNewSourceDialog) NewSourceDialog(
|
||||||
|
onDismissRequest = { showNewSourceDialog = false },
|
||||||
|
onLocalSubmit = { name, patches, integrations ->
|
||||||
|
showNewSourceDialog = false
|
||||||
|
scope.launch {
|
||||||
|
vm.addLocal(name, patches, integrations)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemoteSubmit = { name, url ->
|
||||||
|
showNewSourceDialog = false
|
||||||
|
scope.launch {
|
||||||
|
vm.addRemote(name, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
sources.forEach { (name, source) ->
|
||||||
|
SourceItem(
|
||||||
|
name = name,
|
||||||
|
source = source,
|
||||||
|
onDelete = {
|
||||||
|
vm.deleteSource(source)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button(onClick = vm::redownloadAllSources) {
|
||||||
|
Text(stringResource(R.string.reload_sources))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = { showNewSourceDialog = true }) {
|
||||||
|
Text("Create new source")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = vm::deleteAllSources) {
|
||||||
|
Text("Reset everything.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -23,6 +23,7 @@ import app.revanced.manager.compose.service.InstallService
|
|||||||
import app.revanced.manager.compose.service.UninstallService
|
import app.revanced.manager.compose.service.UninstallService
|
||||||
import app.revanced.manager.compose.util.PM
|
import app.revanced.manager.compose.util.PM
|
||||||
import app.revanced.manager.compose.util.PackageInfo
|
import app.revanced.manager.compose.util.PackageInfo
|
||||||
|
import app.revanced.manager.compose.util.PatchesSelection
|
||||||
import app.revanced.manager.compose.util.toast
|
import app.revanced.manager.compose.util.toast
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -31,11 +32,15 @@ import java.nio.file.Files
|
|||||||
|
|
||||||
class InstallerScreenViewModel(
|
class InstallerScreenViewModel(
|
||||||
input: PackageInfo,
|
input: PackageInfo,
|
||||||
selectedPatches: List<String>,
|
selectedPatches: PatchesSelection,
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val signerService: SignerService
|
private val signerService: SignerService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var stepGroups by mutableStateOf<List<StepGroup>>(PatcherProgressManager.generateGroupsList(app, selectedPatches))
|
var stepGroups by mutableStateOf<List<StepGroup>>(
|
||||||
|
PatcherProgressManager.generateGroupsList(
|
||||||
|
app,
|
||||||
|
selectedPatches.flatMap { (_, selected) -> selected })
|
||||||
|
)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val packageName = input.packageName
|
val packageName = input.packageName
|
||||||
@ -55,7 +60,6 @@ class InstallerScreenViewModel(
|
|||||||
|
|
||||||
val canInstall by derivedStateOf { patcherStatus == true && !isInstalling }
|
val canInstall by derivedStateOf { patcherStatus == true && !isInstalling }
|
||||||
|
|
||||||
|
|
||||||
private val patcherWorker =
|
private val patcherWorker =
|
||||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData(
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
package app.revanced.manager.compose.ui.viewmodel
|
package app.revanced.manager.compose.ui.viewmodel
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import app.revanced.manager.compose.patcher.data.repository.PatchesRepository
|
import app.revanced.manager.compose.domain.repository.BundleRepository
|
||||||
import app.revanced.manager.compose.patcher.patch.PatchInfo
|
import app.revanced.manager.compose.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.compose.util.PackageInfo
|
import app.revanced.manager.compose.util.PackageInfo
|
||||||
|
import app.revanced.manager.compose.util.PatchesSelection
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class PatchesSelectorViewModel(packageInfo: PackageInfo, patchesRepository: PatchesRepository) :
|
class PatchesSelectorViewModel(packageInfo: PackageInfo, bundleRepository: BundleRepository) : ViewModel() {
|
||||||
ViewModel() {
|
val bundlesFlow = bundleRepository.bundles.map { bundles ->
|
||||||
val bundlesFlow = patchesRepository.getPatchInformation().map { patches ->
|
bundles.mapValues { (_, bundle) -> bundle.patches }.map { (name, patches) ->
|
||||||
val supported = mutableListOf<PatchInfo>()
|
val supported = mutableListOf<PatchInfo>()
|
||||||
val unsupported = mutableListOf<PatchInfo>()
|
val unsupported = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
@ -22,26 +20,25 @@ class PatchesSelectorViewModel(packageInfo: PackageInfo, patchesRepository: Patc
|
|||||||
targetList.add(it)
|
targetList.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
Bundle(name, supported, unsupported)
|
||||||
Bundle(
|
}
|
||||||
name = "official",
|
|
||||||
supported, unsupported
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedPatches = mutableStateListOf<String>()
|
private val selectedPatches = mutableStateListOf<Pair<String, String>>()
|
||||||
|
fun isSelected(bundle: String, patch: PatchInfo) = selectedPatches.contains(bundle to patch.name)
|
||||||
|
fun togglePatch(bundle: String, patch: PatchInfo) {
|
||||||
|
val pair = bundle to patch.name
|
||||||
|
if (isSelected(bundle, patch)) selectedPatches.remove(pair) else selectedPatches.add(pair)
|
||||||
|
}
|
||||||
|
|
||||||
fun isSelected(patch: PatchInfo) = selectedPatches.contains(patch.name)
|
fun generateSelection(): PatchesSelection = HashMap<String, MutableList<String>>().apply {
|
||||||
fun togglePatch(patch: PatchInfo) {
|
selectedPatches.forEach { (bundleName, patchName) ->
|
||||||
val name = patch.name
|
this.getOrPut(bundleName, ::mutableListOf).add(patchName)
|
||||||
if (isSelected(patch)) selectedPatches.remove(name) else selectedPatches.add(patch.name)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Bundle(
|
data class Bundle(
|
||||||
val name: String,
|
val name: String, val supported: List<PatchInfo>, val unsupported: List<PatchInfo>
|
||||||
val supported: List<PatchInfo>,
|
|
||||||
val unsupported: List<PatchInfo>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var showOptionsDialog by mutableStateOf(false)
|
var showOptionsDialog by mutableStateOf(false)
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package app.revanced.manager.compose.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.compose.R
|
||||||
|
import app.revanced.manager.compose.domain.sources.Source
|
||||||
|
import app.revanced.manager.compose.domain.repository.SourceRepository
|
||||||
|
import app.revanced.manager.compose.util.uiSafe
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SourcesScreenViewModel(private val app: Application, private val sourceRepository: SourceRepository) : ViewModel() {
|
||||||
|
val sources = sourceRepository.sources
|
||||||
|
private val contentResolver: ContentResolver = app.contentResolver
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val failLogMsg = "Failed to update patch bundle(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun redownloadAllSources() = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.source_download_fail, failLogMsg) {
|
||||||
|
sourceRepository.redownloadRemoteSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addLocal(name: String, patchBundle: Uri, integrations: Uri?) {
|
||||||
|
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
|
||||||
|
val integrationsStream = integrations?.let { contentResolver.openInputStream(it) }
|
||||||
|
try {
|
||||||
|
sourceRepository.createLocalSource(name, patchesStream, integrationsStream)
|
||||||
|
} finally {
|
||||||
|
integrationsStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addRemote(name: String, apiUrl: Url) = sourceRepository.createRemoteSource(name, apiUrl)
|
||||||
|
|
||||||
|
fun deleteSource(source: Source) = viewModelScope.launch { sourceRepository.remove(source) }
|
||||||
|
|
||||||
|
fun deleteAllSources() = viewModelScope.launch {
|
||||||
|
sourceRepository.resetConfig()
|
||||||
|
}
|
||||||
|
}
|
@ -9,3 +9,6 @@ const val ghManager = "$team/revanced-manager"
|
|||||||
const val ghIntegrations = "$team/revanced-integrations"
|
const val ghIntegrations = "$team/revanced-integrations"
|
||||||
const val tag = "ReVanced Manager"
|
const val tag = "ReVanced Manager"
|
||||||
const val apiURL = "https://releases.revanced.app"
|
const val apiURL = "https://releases.revanced.app"
|
||||||
|
|
||||||
|
const val JAR_MIMETYPE = "application/java-archive"
|
||||||
|
const val APK_MIMETYPE = "application/vnd.android.package-archive"
|
@ -4,10 +4,19 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
const val APK_MIMETYPE = "application/vnd.android.package-archive"
|
typealias PatchesSelection = Map<String, List<String>>
|
||||||
|
|
||||||
fun Context.openUrl(url: String) {
|
fun Context.openUrl(url: String) {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||||
@ -26,3 +35,38 @@ fun Context.loadIcon(string: String): Drawable? {
|
|||||||
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
Toast.makeText(this, string, duration).show()
|
Toast.makeText(this, string, duration).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.parseUrlOrNull() = try {
|
||||||
|
Url(this)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely perform an operation that may fail to avoid crashing the app.
|
||||||
|
* If [block] fails, the error will be logged and a toast will be shown to the user to inform them that the action failed.
|
||||||
|
*
|
||||||
|
* @param context The android [Context].
|
||||||
|
* @param toastMsg The toast message to show if [block] throws.
|
||||||
|
* @param logMsg The log message.
|
||||||
|
* @param block The code to execute.
|
||||||
|
*/
|
||||||
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (error: Exception) {
|
||||||
|
context.toast(context.getString(toastMsg, error.message ?: error.cause?.message ?: error::class.simpleName))
|
||||||
|
Log.e(tag, logMsg, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
|
||||||
|
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
crossinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.repeatOnLifecycle(minActiveState) {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
app/src/main/res/values/plurals.xml
Normal file
7
app/src/main/res/values/plurals.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<plurals name="patches_count">
|
||||||
|
<item quantity="one">%d Patch</item>
|
||||||
|
<item quantity="other">%d Patches</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
@ -55,7 +55,10 @@
|
|||||||
<string name="storage">Storage</string>
|
<string name="storage">Storage</string>
|
||||||
<string name="tab_apps">Apps</string>
|
<string name="tab_apps">Apps</string>
|
||||||
<string name="tab_sources">Sources</string>
|
<string name="tab_sources">Sources</string>
|
||||||
<string name="no_sources_set">No sources set</string>
|
<string name="reload_sources">Reload all sources</string>
|
||||||
|
<string name="source_download_fail">Failed to download patch bundle: %s</string>
|
||||||
|
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
|
||||||
|
<string name="source_replace_integrations_fail">Failed to update integrations: %s</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="unsupported_patches">Unsupported patches</string>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.0.1" apply false
|
id("com.android.application") version "8.0.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.8.21" apply false
|
id("org.jetbrains.kotlin.android") version "1.8.21" apply false
|
||||||
|
id("com.google.devtools.ksp") version "1.8.21-1.0.11" apply false
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user