refactor: code cleanup and api refactor

This commit is contained in:
Canny 2022-11-10 21:34:22 +03:00 committed by GitHub
parent 0448a1dc87
commit e4fe4a6a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 793 additions and 775 deletions

View File

@ -25,11 +25,6 @@ android {
compileSdk = 33 compileSdk = 33
buildToolsVersion = "33.0.0" buildToolsVersion = "33.0.0"
lint {
abortOnError = false
disable += "DialogFragmentCallbacksDetector"
}
defaultConfig { defaultConfig {
applicationId = "app.revanced.manager.compose" applicationId = "app.revanced.manager.compose"
minSdk = 26 minSdk = 26
@ -38,6 +33,10 @@ android {
versionName = "0.0.1" versionName = "0.0.1"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
buildConfigField("String", "REVANCED_API_URL", "\"https://releases.revanced.app\"")
buildConfigField("String", "GITHUB_API_URL", "\"https://api.github.com\"")
} }
buildTypes { buildTypes {
@ -81,24 +80,24 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0")
// AndroidX activity // AndroidX activity
implementation("androidx.activity:activity-compose:1.6.0") implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.work:work-runtime-ktx:2.7.1") implementation("androidx.work:work-runtime-ktx:2.7.1")
// Koin // Koin
val koinVersion = "3.2.2" val koinVersion = "3.3.0"
implementation("io.insert-koin:koin-android:$koinVersion") implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:3.2.1") implementation("io.insert-koin:koin-androidx-compose:3.3.0")
implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion") implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
// Compose // Compose
val composeVersion = "1.3.0-rc01" val composeVersion = "1.4.0-alpha01"
implementation("androidx.compose.ui:ui:$composeVersion") implementation("androidx.compose.ui:ui:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
implementation("androidx.compose.material3:material3:1.0.0-rc01") implementation("androidx.compose.material3:material3:1.1.0-alpha01")
implementation("androidx.compose.material:material-icons-extended:${composeVersion}") implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
// Accompanist // Accompanist
val accompanistVersion = "0.26.5-rc" val accompanistVersion = "0.27.0"
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion") implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion") implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion") implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
@ -108,7 +107,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.2.2") implementation("io.coil-kt:coil-compose:2.2.2")
// KotlinX // KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Taxi (navigation) // Taxi (navigation)
implementation("com.github.X1nto:Taxi:1.2.0") implementation("com.github.X1nto:Taxi:1.2.0")
@ -118,18 +117,16 @@ dependencies {
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.4.0-beta02") implementation("com.android.tools.build:apksig:8.0.0-alpha07")
// Licenses // Licenses
implementation("com.mikepenz:aboutlibraries-compose:10.5.1") implementation("com.mikepenz:aboutlibraries-compose:10.5.1")
// ListenableFuture // Ktor
implementation("com.google.guava:guava:31.1-android") val ktorVersion = "2.1.3"
implementation("androidx.concurrent:concurrent-futures:1.1.0") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.4") implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
// Networking implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("com.vk.knet:core:1.0") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("com.vk.knet:cronet:1.0")
implementation("com.vk.knet:okcronet:1.0")
} }

View File

@ -1,12 +1,6 @@
package app.revanced.manager package app.revanced.manager
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -18,12 +12,14 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.preferences.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.navigation.AppDestination import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.screen.MainDashboardScreen import app.revanced.manager.ui.screen.MainDashboardScreen
import app.revanced.manager.ui.screen.subscreens.* import app.revanced.manager.ui.screen.subscreens.*
import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.requestAllFilesAccess
import app.revanced.manager.util.requestIgnoreBatteryOptimizations
import com.xinto.taxi.Taxi import com.xinto.taxi.Taxi
import com.xinto.taxi.rememberBackstackNavigator import com.xinto.taxi.rememberBackstackNavigator
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -35,7 +31,6 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
permissions()
setContent { setContent {
ReVancedManagerTheme( ReVancedManagerTheme(
dynamicColor = prefs.dynamicColor, dynamicColor = prefs.dynamicColor,
@ -65,26 +60,4 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
private fun permissions() {
fun request(string: String) {
val intent = Intent(string)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = Uri.fromParts("package", applicationContext.packageName, null)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivityForResult(intent, 1)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
request(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
} else {
requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), 1)
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
val pm = applicationContext.getSystemService(POWER_SERVICE) as PowerManager
if (!pm.isIgnoringBatteryOptimizations(applicationContext.packageName)) {
request(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
}
}
} }

View File

@ -2,6 +2,8 @@ package app.revanced.manager
import android.app.Application import android.app.Application
import app.revanced.manager.di.* import app.revanced.manager.di.*
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
@ -13,7 +15,15 @@ class ManagerApplication : Application() {
startKoin { startKoin {
androidContext(this@ManagerApplication) androidContext(this@ManagerApplication)
workManagerFactory() workManagerFactory()
modules(httpModule, preferencesModule, viewModelModule, repositoryModule, workerModule) modules(
httpModule,
preferencesModule,
viewModelModule,
repositoryModule,
workerModule,
patcherModule,
serviceModule
)
} }
} }
} }

View File

@ -1,17 +0,0 @@
package app.revanced.manager
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import app.revanced.manager.ui.Resource
import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.Patch
import java.util.*
object Variables {
val selectedAppPackage = mutableStateOf(Optional.empty<ApplicationInfo>())
val selectedPatches = mutableStateListOf<String>()
val patches = mutableStateOf<Resource<List<Class<out Patch<Context>>>>>(Resource.Loading)
val patchesState by patches
}

View File

@ -1,56 +1,55 @@
package app.revanced.manager.di package app.revanced.manager.di
import android.content.Context import android.content.Context
import com.vk.knet.core.Knet import android.util.Log
import com.vk.knet.core.utils.ByteArrayPool import app.revanced.manager.util.tag
import com.vk.knet.cornet.CronetKnetEngine import io.ktor.client.*
import com.vk.knet.cornet.config.CronetCache import io.ktor.client.engine.okhttp.*
import com.vk.knet.cornet.config.CronetQuic import io.ktor.client.plugins.contentnegotiation.*
import com.vk.knet.cornet.pool.buffer.CronetNativeByteBufferPool import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Cache
import okhttp3.Dns
import okhttp3.Protocol
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
import java.util.concurrent.TimeUnit import java.net.Inet4Address
import java.net.InetAddress
val httpModule = module { val httpModule = module {
fun client(appContext: Context) = CronetKnetEngine.Build(appContext) { fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) {
client { engine {
setCache(CronetCache.Disk(appContext.filesDir, 1024 * 1024 * 10)) config {
dns(object : Dns {
enableHttp2(true) override fun lookup(hostname: String): List<InetAddress> {
enableQuic( val addresses = Dns.SYSTEM.lookup(hostname)
CronetQuic() return if (hostname == "raw.githubusercontent.com") {
) addresses.filterIsInstance<Inet4Address>()
} else {
useBrotli(true) addresses
connectTimeout(15, TimeUnit.SECONDS) }
writeTimeout(15, TimeUnit.SECONDS) }
readTimeout(15, TimeUnit.SECONDS) })
cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100))
nativePool(CronetNativeByteBufferPool.DEFAULT) followRedirects(true)
arrayPool(ByteArrayPool.DEFAULT) followSslRedirects(true)
}
maxConcurrentRequests(50) }
maxConcurrentRequestsPerHost(10) install(ContentNegotiation) {
json(json)
followRedirects(true)
followSslRedirects(true)
} }
} }
fun json() = Json { fun provideJson() = Json {
encodeDefaults = true encodeDefaults = true
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
single { single {
client(androidContext()) provideHttpClient(androidContext(), get())
}
single {
json()
}
single {
Knet.Build(get<CronetKnetEngine>())
} }
singleOf(::provideJson)
} }

View File

@ -1,7 +1,7 @@
package app.revanced.manager.di package app.revanced.manager.di
import android.content.Context import android.content.Context
import app.revanced.manager.preferences.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module

View File

@ -0,0 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.patcher.PatcherUtils
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val patcherModule = module {
singleOf(::PatcherUtils)
}

View File

@ -1,11 +1,13 @@
package app.revanced.manager.di package app.revanced.manager.di
import app.revanced.manager.network.api.GitHubAPI import app.revanced.manager.domain.repository.GithubRepositoryImpl
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.network.api.ManagerAPI
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(::GitHubAPI) singleOf(::GithubRepositoryImpl)
singleOf(::ReVancedAPI) singleOf(::ReVancedRepositoryImpl)
singleOf(::ManagerAPI)
} }

View File

@ -0,0 +1,29 @@
package app.revanced.manager.di
import app.revanced.manager.network.service.*
import io.ktor.client.*
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
fun provideGithubService(
client: HttpService,
): GithubService {
return GithubServiceImpl(
client = client,
)
}
fun provideReVancedService(
client: HttpService,
): ReVancedService {
return ReVancedServiceImpl(
client = client,
)
}
single { provideGithubService(get()) }
single { provideReVancedService(get()) }
singleOf(::HttpService)
}

View File

@ -7,8 +7,8 @@ import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::DashboardViewModel) viewModelOf(::DashboardViewModel)
viewModelOf(::PatcherScreenViewModel)
viewModelOf(::AppSelectorViewModel) viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::PatchingScreenViewModel) viewModelOf(::PatchingScreenViewModel)
viewModelOf(::ContributorsViewModel) viewModelOf(::ContributorsViewModel)
} }

View File

@ -6,5 +6,5 @@ import org.koin.androidx.workmanager.dsl.worker
import org.koin.dsl.module import org.koin.dsl.module
val workerModule = module { val workerModule = module {
worker { PatcherWorker(androidContext(), get(), get(), get(), get()) } worker { PatcherWorker(androidContext(), get(), get(), get()) }
} }

View File

@ -0,0 +1,19 @@
package app.revanced.manager.domain.manager
import android.content.SharedPreferences
import app.revanced.manager.domain.manager.base.BasePreferenceManager
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
/**
* @author Hyperion Authors, zt64
*/
class PreferencesManager(
sharedPreferences: SharedPreferences
) : BasePreferenceManager(sharedPreferences) {
var dynamicColor by booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM)
var srcPatches by stringPreference("src_patches", ghPatches)
var srcIntegrations by stringPreference("src_integrations", ghIntegrations)
}

View File

@ -1,28 +1,15 @@
package app.revanced.manager.preferences package app.revanced.manager.domain.manager.base
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.compose.runtime.getValue 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 androidx.core.content.edit import androidx.core.content.edit
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
class PreferencesManager(
sharedPreferences: SharedPreferences
) : BasePreferenceManager(sharedPreferences) {
var dynamicColor by booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM)
var srcPatches by stringPreference("src_patches", ghPatches)
var srcIntegrations by stringPreference("src_integrations", ghIntegrations)
}
/** /**
* @author Hyperion Authors, zt64 * @author Hyperion Authors, zt64
*/ */
@Suppress("unused")
abstract class BasePreferenceManager( abstract class BasePreferenceManager(
private val prefs: SharedPreferences private val prefs: SharedPreferences
) { ) {

View File

@ -0,0 +1,25 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.network.api.PatchesAsset
import app.revanced.manager.network.dto.GithubContributor
import app.revanced.manager.network.dto.GithubReleases
import app.revanced.manager.network.service.GithubService
import app.revanced.manager.network.utils.APIResponse
interface GithubRepository {
suspend fun getReleases(repo: String): APIResponse<GithubReleases>
suspend fun getContributors(repo: String): APIResponse<List<GithubContributor>>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class GithubRepositoryImpl(
private val service: GithubService
) : GithubRepository {
override suspend fun getReleases(repo: String) = service.getReleases(repo)
override suspend fun getContributors(repo: String) = service.getContributors(repo)
override suspend fun findAsset(repo: String, file: String) = service.findAsset(repo, file)
}

View File

@ -0,0 +1,25 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.network.api.PatchesAsset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.dto.ReVancedRepositories
import app.revanced.manager.network.service.ReVancedService
import app.revanced.manager.network.utils.APIResponse
interface ReVancedRepository {
suspend fun getAssets(): APIResponse<ReVancedReleases>
suspend fun getContributors(): APIResponse<ReVancedRepositories>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class ReVancedRepositoryImpl(
private val service: ReVancedService
) : ReVancedRepository {
override suspend fun getAssets() = service.getAssets()
override suspend fun getContributors() = service.getContributors()
override suspend fun findAsset(repo: String, file: String) = service.findAsset(repo, file)
}

View File

@ -1,64 +0,0 @@
package app.revanced.manager.network.api
import android.util.Log
import app.revanced.manager.network.dto.github.Release
import app.revanced.manager.util.body
import app.revanced.manager.util.get
import app.revanced.manager.util.tag
import com.vk.knet.core.Knet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File
class GitHubAPI(
private val client: Knet,
private val json: Json
) {
private suspend fun findAsset(repo: String, file: String): PatchesAsset {
val release = getLatestRelease(repo)
val asset = release.assets.findAsset(file) ?: throw MissingAssetException()
return PatchesAsset(release, asset)
}
private fun List<Release.Asset>.findAsset(file: String) = find { asset ->
(asset.name.contains(file) && !asset.name.contains("-sources") && !asset.name.contains("-javadoc"))
}
suspend fun downloadAsset(
workdir: File,
repo: String,
name: String
): File {
val asset = findAsset(repo, name).asset
val out = workdir.resolve(asset.name)
if (out.exists()) {
Log.d(
tag,
"Skipping downloading asset ${asset.name} because it exists in cache!"
)
return out
}
Log.d(tag, "Downloading asset ${asset.name} from GitHub API.")
val file = client.get(asset.downloadUrl).body!!.asBytes()
out.writeBytes(file)
return out
}
private suspend fun getLatestRelease(repo: String): Release {
return withContext(Dispatchers.IO) {
val releases = client.get("$baseUrl/$repo/releases").body<List<Release>>(json)
releases.first()
}
}
data class PatchesAsset(
val release: Release,
val asset: Release.Asset
)
private companion object {
private const val baseUrl = "https://api.github.com/repos"
}
}

View File

@ -0,0 +1,76 @@
package app.revanced.manager.network.api
import android.app.Application
import android.util.Log
import android.webkit.URLUtil
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.GithubRepositoryImpl
import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
import app.revanced.manager.util.tag
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class ManagerAPI(
val app: Application,
private val prefs: PreferencesManager,
private val patcherUtils: PatcherUtils,
private val gitHubAPI: GithubRepositoryImpl,
private val reVancedAPI: ReVancedRepositoryImpl,
private val client: HttpClient
) {
private suspend fun downloadAsset(
workdir: File, downloadUrl: String
): File {
val name = URLUtil.guessFileName(downloadUrl, null, null)
val out = workdir.resolve(name)
client.get(downloadUrl).bodyAsChannel().copyAndClose(out.writeChannel())
return out
}
suspend fun downloadPatches() = withContext(Dispatchers.Main) {
try {
val asset =
if (prefs.srcPatches!! == ghPatches) reVancedAPI.findAsset(ghPatches, ".jar")
else gitHubAPI.findAsset(prefs.srcPatches!!, ".jar")
asset.run {
downloadAsset(app.cacheDir, downloadUrl).run {
patcherUtils.run {
patchBundleFile = absolutePath
loadPatchBundle(absolutePath)
}
}
}
} catch (e: Exception) {
Log.e(tag, "An error occurred while downloading patches", e)
}
}
suspend fun downloadIntegrations(workdir: File) = withContext(Dispatchers.IO) {
val asset = if (prefs.srcIntegrations!! == ghIntegrations) {
reVancedAPI.findAsset(prefs.srcIntegrations!!, ".apk")
} else gitHubAPI.findAsset(prefs.srcIntegrations!!, ".apk")
asset.run {
val file = downloadAsset(workdir, downloadUrl)
workdir.resolve(name).writeBytes(file.readBytes())
file
}
}
}
data class PatchesAsset(
val downloadUrl: String, val name: String
)
class MissingAssetException : Exception()

View File

@ -1,84 +0,0 @@
package app.revanced.manager.network.api
import android.util.Log
import app.revanced.manager.network.dto.revanced.Assets
import app.revanced.manager.network.dto.revanced.Repositories
import app.revanced.manager.network.dto.revanced.Tools
import app.revanced.manager.util.body
import app.revanced.manager.util.get
import app.revanced.manager.util.tag
import com.vk.knet.core.Knet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File
class ReVancedAPI(
private val client: Knet,
private val json: Json
) {
suspend fun ping(): Boolean {
return try {
withContext(Dispatchers.Default) {
client.get("$apiUrl/").statusCode == 200
}
} catch (e: Exception) {
Log.e(tag, "ReVanced API isn't available at the moment. switching to GitHub API")
false
}
}
suspend fun fetchAssets(): Tools {
return withContext(Dispatchers.IO) {
client.get("$apiUrl/tools").body(json)
}
}
suspend fun fetchContributors(): Repositories {
return withContext(Dispatchers.IO) {
client.get("$apiUrl/contributors").body(json)
}
}
suspend fun findAsset(repo: String, file: String): PatchesAsset {
val asset = fetchAssets().tools.findAsset(repo, file) ?: throw MissingAssetException()
return PatchesAsset(asset)
}
private fun List<Assets>.findAsset(repo: String, file: String) = find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo))
}
suspend fun downloadAsset(
workdir: File,
repo: String,
name: String,
): File {
val asset = findAsset(repo, name).asset
val out = workdir.resolve(asset.name)
if (out.exists()) {
Log.d(
tag,
"Skipping downloading asset ${asset.name} because it exists in cache!"
)
return out
}
Log.d(tag, "Downloading asset ${asset.name} from ReVanced API.")
val file = client.get(asset.downloadUrl).body!!.asBytes()
out.writeBytes(file)
return out
}
private companion object {
private const val apiUrl = "https://releases.rvcd.win"
}
}
data class PatchesAsset(
val asset: Assets
)
class MissingAssetException : Exception()

View File

@ -0,0 +1,12 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class GithubContributor(
@SerialName("login") val login: String,
@SerialName("avatar_url") val avatar_url: String,
@SerialName("html_url") val url: String,
@SerialName("contributions") val contributions: Int
)

View File

@ -1,10 +1,10 @@
package app.revanced.manager.network.dto.github package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class Release( class GithubReleases(
val assets: List<Asset>, val assets: List<Asset>,
) { ) {
@Serializable @Serializable

View File

@ -1,21 +1,21 @@
package app.revanced.manager.network.dto.revanced package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class Repositories( class ReVancedRepositories(
@SerialName("repositories") val repositories: List<Repository>, @SerialName("repositories") val repositories: List<ReVancedRepository>,
) )
@Serializable @Serializable
class Repository( class ReVancedRepository(
@SerialName("name") val name: String, @SerialName("name") val name: String,
@SerialName("contributors") val contributors: List<Contributor>, @SerialName("contributors") val contributors: List<ReVancedContributor>,
) )
@Serializable @Serializable
class Contributor( class ReVancedContributor(
@SerialName("login") val username: String, @SerialName("login") val username: String,
@SerialName("avatar_url") val avatarUrl: String, @SerialName("avatar_url") val avatarUrl: String,
) )

View File

@ -1,10 +1,10 @@
package app.revanced.manager.network.dto.revanced package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class Tools( class ReVancedReleases(
@SerialName("tools") val tools: List<Assets>, @SerialName("tools") val tools: List<Assets>,
) )

View File

@ -0,0 +1,54 @@
package app.revanced.manager.network.service
import app.revanced.manager.BuildConfig
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.api.PatchesAsset
import app.revanced.manager.network.dto.GithubContributor
import app.revanced.manager.network.dto.GithubReleases
import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.network.utils.getOrNull
import app.revanced.manager.network.utils.getOrThrow
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface GithubService {
suspend fun getReleases(repo: String): APIResponse<GithubReleases>
suspend fun getContributors(repo: String): APIResponse<List<GithubContributor>>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class GithubServiceImpl(
private val client: HttpService,
) : GithubService {
override suspend fun getReleases(repo: String): APIResponse<GithubReleases> {
return withContext(Dispatchers.IO) {
client.request {
url("${baseUrl}/$repo/releases")
}
}
}
override suspend fun getContributors(repo: String): APIResponse<List<GithubContributor>> {
return withContext(Dispatchers.IO) {
client.request {
url("$baseUrl/$repo/contributors")
}
}
}
override suspend fun findAsset(repo: String, file: String): PatchesAsset {
val releases = getReleases(repo).getOrNull() ?: throw Exception("Cannot retrieve assets")
val asset = releases.assets.find { asset ->
(asset.name.contains(file) && !asset.name.contains("-sources") && !asset.name.contains("-javadoc"))
} ?: throw MissingAssetException()
return asset.run { PatchesAsset(downloadUrl, name) }
}
companion object {
private const val baseUrl = BuildConfig.GITHUB_API_URL
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.manager.network.service
import android.util.Log
import app.revanced.manager.network.utils.APIError
import app.revanced.manager.network.utils.APIFailure
import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.util.tag
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
/**
* @author Aliucord Authors, DiamondMiner88
*/
class HttpService(
val json: Json,
val http: HttpClient,
) {
suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): APIResponse<T> {
var body: String? = null
val response = try {
val response = http.request(builder)
if (response.status.isSuccess()) {
body = response.bodyAsText()
if (T::class == String::class) {
return APIResponse.Success(body as T)
}
APIResponse.Success(json.decodeFromString<T>(body))
} else {
body = try {
response.bodyAsText()
} catch (t: Throwable) {
null
}
Log.e(tag, "Failed to fetch: API error, http status: ${response.status}, body: $body")
APIResponse.Error(APIError(response.status, body))
}
} catch (t: Throwable) {
Log.e(tag, "Failed to fetch: error: $t, body: $body")
APIResponse.Failure(APIFailure(t, body))
}
return response
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.manager.network.service
import app.revanced.manager.BuildConfig
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.api.PatchesAsset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.dto.ReVancedRepositories
import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.network.utils.getOrNull
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface ReVancedService {
suspend fun getAssets(): APIResponse<ReVancedReleases>
suspend fun getContributors(): APIResponse<ReVancedRepositories>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class ReVancedServiceImpl(
private val client: HttpService,
) : ReVancedService {
override suspend fun getAssets(): APIResponse<ReVancedReleases> {
return withContext(Dispatchers.IO) {
client.request {
url("$apiUrl/tools")
}
}
}
override suspend fun getContributors(): APIResponse<ReVancedRepositories> {
return withContext(Dispatchers.IO) {
client.request {
url("$apiUrl/contributors")
}
}
}
override suspend fun findAsset(repo: String, file: String): PatchesAsset {
val releases = getAssets().getOrNull() ?: throw Exception("Cannot retrieve assets")
val asset = releases.tools.find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo))
} ?: throw MissingAssetException()
return PatchesAsset(asset.downloadUrl, asset.name)
}
private companion object {
private const val apiUrl = BuildConfig.REVANCED_API_URL
}
}

View File

@ -0,0 +1,86 @@
@file:Suppress("NOTHING_TO_INLINE")
package app.revanced.manager.network.utils
import io.ktor.http.*
/**
* @author Aliucord Authors, DiamondMiner88
*/
sealed interface APIResponse<T> {
data class Success<T>(val data: T) : APIResponse<T>
data class Error<T>(val error: APIError) : APIResponse<T>
data class Failure<T>(val error: APIFailure) : APIResponse<T>
}
class APIError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
class APIFailure(error: Throwable, body: String?) : Error(body, error)
inline fun <T, R> APIResponse<T>.fold(
success: (T) -> R,
error: (APIError) -> R,
failure: (APIFailure) -> R
): R {
return when (this) {
is APIResponse.Success -> success(this.data)
is APIResponse.Error -> error(this.error)
is APIResponse.Failure -> failure(this.error)
}
}
inline fun <T, R> APIResponse<T>.fold(
success: (T) -> R,
fail: (Error) -> R,
): R {
return when (this) {
is APIResponse.Success -> success(data)
is APIResponse.Error -> fail(error)
is APIResponse.Failure -> fail(error)
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.transform(block: (T) -> R): APIResponse<R> {
return if (this !is APIResponse.Success) {
// Error and Failure do not use the generic value
this as APIResponse<R>
} else {
APIResponse.Success(block(data))
}
}
inline fun <T> APIResponse<T>.getOrThrow(): T {
return fold(
success = { it },
fail = { throw it }
)
}
inline fun <T> APIResponse<T>.getOrNull(): T? {
return fold(
success = { it },
fail = { null }
)
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.chain(block: (T) -> APIResponse<R>): APIResponse<R> {
return if (this !is APIResponse.Success) {
// Error and Failure do not use the generic value
this as APIResponse<R>
} else {
block(data)
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.chain(secondary: APIResponse<R>): APIResponse<R> {
return if (secondary is APIResponse.Success) {
secondary
} else {
// Error and Failure do not use the generic value
this as APIResponse<R>
}
}

View File

@ -0,0 +1,60 @@
package app.revanced.manager.patcher
import android.app.Application
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import app.revanced.manager.ui.Resource
import app.revanced.manager.util.tag
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
import java.util.*
class PatcherUtils(val app: Application) {
val patches = mutableStateOf<Resource<List<Class<out Patch<Context>>>>>(Resource.Loading)
val selectedAppPackage = mutableStateOf(Optional.empty<ApplicationInfo>())
val selectedPatches = mutableStateListOf<String>()
lateinit var patchBundleFile: String
fun cleanup() {
patches.value = Resource.Success(emptyList())
selectedAppPackage.value = Optional.empty()
selectedPatches.clear()
}
fun loadPatchBundle(file: String? = patchBundleFile) {
try {
if (this::patchBundleFile.isInitialized) {
val patchClasses = PatchBundle.Dex(
file!!, DexClassLoader(
file, app.codeCacheDir.absolutePath, null, javaClass.classLoader
)
).loadPatches()
patches.value = Resource.Success(patchClasses)
} else throw IllegalStateException("No patch bundle(s) selected.")
} catch (e: Exception) {
Log.e(tag, "Failed to load patch bundle.", e)
}
}
fun getSelectedPackageInfo(): PackageInfo? {
return if (selectedAppPackage.value.isPresent) {
app.packageManager.getPackageArchiveInfo(
selectedAppPackage.value.get().publicSourceDir, PackageManager.GET_META_DATA
)
} else {
null
}
}
fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<Context>>> {
val (patches) = patches.value as? Resource.Success ?: return listOf()
return patches.filter { patch -> ids.any { it == patch.patchName } }
}
}

View File

@ -15,20 +15,15 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.Variables.patches import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.Variables.selectedAppPackage import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.network.api.GitHubAPI
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.aligning.ZipAligner import app.revanced.manager.patcher.aligning.ZipAligner
import app.revanced.manager.patcher.aligning.zip.ZipFile import app.revanced.manager.patcher.aligning.zip.ZipFile
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import app.revanced.manager.patcher.signing.Signer import app.revanced.manager.patcher.signing.Signer
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.Resource import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.viewmodel.Logging import app.revanced.manager.ui.viewmodel.Logging
import app.revanced.manager.util.ghIntegrations
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
@ -41,14 +36,12 @@ import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.time.LocalDateTime
class PatcherWorker( class PatcherWorker(
context: Context, context: Context,
parameters: WorkerParameters, parameters: WorkerParameters,
private val reVancedAPI: ReVancedAPI, private val managerAPI: ManagerAPI,
private val gitHubAPI: GitHubAPI, private val patcherUtils: PatcherUtils
private val prefs: PreferencesManager
) : CoroutineWorker(context, parameters), KoinComponent { ) : CoroutineWorker(context, parameters), KoinComponent {
val tag = "ReVanced Manager" val tag = "ReVanced Manager"
private val workdir = createWorkDir() private val workdir = createWorkDir()
@ -118,10 +111,10 @@ class PatcherWorker(
applicationContext.filesDir.resolve("integrations-cache").also { it.mkdirs() } applicationContext.filesDir.resolve("integrations-cache").also { it.mkdirs() }
val reVancedFolder = val reVancedFolder =
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() } Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
val appInfo = selectedAppPackage.value.get() val appInfo = patcherUtils.selectedAppPackage.value.get()
Logging.log += "Checking prerequisites\n" Logging.log += "Checking prerequisites\n"
val patches = findPatchesByIds(selectedPatches) val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
if (patches.isEmpty()) return true if (patches.isEmpty()) return true
@ -131,16 +124,7 @@ class PatcherWorker(
val outputFile = File(applicationContext.filesDir, "output.apk") val outputFile = File(applicationContext.filesDir, "output.apk")
val cacheDirectory = workdir.resolve("cache") val cacheDirectory = workdir.resolve("cache")
val integrations = if (prefs.srcIntegrations != ghIntegrations || !reVancedAPI.ping()) { val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
Logging.log += "Downloading integrations from GitHub API\n"
gitHubAPI.downloadAsset(integrationsCacheDir, prefs.srcIntegrations!!, ".apk")
} else {
Logging.log += "Downloading integrations from ReVanced API\n"
reVancedAPI.downloadAsset(
integrationsCacheDir,
prefs.srcIntegrations!!, ".apk"
)
}
Logging.log += "Copying base.apk from device\n" Logging.log += "Copying base.apk from device\n"
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -217,12 +201,13 @@ class PatcherWorker(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Files.copy( Files.copy(
outputFile.inputStream(), outputFile.inputStream(),
reVancedFolder.resolve(appInfo.packageName + "-" + LocalDateTime.now() + ".apk") reVancedFolder.resolve(appInfo.packageName + ".apk")
.toPath(), .toPath(),
StandardCopyOption.REPLACE_EXISTING StandardCopyOption.REPLACE_EXISTING
) )
} }
Logging.log += "Copied file to storage!\n" Logging.log += "Copied file to storage!\n"
patcherUtils.cleanup()
} finally { } finally {
Log.d(tag, "Deleting workdir") Log.d(tag, "Deleting workdir")
workdir.deleteRecursively() workdir.deleteRecursively()
@ -232,14 +217,9 @@ class PatcherWorker(
return false return false
} }
private fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<app.revanced.patcher.data.Context>>> {
val (patches) = patches.value as? Resource.Success ?: return listOf()
return patches.filter { patch -> ids.any { it == patch.patchName } }
}
private fun createWorkDir(): File { private fun createWorkDir(): File {
return applicationContext.filesDir.resolve("tmp-${System.currentTimeMillis()}") return applicationContext.cacheDir.resolve("tmp-${System.currentTimeMillis()}")
.also { it.mkdirs() } .also { it.mkdirs() }
} }
} }

View File

@ -15,7 +15,7 @@ import androidx.compose.ui.platform.LocalContext
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.R import app.revanced.manager.R
import app.revanced.manager.network.dto.revanced.Contributor import app.revanced.manager.network.dto.ReVancedContributor
import app.revanced.manager.ui.viewmodel.ContributorsViewModel import app.revanced.manager.ui.viewmodel.ContributorsViewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
@ -27,7 +27,7 @@ import org.koin.androidx.compose.getViewModel
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
fun ContributorsCard( fun ContributorsCard(
title: String, title: String,
data: SnapshotStateList<Contributor>, data: SnapshotStateList<ReVancedContributor>,
vm: ContributorsViewModel = getViewModel() vm: ContributorsViewModel = getViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current

View File

@ -9,18 +9,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.viewmodel.PatchClass import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel
import app.revanced.patcher.annotation.Package import app.revanced.patcher.annotation.Package
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.get
@Composable @Composable
fun PatchCompatibilityDialog( fun PatchCompatibilityDialog(
patchClass: PatchClass, pvm: PatcherScreenViewModel = getViewModel(), onClose: () -> Unit patchClass: PatchClass, patcherUtils: PatcherUtils = get(), onClose: () -> Unit
) { ) {
val patch = patchClass.patch val patch = patchClass.patch
val packageName = pvm.getSelectedPackageInfo()?.packageName val packageName = patcherUtils.getSelectedPackageInfo()?.packageName
AlertDialog(onDismissRequest = onClose, shape = RoundedCornerShape(12.dp), title = { AlertDialog(onDismissRequest = onClose, shape = RoundedCornerShape(12.dp), title = {
Text(stringResource(id = R.string.unsupported), textAlign = TextAlign.Center) Text(stringResource(id = R.string.unsupported), textAlign = TextAlign.Center)
}, text = { }, text = {

View File

@ -16,15 +16,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel import app.revanced.manager.patcher.PatcherUtils
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.get
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewPatcherScreen( fun NewPatcherScreen(
onClickAppSelector: () -> Unit, onClickAppSelector: () -> Unit,
onClickPatchSelector: () -> Unit, onClickPatchSelector: () -> Unit,
viewModel: PatcherScreenViewModel = getViewModel() patcherUtils: PatcherUtils = get()
) { ) {
var validBundle = false var validBundle = false
Column( Column(
@ -75,7 +75,7 @@ fun NewPatcherScreen(
fontSize = 13.sp fontSize = 13.sp
) )
Text( Text(
text = viewModel.getSelectedPackageInfo()!!.applicationInfo.name, text = patcherUtils.getSelectedPackageInfo()!!.applicationInfo.name,
fontSize = 13.sp fontSize = 13.sp
) )
} }

View File

@ -12,13 +12,13 @@ import androidx.compose.ui.Modifier
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.R import app.revanced.manager.R
import app.revanced.manager.Variables.patches import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.Variables.selectedAppPackage import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.ui.Resource import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.FloatingActionButton import app.revanced.manager.ui.component.FloatingActionButton
import app.revanced.manager.ui.component.SplitAPKDialog import app.revanced.manager.ui.component.SplitAPKDialog
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import org.koin.androidx.compose.get
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -28,25 +28,26 @@ fun PatcherScreen(
onClickPatchSelector: () -> Unit, onClickPatchSelector: () -> Unit,
onClickPatch: () -> Unit, onClickPatch: () -> Unit,
onClickSourceSelector: () -> Unit, onClickSourceSelector: () -> Unit,
viewModel: PatcherScreenViewModel = getViewModel() patcherUtils: PatcherUtils = get(),
psvm: PatchesSelectorViewModel = getViewModel(),
managerAPI: ManagerAPI = get()
) { ) {
val selectedAmount = selectedPatches.size val selectedAmount = patcherUtils.selectedPatches.size
val selectedAppPackage by selectedAppPackage val selectedAppPackage by patcherUtils.selectedAppPackage
val hasAppSelected = selectedAppPackage.isPresent val hasAppSelected = selectedAppPackage.isPresent
val patchesLoaded = patches.value is Resource.Success val patchesLoaded = patcherUtils.patches.value is Resource.Success
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
LaunchedEffect(patchesLoaded) {
if (!patchesLoaded) {
managerAPI.downloadPatches()
}
}
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
enabled = hasAppSelected && viewModel.anyPatchSelected(), enabled = hasAppSelected && psvm.anyPatchSelected(),
onClick = { onClick = { onClickPatch(); patcherUtils.loadPatchBundle() }, // TODO: replace this with something better
if (viewModel.checkSplitApk()) {
showDialog = true
} else {
onClickPatch(); viewModel.loadPatches0()
}
}, // TODO: replace this with something better
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") }, icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
text = { Text(text = "Patch") } text = { Text(text = "Patch") }
) )
@ -114,7 +115,7 @@ fun PatcherScreen(
Text( Text(
text = if (!hasAppSelected) { text = if (!hasAppSelected) {
"Select an application first." "Select an application first."
} else if (viewModel.anyPatchSelected()) { } else if (psvm.anyPatchSelected()) {
"$selectedAmount patches selected." "$selectedAmount patches selected."
} else { } else {
stringResource(R.string.card_patches_body_patches) stringResource(R.string.card_patches_body_patches)

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
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.R import app.revanced.manager.R
import app.revanced.manager.preferences.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.SocialItem import app.revanced.manager.ui.component.SocialItem
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme

View File

@ -16,18 +16,20 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.Variables.patchesState import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.PatchCompatibilityDialog import app.revanced.manager.ui.component.PatchCompatibilityDialog
import app.revanced.manager.ui.navigation.AppDestination import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.theme.Typography import app.revanced.manager.ui.theme.Typography
import app.revanced.manager.ui.viewmodel.PatchClass import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.patcher.extensions.PatchExtensions.description import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.version import app.revanced.patcher.extensions.PatchExtensions.version
import com.xinto.taxi.BackstackNavigator import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.get
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@SuppressLint("UnrememberedMutableState") @SuppressLint("UnrememberedMutableState")
@ -35,9 +37,11 @@ import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun PatchesSelectorSubscreen( fun PatchesSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>, navigator: BackstackNavigator<AppDestination>,
pvm: PatcherScreenViewModel = getViewModel(), psvm: PatchesSelectorViewModel = getViewModel(),
patcherUtils: PatcherUtils = get()
) { ) {
val patches = pvm.getFilteredPatchesAndCheckOptions() val patchesState by patcherUtils.patches
val patches = psvm.getFilteredPatches()
var query by mutableStateOf("") var query by mutableStateOf("")
Scaffold( Scaffold(
@ -59,9 +63,9 @@ fun PatchesSelectorSubscreen(
}, },
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
pvm.selectAllPatches(patches, !pvm.anyPatchSelected()) psvm.selectAllPatches(patches, !psvm.anyPatchSelected())
}) { }) {
if (!pvm.anyPatchSelected()) Icon( if (!psvm.anyPatchSelected()) Icon(
Icons.Default.SelectAll, Icons.Default.SelectAll,
contentDescription = null contentDescription = null
) else Icon(Icons.Default.Deselect, contentDescription = null) ) else Icon(Icons.Default.Deselect, contentDescription = null)
@ -115,8 +119,8 @@ fun PatchesSelectorSubscreen(
items(count = patches.size) { items(count = patches.size) {
val patch = patches[it] val patch = patches[it]
val name = patch.patch.patchName val name = patch.patch.patchName
PatchCard(patch, pvm.isPatchSelected(name)) { PatchCard(patch, psvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name)) psvm.selectPatch(name, !psvm.isPatchSelected(name))
} }
} }
} else { } else {
@ -124,8 +128,8 @@ fun PatchesSelectorSubscreen(
val patch = patches[it] val patch = patches[it]
val name = patch.patch.patchName val name = patch.patch.patchName
if (name.contains(query.lowercase())) { if (name.contains(query.lowercase())) {
PatchCard(patch, pvm.isPatchSelected(name)) { PatchCard(patch, psvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name)) psvm.selectPatch(name, !psvm.isPatchSelected(name))
} }
} }
} }
@ -190,7 +194,7 @@ fun PatchCard(patchClass: PatchClass, isSelected: Boolean, onSelected: () -> Uni
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(4.dp) modifier = Modifier.padding(4.dp)
) { ) {
if (patchClass.hasPatchOptions) { if (patchClass.patch.options != null) {
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
IconButton(onClick = { }, modifier = Modifier.size(24.dp)) { IconButton(onClick = { }, modifier = Modifier.size(24.dp)) {
Icon( Icon(

View File

@ -20,11 +20,11 @@ import androidx.compose.ui.res.painterResource
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.R import app.revanced.manager.R
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.component.SourceItem import app.revanced.manager.ui.component.SourceItem
import app.revanced.manager.ui.navigation.AppDestination import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel
import com.xinto.taxi.BackstackNavigator import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.get
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
@ -32,7 +32,7 @@ import java.nio.file.StandardCopyOption
@Composable @Composable
fun SourceSelectorSubscreen( fun SourceSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>, navigator: BackstackNavigator<AppDestination>,
pvm: PatcherScreenViewModel = getViewModel() patcherUtils: PatcherUtils = get()
) { ) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
state = rememberTopAppBarState(), state = rememberTopAppBarState(),
@ -48,8 +48,11 @@ fun SourceSelectorSubscreen(
patchesFile.toPath(), patchesFile.toPath(),
StandardCopyOption.REPLACE_EXISTING StandardCopyOption.REPLACE_EXISTING
) )
pvm.patchBundleFile = patchesFile.absolutePath patchesFile.absolutePath.also {
pvm.loadPatches0() patcherUtils.patchBundleFile = it
patcherUtils.loadPatchBundle(it)
}
navigator.pop() navigator.pop()
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }

View File

@ -8,28 +8,30 @@ import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.Variables import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.ui.Resource import app.revanced.manager.ui.Resource
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.nio.file.Files import kotlinx.coroutines.withContext
import java.nio.file.StandardCopyOption import java.io.File
import java.util.* import java.util.*
class AppSelectorViewModel( class AppSelectorViewModel(
val app: Application, val app: Application, patcherUtils: PatcherUtils
) : ViewModel() { ) : ViewModel() {
val filteredApps = mutableListOf<ApplicationInfo>() val filteredApps = mutableListOf<ApplicationInfo>()
val patches = patcherUtils.patches
private val selectedAppPackage = patcherUtils.selectedAppPackage
private val selectedPatches = patcherUtils.selectedPatches
init { init {
viewModelScope.launch { filterApps() } viewModelScope.launch { filterApps() }
} }
private fun filterApps() { private suspend fun filterApps() = withContext(Dispatchers.Main) {
try { try {
val (patches) = patches.value as Resource.Success val (patches) = patches.value as Resource.Success
patches.forEach patch@{ patch -> patches.forEach patch@{ patch ->
@ -61,23 +63,25 @@ class AppSelectorViewModel(
fun setSelectedAppPackage(appId: ApplicationInfo) { fun setSelectedAppPackage(appId: ApplicationInfo) {
selectedAppPackage.value.ifPresent { s -> selectedAppPackage.value.ifPresent { s ->
if (s != appId) Variables.selectedPatches.clear() if (s != appId) selectedPatches.clear()
} }
selectedAppPackage.value = Optional.of(appId) selectedAppPackage.value = Optional.of(appId)
} }
fun setSelectedAppPackageFromFile(file: Uri?) { fun setSelectedAppPackageFromFile(file: Uri?) {
val apkDir = app.filesDir.resolve("input.apk").toPath() try {
Files.copy( val apkDir = app.cacheDir.resolve(File(file!!.path!!).name)
app.contentResolver.openInputStream(file!!), app.contentResolver.openInputStream(file)!!.run {
apkDir, copyTo(apkDir.outputStream())
StandardCopyOption.REPLACE_EXISTING close()
) }
setSelectedAppPackage( setSelectedAppPackage(
app.packageManager.getPackageArchiveInfo( app.packageManager.getPackageArchiveInfo(
apkDir.toString(), apkDir.path, PackageManager.GET_META_DATA
PackageManager.GET_META_DATA )!!.applicationInfo
)!!.applicationInfo )
) } catch (e: Exception) {
Log.e(tag, "Failed to load apk", e)
}
} }
} }

View File

@ -1,27 +1,28 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.util.Log
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.network.dto.revanced.Contributor import app.revanced.manager.network.dto.ReVancedContributor
import app.revanced.manager.network.utils.getOrNull
import app.revanced.manager.util.* import app.revanced.manager.util.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ContributorsViewModel( class ContributorsViewModel(
private val app: Application, private val app: Application, private val reVancedAPI: ReVancedRepositoryImpl
private val reVancedAPI: ReVancedAPI
) : ViewModel() { ) : ViewModel() {
val patcherContributorsList = mutableStateListOf<Contributor>() val patcherContributorsList = mutableStateListOf<ReVancedContributor>()
val patchesContributorsList = mutableStateListOf<Contributor>() val patchesContributorsList = mutableStateListOf<ReVancedContributor>()
val cliContributorsList = mutableStateListOf<Contributor>() val cliContributorsList = mutableStateListOf<ReVancedContributor>()
val managerContributorsList = mutableStateListOf<Contributor>() val managerContributorsList = mutableStateListOf<ReVancedContributor>()
val integrationsContributorsList = mutableStateListOf<Contributor>() val integrationsContributorsList = mutableStateListOf<ReVancedContributor>()
private fun loadContributors() { private fun loadContributors() {
viewModelScope.launch { viewModelScope.launch {
val contributors = reVancedAPI.fetchContributors() val contributors = reVancedAPI.getContributors().getOrNull() ?: return@launch
contributors.repositories.forEach { repo -> contributors.repositories.forEach { repo ->
when (repo.name) { when (repo.name) {
ghCli -> { ghCli -> {
@ -65,7 +66,7 @@ class ContributorsViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
loadContributors() loadContributors()
} }
} }
} }

View File

@ -2,24 +2,21 @@ package app.revanced.manager.ui.viewmodel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Log
import androidx.compose.runtime.getValue 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 androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.network.dto.revanced.Assets import app.revanced.manager.network.dto.Assets
import app.revanced.manager.network.utils.getOrNull
import app.revanced.manager.util.ghManager import app.revanced.manager.util.ghManager
import app.revanced.manager.util.ghPatcher import app.revanced.manager.util.ghPatcher
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class DashboardViewModel(private val reVancedApi: ReVancedAPI) : ViewModel() { class DashboardViewModel(private val reVancedApi: ReVancedRepositoryImpl) : ViewModel() {
private var _latestPatcherCommit: Assets? by mutableStateOf(null) private var _latestPatcherCommit: Assets? by mutableStateOf(null)
val patcherCommitDate: String val patcherCommitDate: String
get() = _latestPatcherCommit?.commitDate ?: "unknown" get() = _latestPatcherCommit?.commitDate ?: "unknown"
@ -34,8 +31,7 @@ class DashboardViewModel(private val reVancedApi: ReVancedAPI) : ViewModel() {
private fun fetchLastCommit() { private fun fetchLastCommit() {
viewModelScope.launch { viewModelScope.launch {
try { val repo = reVancedApi.getAssets().getOrNull() ?: return@launch
val repo = withContext(Dispatchers.Default) { reVancedApi.fetchAssets() }
for (asset in repo.tools) { for (asset in repo.tools) {
when (asset.repository) { when (asset.repository) {
ghPatcher -> { ghPatcher -> {
@ -46,9 +42,6 @@ class DashboardViewModel(private val reVancedApi: ReVancedAPI) : ViewModel() {
} }
} }
} }
} catch (e: Exception) {
Log.e(tag, "Failed to fetch latest patcher release", e)
}
} }
} }

View File

@ -1,164 +0,0 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.network.api.GitHubAPI
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.Resource
import app.revanced.manager.util.ghPatches
import app.revanced.manager.util.tag
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class PatcherScreenViewModel(
private val app: Application,
private val reVancedApi: ReVancedAPI,
private val gitHubAPI: GitHubAPI,
private val prefs: PreferencesManager
) : ViewModel() {
lateinit var patchBundleFile: String
init {
viewModelScope.launch {
loadPatches()
}
}
fun selectPatch(patchId: String, state: Boolean) {
if (state) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
fun selectAllPatches(patchList: List<PatchClass>, selectAll: Boolean) {
patchList.forEach { patch ->
val patchId = patch.patch.patchName
if (selectAll && !patch.unsupported) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
}
fun setOption(patch: PatchClass, key: String, value: String) {
patch.patch.options?.set(key, value)
for (option in patch.patch.options!!) {
println(option.key + option.value + option.title + option.description)
}
}
fun getOption(patch: PatchClass, key: String) {
patch.patch.options?.get(key)
}
fun isPatchSelected(patchId: String): Boolean {
return selectedPatches.contains(patchId)
}
fun anyPatchSelected(): Boolean {
return !selectedPatches.isEmpty()
}
fun getSelectedPackageInfo(): PackageInfo? {
return if (selectedAppPackage.value.isPresent) {
app.packageManager.getPackageArchiveInfo(
selectedAppPackage.value.get().publicSourceDir,
PackageManager.GET_META_DATA
)
} else {
null
}
}
fun checkSplitApk(): Boolean {
if (getSelectedPackageInfo()!!.applicationInfo!!.metaData!!.getBoolean(
"com.android.vending.splits.required",
false
)
) {
Log.d(tag, "APK is split.")
return true
}
Log.d(tag, "APK is not split.")
return false
}
private fun loadPatches() = viewModelScope.launch {
try {
val file = if (prefs.srcPatches != ghPatches || !reVancedApi.ping()) {
gitHubAPI.downloadAsset(app.cacheDir, prefs.srcPatches!!, ".jar")
} else {
reVancedApi.downloadAsset(app.cacheDir, prefs.srcPatches!!, ".jar")
}
patchBundleFile = file.absolutePath
loadPatches0()
} catch (e: Exception) {
Log.e(tag, "An error occurred while loading patches", e)
}
}
fun loadPatches0() {
try {
val patchClasses = PatchBundle.Dex(
patchBundleFile, DexClassLoader(
patchBundleFile,
app.codeCacheDir.absolutePath,
null,
javaClass.classLoader
)
).loadPatches()
patches.value = Resource.Success(patchClasses)
} catch (e: Exception) {
Toast.makeText(app, "Failed to load patch bundle.", Toast.LENGTH_LONG).show()
Log.e(tag, "Failed to load patch bundle.", e)
return
}
Toast.makeText(app, "Successfully loaded patch bundle.", Toast.LENGTH_LONG).show()
}
fun getFilteredPatchesAndCheckOptions(): List<PatchClass> {
return buildList {
val selected = getSelectedPackageInfo() ?: return@buildList
val (patches) = patches.value as? Resource.Success ?: return@buildList
patches.forEach patch@{ patch ->
var unsupported = false
var hasPatchOptions = false
if (patch.options != null) {
hasPatchOptions = true
Log.d(tag, "${patch.patchName} has patch options.")
}
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
if (pkg.name == selected.packageName) {
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
add(PatchClass(patch, unsupported, hasPatchOptions))
}
}
}
}
}
}
@Parcelize
data class PatchClass(
val patch: Class<out Patch<Context>>,
val unsupported: Boolean,
val hasPatchOptions: Boolean,
) : Parcelable

View File

@ -0,0 +1,63 @@
package app.revanced.manager.ui.viewmodel
import android.os.Parcelable
import androidx.lifecycle.ViewModel
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import kotlinx.parcelize.Parcelize
class PatchesSelectorViewModel(
private val patcherUtils: PatcherUtils
) : ViewModel() {
private val selectedPatches = patcherUtils.selectedPatches
fun isPatchSelected(patchId: String): Boolean {
return selectedPatches.contains(patchId)
}
fun anyPatchSelected(): Boolean {
return !selectedPatches.isEmpty()
}
fun selectPatch(patchId: String, state: Boolean) {
if (state) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
fun selectAllPatches(patchList: List<PatchClass>, selectAll: Boolean) {
patchList.forEach { patch ->
val patchId = patch.patch.patchName
if (selectAll && !patch.unsupported) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
}
}
fun getFilteredPatches(): List<PatchClass> {
return buildList {
val selected = patcherUtils.getSelectedPackageInfo() ?: return@buildList
val (patches) = patcherUtils.patches.value as? Resource.Success ?: return@buildList
patches.forEach patch@{ patch ->
var unsupported = false
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
if (pkg.name == selected.packageName) {
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
add(PatchClass(patch, unsupported))
}
}
}
}
}
}
@Parcelize
data class PatchClass(
val patch: Class<out Patch<Context>>,
val unsupported: Boolean,
) : Parcelable

View File

@ -1,6 +1,6 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.getValue 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

View File

@ -5,7 +5,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 androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.revanced.manager.preferences.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.ghOrganization import app.revanced.manager.util.ghOrganization
import app.revanced.manager.util.openUrl import app.revanced.manager.util.openUrl

View File

@ -1,23 +0,0 @@
package app.revanced.manager.util
import com.vk.knet.core.Knet
import com.vk.knet.core.http.HttpMethod
import com.vk.knet.core.http.HttpPayload
import com.vk.knet.core.http.HttpRequest
import com.vk.knet.core.http.HttpResponse
import com.vk.knet.core.http.body.request.HttpRequestBody
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
fun Knet.get(
url: String,
headers: Map<String, List<String>> = emptyMap(),
body: HttpRequestBody? = null,
payload: Map<HttpPayload, Any>? = null
): HttpResponse {
return execute(HttpRequest(HttpMethod.GET, url, headers, body, payload))
}
inline fun <reified T> HttpResponse.body(json: Json = Json.Default): T {
return json.decodeFromString(body!!.toString())
}

View File

@ -1,11 +1,46 @@
package app.revanced.manager.util package app.revanced.manager.util
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.core.net.toUri import androidx.core.net.toUri
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 {
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
}) })
}
fun Context.requestAllFilesAccess() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startActivity(Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.fromParts("package", applicationContext.packageName, null)
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
addCategory("android.intent.category.DEFAULT")
})
}
}
@SuppressLint("BatteryLife")
fun Context.requestIgnoreBatteryOptimizations() {
startActivity(Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.fromParts("package", packageName, null)
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
addCategory("android.intent.category.DEFAULT")
})
}
fun Context.isIgnoringOptimizations(): Boolean {
val pm = getSystemService(ComponentActivity.POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
} }

View File

@ -1,49 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="278"
android:viewportHeight="278">
<group android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="34.75"
android:translateY="34.75">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -1,44 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="278dp"
android:height="278dp"
android:viewportWidth="278"
android:viewportHeight="278">
<path
android:pathData="M0.06,0.83h277v277h-277z"
android:fillColor="#111623"/>
<path
android:pathData="M108.01,80.13H174.31V95.81H108.01V80.13ZM96.91,95.81V80.13C96.91,73.98 101.88,69 108.01,69H174.31C180.44,69 185.41,73.98 185.41,80.13V95.81H189.78C198.97,95.81 206.43,103.29 206.43,112.5V189.31C206.43,198.53 198.97,206 189.78,206H92.32C83.12,206 75.67,198.53 75.67,189.31V184.16H127.18C133.31,184.16 138.28,179.18 138.28,173.03V164.13C138.28,157.98 133.31,153 127.18,153H75.67V112.5C75.67,103.29 83.12,95.81 92.32,95.81H96.91Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M75.67,184.16H56.53C50.4,184.16 45.43,179.18 45.43,173.03V164.13C45.43,157.98 50.4,153 56.53,153H75.67V184.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="45.43"
android:startY="132.25"
android:endX="87.19"
android:endY="221.04"
android:type="linear">
<item android:offset="0" android:color="#FF353DFF"/>
<item android:offset="1" android:color="#FF3DDCFF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:aapt="http://schemas.android.com/aapt"
android:width="278dp" android:width="200dp"
android:height="278dp" android:height="200dp"
android:viewportWidth="278" android:viewportWidth="278"
android:viewportHeight="278"> android:viewportHeight="278">
<path <path

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -7,71 +7,25 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
<string name="update_manager">Update Manager</string>
<string name="update_patch_bundle">Update Patches</string> <string name="update_patch_bundle">Update Patches</string>
<string name="patched_apps">Installed</string>
<string name="update_all">Update All</string>
<string name="updates_available">Available updates</string>
<string name="expand">Expand</string> <string name="expand">Expand</string>
<string name="update">Update</string>
<string name="app_selector_title">Select an app…</string> <string name="app_selector_title">Select an app…</string>
<string name="app_bar_open_discord">Discord</string>
<string name="app_bar_open_github">GitHub</string>
<string name="card_announcement_header">Announcement</string>
<string name="card_commits_header">Latest updates</string>
<string name="card_logs_header">Logs</string>
<string name="card_application_header">Select Application</string> <string name="card_application_header">Select Application</string>
<string name="card_patches_header">Select Patches</string> <string name="card_patches_header">Select Patches</string>
<string name="card_options_header">Options</string>
<string name="card_contributors_header">Contributors</string>
<string name="card_commits_body_patcher">Patcher:</string>
<string name="card_commits_body_manager">Manager:</string>
<string name="card_announcement_body_placeholder">This is an example text for previewing ReVanced Manager design that will be replaced by dynamic announcements in the future.</string>
<string name="card_options_body_root">Root</string>
<string name="card_options_body_use_installed">Use installed YouTube app</string>
<string name="card_application_not_loaded">Loading applications.</string> <string name="card_application_not_loaded">Loading applications.</string>
<string name="card_application_not_selected">No application selected.</string> <string name="card_application_not_selected">No application selected.</string>
<string name="card_patches_body_source">No patches source selected.</string>
<string name="card_patches_body_patches">No patches selected.</string> <string name="card_patches_body_patches">No patches selected.</string>
<string name="card_application_body_selected">Selected:</string>
<string name="card_logs_body">Click here to view patcher logs.</string>
<string name="card_credits_body">Click here to view people who have contributed to the ReVanced Project.</string>
<string name="loading_body">One moment, please…</string> <string name="loading_body">One moment, please…</string>
<string name="loading_fetching_patches">Fetching patches</string>
<string name="unsupported">Unsupported version</string> <string name="unsupported">Unsupported version</string>
<string name="only_compatible">This patch is only compatible with version: </string> <string name="only_compatible">This patch is only compatible with version: </string>
<string name="card_announcement_button_changelog">Changelog</string>
<string name="button_patch">Patch</string>
<string name="navigation_dashboard">Dashboard</string>
<string name="navigation_patcher">Patcher</string>
<string name="screen_logs_title">Logs</string>
<string name="screen_contributors_title">Contributors</string> <string name="screen_contributors_title">Contributors</string>
<string name="screen_credits_team_manager">ReVanced Manager</string>
<string name="screen_credits_team_website">Website</string>
<string name="screen_credits_team">Team</string>
<string name="screen_credits_translators">Translators</string>
<string name="screen_credits_team_patcher">Patcher</string>
<string name="help">Help</string>
<string name="help_translate">Help translate</string>
<string name="whats_new">What\'s New</string>
<string name="ic_non_selected">No patches are selected!</string>
<string name="contributor_image">Contributor image</string> <string name="contributor_image">Contributor image</string>
<string name="no_contributors">No contributors</string>
<string name="screen_settings_title">Settings</string>
<string name="screen_about_title">About</string>
<string name="about">About</string>
<string name="navigation_more">More</string>
<string name="cli_contributors">CLI</string> <string name="cli_contributors">CLI</string>
<string name="patcher_contributors">Patcher</string> <string name="patcher_contributors">Patcher</string>
<string name="patches_contributors">Patches</string> <string name="patches_contributors">Patches</string>
<string name="manager_contributors">Manager</string> <string name="manager_contributors">Manager</string>
<string name="integrations_contributors">Integrations</string> <string name="integrations_contributors">Integrations</string>
<string name="dropdown_button">Dropdown Button</string>
<string name="app_version">Version</string>
<string name="faq">FAQ</string>
<string name="version_info">Version info</string>
<string name="unsupported_version">Unsupported version</string> <string name="unsupported_version">Unsupported version</string>
<string name="compatible_versions">Compatible app versions</string>
<string name="patcher_notification_title">Patching</string> <string name="patcher_notification_title">Patching</string>
<string name="patcher_notification_message">ReVanced Manager is patching</string> <string name="patcher_notification_message">ReVanced Manager is patching</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>