mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-11-18 01:09:23 +01:00
feat: backend
This commit is contained in:
parent
63feb1023a
commit
2eb63b71d6
@ -2,13 +2,22 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.7.20"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
google()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.compose"
|
||||
compileSdk = 33
|
||||
@ -40,24 +49,39 @@ android {
|
||||
|
||||
buildFeatures.compose = true
|
||||
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.3.2"
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.4.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// AndroidX Core
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||
implementation("androidx.activity:activity-compose:1.6.1")
|
||||
|
||||
// Compose
|
||||
val composeVersion = "1.3.3"
|
||||
val composeVersion = "1.4.0-alpha05"
|
||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
|
||||
|
||||
// Accompanist
|
||||
val accompanistVersion = "0.29.1-alpha"
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
|
||||
|
||||
// KotlinX
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
||||
|
||||
// Material 3
|
||||
implementation("androidx.compose.material3:material3:1.0.1")
|
||||
implementation("androidx.compose.material3:material3:1.1.0-alpha08")
|
||||
|
||||
|
||||
// ReVanced
|
||||
implementation("app.revanced:revanced-patcher:6.4.3")
|
||||
|
||||
// Koin
|
||||
implementation("io.insert-koin:koin-android:3.3.2")
|
||||
@ -65,4 +89,12 @@ dependencies {
|
||||
|
||||
// Compose Navigation
|
||||
implementation("dev.olshevski.navigation:reimagined:1.3.1")
|
||||
|
||||
// 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")
|
||||
}
|
@ -2,6 +2,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="ReservedSystemPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".ManagerApplication"
|
||||
android:allowBackup="true"
|
||||
@ -13,16 +26,19 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ReVancedManager"
|
||||
tools:targetApi="33">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.ReVancedManager">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<service android:name=".installer.service.InstallService" />
|
||||
<service android:name=".installer.service.UninstallService" />
|
||||
</application>
|
||||
</manifest>
|
@ -4,14 +4,17 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import app.revanced.manager.compose.domain.manager.PreferencesManager
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.compose.destination.Destination
|
||||
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
|
||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import app.revanced.manager.compose.ui.theme.Theme
|
||||
import dev.olshevski.navigation.reimagined.*
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val prefs: PreferencesManager by inject()
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -20,8 +23,8 @@ class MainActivity : ComponentActivity() {
|
||||
installSplashScreen()
|
||||
setContent {
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = true, // TODO: Implement preferences
|
||||
dynamicColor = false
|
||||
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
|
||||
dynamicColor = prefs.dynamicColor
|
||||
) {
|
||||
val navController = rememberNavController<Destination>(startDestination = Destination.Home)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.manager.compose
|
||||
|
||||
import android.app.Application
|
||||
import app.revanced.manager.compose.di.*
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
@ -10,7 +11,13 @@ class ManagerApplication: Application() {
|
||||
|
||||
startKoin {
|
||||
androidContext(this@ManagerApplication)
|
||||
modules(emptyList()) // TODO: Add modules
|
||||
modules(
|
||||
httpModule,
|
||||
preferencesModule,
|
||||
repositoryModule,
|
||||
serviceModule,
|
||||
viewModelModule
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package app.revanced.manager.compose.di
|
||||
|
||||
import android.content.Context
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Dns
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
|
||||
val httpModule = module {
|
||||
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 provideJson() = Json {
|
||||
encodeDefaults = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
single {
|
||||
provideHttpClient(androidContext(), get())
|
||||
}
|
||||
singleOf(::provideJson)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package app.revanced.manager.compose.di
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.compose.domain.manager.PreferencesManager
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val preferencesModule = module {
|
||||
fun providePreferences(
|
||||
context: Context
|
||||
) = PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
|
||||
|
||||
singleOf(::providePreferences)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.compose.di
|
||||
|
||||
import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl
|
||||
import app.revanced.manager.compose.network.api.ManagerAPI
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val repositoryModule = module {
|
||||
singleOf(::ReVancedRepositoryImpl)
|
||||
singleOf(::ManagerAPI)
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package app.revanced.manager.compose.di
|
||||
|
||||
import app.revanced.manager.compose.network.service.HttpService
|
||||
import app.revanced.manager.compose.network.service.ReVancedService
|
||||
import app.revanced.manager.compose.network.service.ReVancedServiceImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val serviceModule = module {
|
||||
fun provideReVancedService(
|
||||
client: HttpService,
|
||||
): ReVancedService {
|
||||
return ReVancedServiceImpl(
|
||||
client = client,
|
||||
)
|
||||
}
|
||||
|
||||
single { provideReVancedService(get()) }
|
||||
singleOf(::HttpService)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package app.revanced.manager.compose.domain.manager
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import app.revanced.manager.compose.domain.manager.base.BasePreferenceManager
|
||||
import app.revanced.manager.compose.ui.theme.Theme
|
||||
|
||||
/**
|
||||
* @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 sentry by booleanPreference("sentry", true)
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package app.revanced.manager.compose.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 kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* @author Hyperion Authors, zt64
|
||||
*/
|
||||
abstract class BasePreferenceManager(
|
||||
private val prefs: SharedPreferences
|
||||
) {
|
||||
protected fun getString(key: String, defaultValue: String?) =
|
||||
prefs.getString(key, defaultValue)!!
|
||||
|
||||
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
|
||||
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
|
||||
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
|
||||
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
|
||||
enumValueOf<E>(getString(key, defaultValue.name))
|
||||
|
||||
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
|
||||
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
|
||||
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
|
||||
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
|
||||
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) =
|
||||
putString(key, value.name)
|
||||
|
||||
protected class Preference<T>(
|
||||
private val key: String,
|
||||
defaultValue: T,
|
||||
getter: (key: String, defaultValue: T) -> T,
|
||||
private val setter: (key: String, newValue: T) -> Unit
|
||||
) {
|
||||
@Suppress("RedundantSetter")
|
||||
var value by mutableStateOf(getter(key, defaultValue))
|
||||
private set
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
||||
value = newValue
|
||||
setter(key, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun stringPreference(
|
||||
key: String,
|
||||
defaultValue: String?
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getString,
|
||||
setter = ::putString
|
||||
)
|
||||
|
||||
protected fun booleanPreference(
|
||||
key: String,
|
||||
defaultValue: Boolean
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getBoolean,
|
||||
setter = ::putBoolean
|
||||
)
|
||||
|
||||
protected fun intPreference(
|
||||
key: String,
|
||||
defaultValue: Int
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getInt,
|
||||
setter = ::putInt
|
||||
)
|
||||
|
||||
protected fun floatPreference(
|
||||
key: String,
|
||||
defaultValue: Float
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getFloat,
|
||||
setter = ::putFloat
|
||||
)
|
||||
|
||||
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||
key: String,
|
||||
defaultValue: E
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getEnum,
|
||||
setter = ::putEnum
|
||||
)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package app.revanced.manager.compose.domain.repository
|
||||
|
||||
import app.revanced.manager.compose.network.api.PatchesAsset
|
||||
import app.revanced.manager.compose.network.dto.ReVancedReleases
|
||||
import app.revanced.manager.compose.network.dto.ReVancedRepositories
|
||||
import app.revanced.manager.compose.network.service.ReVancedService
|
||||
import app.revanced.manager.compose.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)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package app.revanced.manager.compose.installer.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
class InstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
when (extraStatus) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}.apply {
|
||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||
|
||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package app.revanced.manager.compose.installer.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
class UninstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent,
|
||||
flags: Int,
|
||||
startId: Int
|
||||
): Int {
|
||||
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}.apply {
|
||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_UNINSTALL_ACTION
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package app.revanced.manager.compose.installer.utils
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import app.revanced.manager.compose.installer.service.InstallService
|
||||
import app.revanced.manager.compose.installer.service.UninstallService
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
object PM {
|
||||
|
||||
fun installApp(apk: File, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
val session =
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||
session.writeApk(apk)
|
||||
session.commit(context.installIntentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
@ -0,0 +1,66 @@
|
||||
package app.revanced.manager.compose.network.api
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl
|
||||
import app.revanced.manager.compose.util.ghIntegrations
|
||||
import app.revanced.manager.compose.util.ghPatches
|
||||
import app.revanced.manager.compose.util.tag
|
||||
import app.revanced.manager.compose.util.toast
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import java.io.File
|
||||
|
||||
class ManagerAPI(
|
||||
private val app: Application,
|
||||
private val client: HttpClient,
|
||||
private val revancedRepository: ReVancedRepositoryImpl
|
||||
) {
|
||||
var downloadProgress: Float? by mutableStateOf(null)
|
||||
|
||||
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
|
||||
client.get(downloadUrl) {
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
|
||||
}
|
||||
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
|
||||
downloadProgress = null
|
||||
}
|
||||
|
||||
suspend fun downloadPatchBundle() {
|
||||
try {
|
||||
val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl
|
||||
val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() }
|
||||
.resolve("patchbundle.jar")
|
||||
downloadAsset(downloadUrl, patchesFile)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to download patch bundle", e)
|
||||
app.toast("Failed to download patch bundle")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadIntegrations() {
|
||||
try {
|
||||
val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl
|
||||
val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() }
|
||||
.resolve("integrations.apk")
|
||||
downloadAsset(downloadUrl, integrationsFile)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to download integrations", e)
|
||||
app.toast("Failed to download integrations")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PatchesAsset(
|
||||
val downloadUrl: String, val name: String
|
||||
)
|
||||
|
||||
class MissingAssetException : Exception()
|
@ -0,0 +1,21 @@
|
||||
package app.revanced.manager.compose.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ReVancedRepositories(
|
||||
@SerialName("repositories") val repositories: List<ReVancedRepository>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ReVancedRepository(
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("contributors") val contributors: List<ReVancedContributor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ReVancedContributor(
|
||||
@SerialName("login") val username: String,
|
||||
@SerialName("avatar_url") val avatarUrl: String,
|
||||
)
|
@ -0,0 +1,20 @@
|
||||
package app.revanced.manager.compose.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ReVancedReleases(
|
||||
@SerialName("tools") val tools: List<Assets>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Assets(
|
||||
@SerialName("repository") val repository: String,
|
||||
@SerialName("version") val version: String,
|
||||
@SerialName("timestamp") val timestamp: String,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("size") val size: String?,
|
||||
@SerialName("browser_download_url") val downloadUrl: String,
|
||||
@SerialName("content_type") val content_type: String
|
||||
)
|
@ -0,0 +1,51 @@
|
||||
package app.revanced.manager.compose.network.service
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.manager.compose.network.utils.APIError
|
||||
import app.revanced.manager.compose.network.utils.APIFailure
|
||||
import app.revanced.manager.compose.network.utils.APIResponse
|
||||
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("ReVanced Manager", "Failed to fetch: API error, http status: ${response.status}, body: $body")
|
||||
APIResponse.Error(APIError(response.status, body))
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e("ReVanced Manager", "Failed to fetch: error: $t, body: $body")
|
||||
APIResponse.Failure(APIFailure(t, body))
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.manager.compose.network.service
|
||||
|
||||
import app.revanced.manager.compose.network.api.MissingAssetException
|
||||
import app.revanced.manager.compose.network.api.PatchesAsset
|
||||
import app.revanced.manager.compose.network.dto.ReVancedReleases
|
||||
import app.revanced.manager.compose.network.dto.ReVancedRepositories
|
||||
import app.revanced.manager.compose.network.utils.APIResponse
|
||||
import app.revanced.manager.compose.network.utils.getOrNull
|
||||
import app.revanced.manager.compose.util.apiURL
|
||||
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 = apiURL
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package app.revanced.manager.compose.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>
|
||||
}
|
||||
}
|
@ -3,11 +3,7 @@ package app.revanced.manager.compose.ui.theme
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
@ -54,4 +50,10 @@ fun ReVancedManagerTheme(
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
enum class Theme(val displayName: String) {
|
||||
SYSTEM("System"),
|
||||
LIGHT("Light"),
|
||||
DARK("Dark");
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.compose.util
|
||||
|
||||
private const val team = "revanced"
|
||||
const val ghOrganization = "https://github.com/$team"
|
||||
const val ghCli = "$team/revanced-cli"
|
||||
const val ghPatches = "$team/revanced-patches"
|
||||
const val ghPatcher = "$team/revanced-patcher"
|
||||
const val ghManager = "$team/revanced-manager"
|
||||
const val ghIntegrations = "$team/revanced-integrations"
|
||||
const val tag = "ReVanced Manager"
|
||||
const val apiURL = "https://releases.revanced.app"
|
27
app/src/main/java/app/revanced/manager/compose/util/Util.kt
Normal file
27
app/src/main/java/app/revanced/manager/compose/util/Util.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package app.revanced.manager.compose.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.Toast
|
||||
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.loadIcon(string: String): Drawable? {
|
||||
return try {
|
||||
packageManager.getApplicationIcon(string)
|
||||
} catch (e: NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Toast.makeText(this, string, duration).show()
|
||||
}
|
@ -7,7 +7,7 @@ buildscript {
|
||||
plugins {
|
||||
id("com.android.application") version "7.3.1" apply false
|
||||
id("com.android.library") version "7.3.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.7.20" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.0" apply false
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
Loading…
Reference in New Issue
Block a user