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
buildToolsVersion = "33.0.0"
lint {
abortOnError = false
disable += "DialogFragmentCallbacksDetector"
}
defaultConfig {
applicationId = "app.revanced.manager.compose"
minSdk = 26
@ -38,6 +33,10 @@ android {
versionName = "0.0.1"
vectorDrawables.useSupportLibrary = true
buildConfigField("String", "REVANCED_API_URL", "\"https://releases.revanced.app\"")
buildConfigField("String", "GITHUB_API_URL", "\"https://api.github.com\"")
}
buildTypes {
@ -81,24 +80,24 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.0")
// 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")
// Koin
val koinVersion = "3.2.2"
val koinVersion = "3.3.0"
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")
// Compose
val composeVersion = "1.3.0-rc01"
val composeVersion = "1.4.0-alpha01"
implementation("androidx.compose.ui:ui:$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}")
// Accompanist
val accompanistVersion = "0.26.5-rc"
val accompanistVersion = "0.27.0"
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
@ -108,7 +107,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.2.2")
// KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Taxi (navigation)
implementation("com.github.X1nto:Taxi:1.2.0")
@ -118,18 +117,16 @@ dependencies {
// Signing & aligning
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
implementation("com.mikepenz:aboutlibraries-compose:10.5.1")
// ListenableFuture
implementation("com.google.guava:guava:31.1-android")
implementation("androidx.concurrent:concurrent-futures:1.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.4")
// Networking
implementation("com.vk.knet:core:1.0")
implementation("com.vk.knet:cronet:1.0")
implementation("com.vk.knet:okcronet:1.0")
// Ktor
val ktorVersion = "2.1.3"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}

View File

@ -1,12 +1,6 @@
package app.revanced.manager
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
@ -18,12 +12,14 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
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.screen.MainDashboardScreen
import app.revanced.manager.ui.screen.subscreens.*
import app.revanced.manager.ui.theme.ReVancedManagerTheme
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.rememberBackstackNavigator
import org.koin.android.ext.android.inject
@ -35,7 +31,6 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
permissions()
setContent {
ReVancedManagerTheme(
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 app.revanced.manager.di.*
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
@ -13,7 +15,15 @@ class ManagerApplication : Application() {
startKoin {
androidContext(this@ManagerApplication)
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
import android.content.Context
import com.vk.knet.core.Knet
import com.vk.knet.core.utils.ByteArrayPool
import com.vk.knet.cornet.CronetKnetEngine
import com.vk.knet.cornet.config.CronetCache
import com.vk.knet.cornet.config.CronetQuic
import com.vk.knet.cornet.pool.buffer.CronetNativeByteBufferPool
import android.util.Log
import app.revanced.manager.util.tag
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.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.core.module.dsl.singleOf
import org.koin.dsl.module
import java.util.concurrent.TimeUnit
import java.net.Inet4Address
import java.net.InetAddress
val httpModule = module {
fun client(appContext: Context) = CronetKnetEngine.Build(appContext) {
client {
setCache(CronetCache.Disk(appContext.filesDir, 1024 * 1024 * 10))
enableHttp2(true)
enableQuic(
CronetQuic()
)
useBrotli(true)
connectTimeout(15, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
readTimeout(15, TimeUnit.SECONDS)
nativePool(CronetNativeByteBufferPool.DEFAULT)
arrayPool(ByteArrayPool.DEFAULT)
maxConcurrentRequests(50)
maxConcurrentRequestsPerHost(10)
followRedirects(true)
followSslRedirects(true)
fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) {
engine {
config {
dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val addresses = Dns.SYSTEM.lookup(hostname)
return if (hostname == "raw.githubusercontent.com") {
addresses.filterIsInstance<Inet4Address>()
} else {
addresses
}
}
})
cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100))
followRedirects(true)
followSslRedirects(true)
}
}
install(ContentNegotiation) {
json(json)
}
}
fun json() = Json {
fun provideJson() = Json {
encodeDefaults = true
isLenient = true
ignoreUnknownKeys = true
}
single {
client(androidContext())
}
single {
json()
}
single {
Knet.Build(get<CronetKnetEngine>())
provideHttpClient(androidContext(), get())
}
singleOf(::provideJson)
}

View File

@ -1,7 +1,7 @@
package app.revanced.manager.di
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.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
import app.revanced.manager.network.api.GitHubAPI
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.domain.repository.GithubRepositoryImpl
import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.network.api.ManagerAPI
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::GitHubAPI)
singleOf(::ReVancedAPI)
singleOf(::GithubRepositoryImpl)
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 {
viewModelOf(::SettingsViewModel)
viewModelOf(::DashboardViewModel)
viewModelOf(::PatcherScreenViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::PatchingScreenViewModel)
viewModelOf(::ContributorsViewModel)
}

View File

@ -6,5 +6,5 @@ import org.koin.androidx.workmanager.dsl.worker
import org.koin.dsl.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 androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
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
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
*/
@Suppress("unused")
abstract class BasePreferenceManager(
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.Serializable
@Serializable
class Release(
class GithubReleases(
val assets: List<Asset>,
) {
@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.Serializable
@Serializable
class Repositories(
@SerialName("repositories") val repositories: List<Repository>,
class ReVancedRepositories(
@SerialName("repositories") val repositories: List<ReVancedRepository>,
)
@Serializable
class Repository(
class ReVancedRepository(
@SerialName("name") val name: String,
@SerialName("contributors") val contributors: List<Contributor>,
@SerialName("contributors") val contributors: List<ReVancedContributor>,
)
@Serializable
class Contributor(
class ReVancedContributor(
@SerialName("login") val username: 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.Serializable
@Serializable
class Tools(
class ReVancedReleases(
@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.WorkerParameters
import app.revanced.manager.R
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.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.aligning.ZipAligner
import app.revanced.manager.patcher.aligning.zip.ZipFile
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import app.revanced.manager.patcher.signing.Signer
import app.revanced.manager.preferences.PreferencesManager
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.viewmodel.Logging
import app.revanced.manager.util.ghIntegrations
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.patchName
@ -41,14 +36,12 @@ import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.time.LocalDateTime
class PatcherWorker(
context: Context,
parameters: WorkerParameters,
private val reVancedAPI: ReVancedAPI,
private val gitHubAPI: GitHubAPI,
private val prefs: PreferencesManager
private val managerAPI: ManagerAPI,
private val patcherUtils: PatcherUtils
) : CoroutineWorker(context, parameters), KoinComponent {
val tag = "ReVanced Manager"
private val workdir = createWorkDir()
@ -118,10 +111,10 @@ class PatcherWorker(
applicationContext.filesDir.resolve("integrations-cache").also { it.mkdirs() }
val reVancedFolder =
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
val appInfo = selectedAppPackage.value.get()
val appInfo = patcherUtils.selectedAppPackage.value.get()
Logging.log += "Checking prerequisites\n"
val patches = findPatchesByIds(selectedPatches)
val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
if (patches.isEmpty()) return true
@ -131,16 +124,7 @@ class PatcherWorker(
val outputFile = File(applicationContext.filesDir, "output.apk")
val cacheDirectory = workdir.resolve("cache")
val integrations = if (prefs.srcIntegrations != ghIntegrations || !reVancedAPI.ping()) {
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"
)
}
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
Logging.log += "Copying base.apk from device\n"
withContext(Dispatchers.IO) {
@ -217,12 +201,13 @@ class PatcherWorker(
withContext(Dispatchers.IO) {
Files.copy(
outputFile.inputStream(),
reVancedFolder.resolve(appInfo.packageName + "-" + LocalDateTime.now() + ".apk")
reVancedFolder.resolve(appInfo.packageName + ".apk")
.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
Logging.log += "Copied file to storage!\n"
patcherUtils.cleanup()
} finally {
Log.d(tag, "Deleting workdir")
workdir.deleteRecursively()
@ -232,14 +217,9 @@ class PatcherWorker(
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 {
return applicationContext.filesDir.resolve("tmp-${System.currentTimeMillis()}")
return applicationContext.cacheDir.resolve("tmp-${System.currentTimeMillis()}")
.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.unit.dp
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 coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
@ -27,7 +27,7 @@ import org.koin.androidx.compose.getViewModel
@ExperimentalMaterial3Api
fun ContributorsCard(
title: String,
data: SnapshotStateList<Contributor>,
data: SnapshotStateList<ReVancedContributor>,
vm: ContributorsViewModel = getViewModel()
) {
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.unit.dp
import app.revanced.manager.R
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel
import app.revanced.patcher.annotation.Package
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.get
@Composable
fun PatchCompatibilityDialog(
patchClass: PatchClass, pvm: PatcherScreenViewModel = getViewModel(), onClose: () -> Unit
patchClass: PatchClass, patcherUtils: PatcherUtils = get(), onClose: () -> Unit
) {
val patch = patchClass.patch
val packageName = pvm.getSelectedPackageInfo()?.packageName
val packageName = patcherUtils.getSelectedPackageInfo()?.packageName
AlertDialog(onDismissRequest = onClose, shape = RoundedCornerShape(12.dp), title = {
Text(stringResource(id = R.string.unsupported), textAlign = TextAlign.Center)
}, text = {

View File

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

View File

@ -12,13 +12,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.Variables.patches
import app.revanced.manager.Variables.selectedAppPackage
import app.revanced.manager.Variables.selectedPatches
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.FloatingActionButton
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
@OptIn(ExperimentalMaterial3Api::class)
@ -28,25 +28,26 @@ fun PatcherScreen(
onClickPatchSelector: () -> Unit,
onClickPatch: () -> Unit,
onClickSourceSelector: () -> Unit,
viewModel: PatcherScreenViewModel = getViewModel()
patcherUtils: PatcherUtils = get(),
psvm: PatchesSelectorViewModel = getViewModel(),
managerAPI: ManagerAPI = get()
) {
val selectedAmount = selectedPatches.size
val selectedAppPackage by selectedAppPackage
val selectedAmount = patcherUtils.selectedPatches.size
val selectedAppPackage by patcherUtils.selectedAppPackage
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) }
LaunchedEffect(patchesLoaded) {
if (!patchesLoaded) {
managerAPI.downloadPatches()
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
enabled = hasAppSelected && viewModel.anyPatchSelected(),
onClick = {
if (viewModel.checkSplitApk()) {
showDialog = true
} else {
onClickPatch(); viewModel.loadPatches0()
}
}, // TODO: replace this with something better
enabled = hasAppSelected && psvm.anyPatchSelected(),
onClick = { onClickPatch(); patcherUtils.loadPatchBundle() }, // TODO: replace this with something better
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
text = { Text(text = "Patch") }
)
@ -114,7 +115,7 @@ fun PatcherScreen(
Text(
text = if (!hasAppSelected) {
"Select an application first."
} else if (viewModel.anyPatchSelected()) {
} else if (psvm.anyPatchSelected()) {
"$selectedAmount patches selected."
} else {
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.unit.dp
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.SocialItem
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.unit.dp
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.component.LoadingIndicator
import app.revanced.manager.ui.component.PatchCompatibilityDialog
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.theme.Typography
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.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.version
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.get
import org.koin.androidx.compose.getViewModel
@SuppressLint("UnrememberedMutableState")
@ -35,9 +37,11 @@ import org.koin.androidx.compose.getViewModel
@Composable
fun PatchesSelectorSubscreen(
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("")
Scaffold(
@ -59,9 +63,9 @@ fun PatchesSelectorSubscreen(
},
actions = {
IconButton(onClick = {
pvm.selectAllPatches(patches, !pvm.anyPatchSelected())
psvm.selectAllPatches(patches, !psvm.anyPatchSelected())
}) {
if (!pvm.anyPatchSelected()) Icon(
if (!psvm.anyPatchSelected()) Icon(
Icons.Default.SelectAll,
contentDescription = null
) else Icon(Icons.Default.Deselect, contentDescription = null)
@ -115,8 +119,8 @@ fun PatchesSelectorSubscreen(
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
PatchCard(patch, pvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name))
PatchCard(patch, psvm.isPatchSelected(name)) {
psvm.selectPatch(name, !psvm.isPatchSelected(name))
}
}
} else {
@ -124,8 +128,8 @@ fun PatchesSelectorSubscreen(
val patch = patches[it]
val name = patch.patch.patchName
if (name.contains(query.lowercase())) {
PatchCard(patch, pvm.isPatchSelected(name)) {
pvm.selectPatch(name, !pvm.isPatchSelected(name))
PatchCard(patch, psvm.isPatchSelected(name)) {
psvm.selectPatch(name, !psvm.isPatchSelected(name))
}
}
}
@ -190,7 +194,7 @@ fun PatchCard(patchClass: PatchClass, isSelected: Boolean, onSelected: () -> Uni
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(4.dp)
) {
if (patchClass.hasPatchOptions) {
if (patchClass.patch.options != null) {
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
IconButton(onClick = { }, modifier = Modifier.size(24.dp)) {
Icon(

View File

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

View File

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

View File

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

View File

@ -2,24 +2,21 @@ package app.revanced.manager.ui.viewmodel
import android.annotation.SuppressLint
import android.text.format.DateUtils
import android.util.Log
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.network.api.ReVancedAPI
import app.revanced.manager.network.dto.revanced.Assets
import app.revanced.manager.domain.repository.ReVancedRepositoryImpl
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.ghPatcher
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*
class DashboardViewModel(private val reVancedApi: ReVancedAPI) : ViewModel() {
class DashboardViewModel(private val reVancedApi: ReVancedRepositoryImpl) : ViewModel() {
private var _latestPatcherCommit: Assets? by mutableStateOf(null)
val patcherCommitDate: String
get() = _latestPatcherCommit?.commitDate ?: "unknown"
@ -34,8 +31,7 @@ class DashboardViewModel(private val reVancedApi: ReVancedAPI) : ViewModel() {
private fun fetchLastCommit() {
viewModelScope.launch {
try {
val repo = withContext(Dispatchers.Default) { reVancedApi.fetchAssets() }
val repo = reVancedApi.getAssets().getOrNull() ?: return@launch
for (asset in repo.tools) {
when (asset.repository) {
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
import android.app.Application
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.setValue
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.util.ghOrganization
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
import android.annotation.SuppressLint
import android.content.Context
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
fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
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"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="278dp"
android:height="278dp"
android:width="200dp"
android:height="200dp"
android:viewportWidth="278"
android:viewportHeight="278">
<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="updates">Updates</string>
<string name="manager">Manager</string>
<string name="update_manager">Update Manager</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="update">Update</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_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_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_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_fetching_patches">Fetching patches</string>
<string name="unsupported">Unsupported 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_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="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="patcher_contributors">Patcher</string>
<string name="patches_contributors">Patches</string>
<string name="manager_contributors">Manager</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="compatible_versions">Compatible app versions</string>
<string name="patcher_notification_title">Patching</string>
<string name="patcher_notification_message">ReVanced Manager is patching</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>