feat: patcher
Co-authored-by: Canny <canny1913@outlook.com>
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "hourly"
|
34
.github/workflows/android.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
pull_request:
|
||||
branches: [ "dev" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: manager
|
||||
path: ./app/build/outputs/apk/debug/app-debug.apk
|
34
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
name: Android Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
pull_request:
|
||||
branches: [ "dev" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: manager
|
||||
path: ./app/build/outputs/apk/release/app-release-unsigned.apk
|
@ -1,18 +1,37 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.devtools.ksp")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.7.10"
|
||||
}
|
||||
|
||||
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"
|
||||
compileSdk = 32
|
||||
compileSdk = 33
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
disable += "DialogFragmentCallbacksDetector"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager.compose"
|
||||
minSdk = 26
|
||||
targetSdk = 32
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "0.0.1"
|
||||
|
||||
@ -22,7 +41,11 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,17 +59,28 @@ android {
|
||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/prebuilt/**"
|
||||
excludes += "/**/*.version"
|
||||
excludes += "/kotlin-tooling-metadata.json"
|
||||
excludes += "/okhttp3/**"
|
||||
excludes += "/DebugProbesKt.bin"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures.compose = true
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.2.0"
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.3.0-rc02"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// AndroidX core
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||
|
||||
// AndroidX activity
|
||||
implementation("androidx.activity:activity-compose:1.6.0-alpha05")
|
||||
implementation("androidx.activity:activity-compose:1.6.0-rc02")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||
|
||||
// Koin
|
||||
val koinVersion = "3.2.0"
|
||||
@ -54,16 +88,17 @@ dependencies {
|
||||
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
||||
|
||||
// Compose
|
||||
val composeVersion = "1.3.0-alpha01"
|
||||
val composeVersion = "1.3.0-alpha03"
|
||||
implementation("androidx.compose.ui:ui:${composeVersion}")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:${composeVersion}")
|
||||
implementation("androidx.compose.material3:material3:1.0.0-alpha15")
|
||||
implementation("androidx.compose.material3:material3:1.0.0-beta02")
|
||||
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
|
||||
|
||||
// Accompanist
|
||||
val accompanistVersion = "0.26.0-alpha"
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||
|
||||
// Coil (async image loading)
|
||||
implementation("io.coil-kt:coil-compose:2.1.0")
|
||||
@ -74,10 +109,20 @@ dependencies {
|
||||
// Taxi (navigation)
|
||||
implementation("com.github.X1nto:Taxi:1.2.0")
|
||||
|
||||
// Ktor
|
||||
val ktorVersion = "2.0.3"
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
|
||||
// ReVanced
|
||||
implementation("app.revanced:revanced-patcher:4.2.3")
|
||||
|
||||
// Coil for network image
|
||||
implementation("io.coil-kt:coil-compose:2.1.0")
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("com.android.tools.build:apksig:7.2.2")
|
||||
}
|
41
app/proguard-rules.pro
vendored
@ -14,8 +14,41 @@
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-dontobfuscate
|
||||
-if @kotlinx.serialization.Serializable class **
|
||||
-keepclassmembers class <1> {
|
||||
static <1>$Companion Companion;
|
||||
}
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
static **$* *;
|
||||
}
|
||||
-keepclassmembers class <2>$<3> {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
public static ** INSTANCE;
|
||||
}
|
||||
-keepclassmembers class <1> {
|
||||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# These classes required for the patcher to function correctly.
|
||||
-keep class app.revanced.patcher.** {
|
||||
*;
|
||||
}
|
||||
-keep class brut.** {
|
||||
*;
|
||||
}
|
||||
-keep class org.xmlpull.** {
|
||||
*;
|
||||
}
|
||||
-keep class kotlin.** {
|
||||
*;
|
||||
}
|
||||
-keep class org.jf.** {
|
||||
*;
|
||||
}
|
||||
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
@ -2,26 +2,46 @@
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".ManagerApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.ReVancedManager"
|
||||
tools:targetApi="31">
|
||||
|
||||
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>
|
||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
@ -8,12 +8,17 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.with
|
||||
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.ui.navigation.AppDestination
|
||||
import app.revanced.manager.ui.screen.MainDashboardScreen
|
||||
import app.revanced.manager.ui.screen.subscreens.AppSelectorSubscreen
|
||||
import app.revanced.manager.ui.screen.subscreens.PatchesSelectorSubscreen
|
||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import com.xinto.taxi.Taxi
|
||||
import com.xinto.taxi.rememberBackstackNavigator
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -23,9 +28,13 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
ReVancedManagerTheme(dynamicColor = prefs.dynamicColor) {
|
||||
ReVancedManagerTheme(
|
||||
dynamicColor = prefs.dynamicColor,
|
||||
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
|
||||
) {
|
||||
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Dashboard)
|
||||
|
||||
BackHandler {
|
||||
@ -39,6 +48,10 @@ class MainActivity : ComponentActivity() {
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is AppDestination.Dashboard -> MainDashboardScreen(navigator = navigator)
|
||||
is AppDestination.AppSelector -> AppSelectorSubscreen(
|
||||
navigator = navigator
|
||||
)
|
||||
is AppDestination.PatchSelector -> PatchesSelectorSubscreen(navigator = navigator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
app/src/main/java/app/revanced/manager/Variables.kt
Normal file
@ -0,0 +1,19 @@
|
||||
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.Data
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import java.util.*
|
||||
|
||||
|
||||
object Variables {
|
||||
val selectedAppPackage = mutableStateOf(Optional.empty<String>())
|
||||
val selectedPatches = mutableStateListOf<String>()
|
||||
val patches = mutableStateOf<Resource<List<Class<out Patch<Data>>>>>(Resource.Loading)
|
||||
val patchesState by patches
|
||||
val filteredApps = mutableListOf<ApplicationInfo>()
|
||||
}
|
90
app/src/main/java/app/revanced/manager/api/API.kt
Normal file
@ -0,0 +1,90 @@
|
||||
package app.revanced.manager.api
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.manager.dto.github.APIRelease
|
||||
import app.revanced.manager.preferences.PreferencesManager
|
||||
import app.revanced.manager.repository.GitHubRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.android.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
val client = HttpClient(Android) {
|
||||
BrowserUserAgent()
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
encodeDefaults = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class API(private val repository: GitHubRepository, private val prefs: PreferencesManager) {
|
||||
|
||||
private suspend fun findAsset(repo: String, file: String): PatchesAsset {
|
||||
val release = repository.getLatestRelease(repo)
|
||||
val asset = release.assets.findAsset(file) ?: throw MissingAssetException()
|
||||
return PatchesAsset(release, asset)
|
||||
}
|
||||
|
||||
private fun List<APIRelease.Asset>.findAsset(file: String) = find { asset ->
|
||||
(asset.name.contains(file) && !asset.name.contains("-sources") && !asset.name.contains("-javadoc"))
|
||||
}
|
||||
|
||||
suspend fun downloadPatchBundle(workdir: File): File {
|
||||
return try {
|
||||
val (_, out) = downloadAsset(workdir, findAsset(prefs.srcPatches.toString(), ".jar"))
|
||||
out
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to download patch bundle", e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadIntegrations(workdir: File): File {
|
||||
return try {
|
||||
val (_, out) = downloadAsset(
|
||||
workdir,
|
||||
findAsset(prefs.srcIntegrations.toString(), ".apk")
|
||||
)
|
||||
out
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to download integrations", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadAsset(
|
||||
workdir: File,
|
||||
patchesAsset: PatchesAsset
|
||||
): Pair<PatchesAsset, File> {
|
||||
val (release, asset) = patchesAsset
|
||||
val out = workdir.resolve("${release.tagName}-${asset.name}")
|
||||
if (out.exists()) {
|
||||
Log.d(
|
||||
"ReVanced Manager",
|
||||
"Skipping downloading asset ${asset.name} because it exists in cache!"
|
||||
)
|
||||
return patchesAsset to out
|
||||
}
|
||||
Log.d("ReVanced Manager", "Downloading asset ${asset.name}")
|
||||
client.get(asset.downloadUrl)
|
||||
.bodyAsChannel()
|
||||
.copyAndClose(out.writeChannel())
|
||||
|
||||
return patchesAsset to out
|
||||
}
|
||||
}
|
||||
data class PatchesAsset(
|
||||
val release: APIRelease,
|
||||
val asset: APIRelease.Asset
|
||||
)
|
||||
|
||||
|
||||
class MissingAssetException : Exception()
|
@ -1,9 +1,11 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.api.API
|
||||
import app.revanced.manager.repository.GitHubRepository
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val repositoryModule = module {
|
||||
singleOf(::GitHubRepository)
|
||||
singleOf(::API)
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.ui.viewmodel.*
|
||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val viewModelModule = module {
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
}
|
@ -3,7 +3,7 @@ package app.revanced.manager.dto.github
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ApiCommit(
|
||||
class APICommit(
|
||||
val sha: String,
|
||||
val commit: Object
|
||||
) {
|
||||
|
@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ApiContributor(
|
||||
class APIContributor(
|
||||
@SerialName("login") val username: String,
|
||||
@SerialName("avatar_url") val avatarUrl: String,
|
||||
@SerialName("html_url") val profileUrl: String,
|
||||
|
@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiRelease(
|
||||
data class APIRelease(
|
||||
@SerialName("tag_name") val tagName: String,
|
||||
@SerialName("published_at") val publishedAt: String,
|
||||
val prerelease: Boolean,
|
||||
|
12
app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package app.revanced.manager.patcher.aapt
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
object Aapt {
|
||||
fun binary(context: Context): File {
|
||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory }!!.first())
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.patcher.aligning
|
||||
|
||||
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
|
||||
|
||||
internal object ZipAligner {
|
||||
private const val DEFAULT_ALIGNMENT = 4
|
||||
private const val LIBRARY_ALIGNMENT = 4096
|
||||
|
||||
fun getEntryAlignment(entry: ZipEntry): Int? =
|
||||
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package app.revanced.manager.patcher.aligning.zip
|
||||
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun UInt.toLittleEndian() =
|
||||
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
|
||||
|
||||
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
|
||||
|
||||
fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
|
||||
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
|
||||
|
||||
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
|
||||
|
||||
fun ByteBuffer.getUShort() = this.getShort().toUShort()
|
||||
fun ByteBuffer.getUInt() = this.getInt().toUInt()
|
||||
|
||||
fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
|
||||
fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShort() = this.readShort().toUShort()
|
||||
fun DataInput.readUInt() = this.readInt().toUInt()
|
||||
|
||||
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
|
||||
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
|
||||
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
|
||||
|
||||
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
|
||||
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())
|
@ -0,0 +1,176 @@
|
||||
package app.revanced.manager.patcher.aligning.zip
|
||||
|
||||
import app.revanced.manager.patcher.aligning.zip.structures.ZipEndRecord
|
||||
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.Deflater
|
||||
|
||||
class ZipFile(val file: File) : Closeable {
|
||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
||||
|
||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
||||
private var CDNeedsRewrite = false
|
||||
|
||||
private val compressionLevel = 5
|
||||
|
||||
init {
|
||||
//if file isn't empty try to load entries
|
||||
if (file.length() > 0) {
|
||||
val endRecord = findEndRecord()
|
||||
|
||||
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
|
||||
throw IllegalArgumentException("Multi-file archives are not supported")
|
||||
|
||||
entries = readEntries(endRecord).toMutableList()
|
||||
}
|
||||
|
||||
//seek back to start for writing
|
||||
filePointer.seek(0)
|
||||
}
|
||||
|
||||
private fun findEndRecord(): ZipEndRecord {
|
||||
//look from end to start since end record is at the end
|
||||
for (i in filePointer.length() - 1 downTo 0) {
|
||||
filePointer.seek(i)
|
||||
//possible beginning of signature
|
||||
if (filePointer.readByte() == 0x50.toByte()) {
|
||||
//seek back to get the full int
|
||||
filePointer.seek(i)
|
||||
val possibleSignature = filePointer.readUIntLE()
|
||||
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
||||
filePointer.seek(i)
|
||||
return ZipEndRecord.fromECD(filePointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception("Couldn't find end record")
|
||||
}
|
||||
|
||||
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
|
||||
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
|
||||
|
||||
val numberOfEntries = endRecord.diskEntries.toInt()
|
||||
|
||||
return buildList(numberOfEntries) {
|
||||
for (i in 1..numberOfEntries) {
|
||||
add(
|
||||
ZipEntry.fromCDE(filePointer).also
|
||||
{
|
||||
//for some reason the local extra field can be different from the central one
|
||||
it.readLocalExtra(
|
||||
filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
it.localHeaderOffset.toLong() + 28,
|
||||
2
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeCD() {
|
||||
val CDStart = filePointer.channel.position().toUInt()
|
||||
|
||||
entries.forEach {
|
||||
filePointer.channel.write(it.toCDE())
|
||||
}
|
||||
|
||||
val entriesCount = entries.size.toUShort()
|
||||
|
||||
val endRecord = ZipEndRecord(
|
||||
0u,
|
||||
0u,
|
||||
entriesCount,
|
||||
entriesCount,
|
||||
filePointer.channel.position().toUInt() - CDStart,
|
||||
CDStart,
|
||||
""
|
||||
)
|
||||
|
||||
filePointer.channel.write(endRecord.toECD())
|
||||
}
|
||||
|
||||
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
||||
CDNeedsRewrite = true
|
||||
|
||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
||||
|
||||
filePointer.channel.write(entry.toLFH())
|
||||
filePointer.channel.write(data)
|
||||
|
||||
entries.add(entry)
|
||||
}
|
||||
|
||||
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
|
||||
val compressor = Deflater(compressionLevel, true)
|
||||
compressor.setInput(data)
|
||||
compressor.finish()
|
||||
|
||||
val uncompressedSize = data.size
|
||||
val compressedData =
|
||||
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
|
||||
|
||||
val compressedDataLength = compressor.deflate(compressedData)
|
||||
val compressedBuffer =
|
||||
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
|
||||
|
||||
compressor.end()
|
||||
|
||||
val crc = CRC32()
|
||||
crc.update(data)
|
||||
|
||||
entry.compression = 8u //deflate compression
|
||||
entry.uncompressedSize = uncompressedSize.toUInt()
|
||||
entry.compressedSize = compressedDataLength.toUInt()
|
||||
entry.crc32 = crc.value.toUInt()
|
||||
|
||||
addEntry(entry, compressedBuffer)
|
||||
}
|
||||
|
||||
fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
|
||||
alignment?.let { alignment ->
|
||||
//calculate where data would end up
|
||||
val dataOffset = filePointer.filePointer + entry.LFHSize
|
||||
|
||||
val mod = dataOffset % alignment
|
||||
|
||||
//wrong alignment
|
||||
if (mod != 0L) {
|
||||
//add padding at end of extra field
|
||||
entry.localExtraField =
|
||||
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
addEntry(entry, data)
|
||||
}
|
||||
|
||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||
return filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
entry.dataOffset.toLong(),
|
||||
entry.compressedSize.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
|
||||
for (entry in file.entries) {
|
||||
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
|
||||
|
||||
val data = file.getDataForEntry(entry)
|
||||
addEntryCopyData(entry, data, entryAlignment(entry))
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (CDNeedsRewrite) writeCD()
|
||||
filePointer.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package app.revanced.manager.patcher.aligning.zip.structures
|
||||
|
||||
import app.revanced.manager.patcher.aligning.zip.putUInt
|
||||
import app.revanced.manager.patcher.aligning.zip.putUShort
|
||||
import app.revanced.manager.patcher.aligning.zip.readUIntLE
|
||||
import app.revanced.manager.patcher.aligning.zip.readUShortLE
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
data class ZipEndRecord(
|
||||
val diskNumber: UShort,
|
||||
val startingDiskNumber: UShort,
|
||||
val diskEntries: UShort,
|
||||
val totalEntries: UShort,
|
||||
val centralDirectorySize: UInt,
|
||||
val centralDirectoryStartOffset: UInt,
|
||||
val fileComment: String,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val ECD_HEADER_SIZE = 22
|
||||
const val ECD_SIGNATURE = 0x06054b50u
|
||||
|
||||
fun fromECD(input: DataInput): ZipEndRecord {
|
||||
val signature = input.readUIntLE()
|
||||
|
||||
if (signature != ECD_SIGNATURE)
|
||||
throw IllegalArgumentException("Input doesn't start with end record signature")
|
||||
|
||||
val diskNumber = input.readUShortLE()
|
||||
val startingDiskNumber = input.readUShortLE()
|
||||
val diskEntries = input.readUShortLE()
|
||||
val totalEntries = input.readUShortLE()
|
||||
val centralDirectorySize = input.readUIntLE()
|
||||
val centralDirectoryStartOffset = input.readUIntLE()
|
||||
val fileCommentLength = input.readUShortLE()
|
||||
var fileComment = ""
|
||||
|
||||
if (fileCommentLength > 0u) {
|
||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
||||
input.readFully(fileCommentBytes)
|
||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
return ZipEndRecord(
|
||||
diskNumber,
|
||||
startingDiskNumber,
|
||||
diskEntries,
|
||||
totalEntries,
|
||||
centralDirectorySize,
|
||||
centralDirectoryStartOffset,
|
||||
fileComment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toECD(): ByteBuffer {
|
||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size)
|
||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(ECD_SIGNATURE)
|
||||
buffer.putUShort(diskNumber)
|
||||
buffer.putUShort(startingDiskNumber)
|
||||
buffer.putUShort(diskEntries)
|
||||
buffer.putUShort(totalEntries)
|
||||
buffer.putUInt(centralDirectorySize)
|
||||
buffer.putUInt(centralDirectoryStartOffset)
|
||||
buffer.putUShort(commentBytes.size.toUShort())
|
||||
|
||||
buffer.put(commentBytes)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package app.revanced.manager.patcher.aligning.zip.structures
|
||||
|
||||
import app.revanced.manager.patcher.aligning.zip.*
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
data class ZipEntry(
|
||||
val version: UShort,
|
||||
val versionNeeded: UShort,
|
||||
val flags: UShort,
|
||||
var compression: UShort,
|
||||
val modificationTime: UShort,
|
||||
val modificationDate: UShort,
|
||||
var crc32: UInt,
|
||||
var compressedSize: UInt,
|
||||
var uncompressedSize: UInt,
|
||||
val diskNumber: UShort,
|
||||
val internalAttributes: UShort,
|
||||
val externalAttributes: UInt,
|
||||
var localHeaderOffset: UInt,
|
||||
val fileName: String,
|
||||
val extraField: ByteArray,
|
||||
val fileComment: String,
|
||||
var localExtraField: ByteArray = ByteArray(0), //separate for alignment
|
||||
) {
|
||||
val LFHSize: Int
|
||||
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
|
||||
|
||||
val dataOffset: UInt
|
||||
get() = localHeaderOffset + LFHSize.toUInt()
|
||||
|
||||
companion object {
|
||||
const val CDE_HEADER_SIZE = 46
|
||||
const val CDE_SIGNATURE = 0x02014b50u
|
||||
|
||||
const val LFH_HEADER_SIZE = 30
|
||||
const val LFH_SIGNATURE = 0x04034b50u
|
||||
|
||||
fun createWithName(fileName: String): ZipEntry {
|
||||
return ZipEntry(
|
||||
0x1403u, //made by unix, version 20
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0x0821u, //seems to be static time google uses, no idea
|
||||
0x0221u, //same as above
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
fileName,
|
||||
ByteArray(0),
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
fun fromCDE(input: DataInput): ZipEntry {
|
||||
val signature = input.readUIntLE()
|
||||
|
||||
if (signature != CDE_SIGNATURE)
|
||||
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
|
||||
|
||||
val version = input.readUShortLE()
|
||||
val versionNeeded = input.readUShortLE()
|
||||
var flags = input.readUShortLE()
|
||||
val compression = input.readUShortLE()
|
||||
val modificationTime = input.readUShortLE()
|
||||
val modificationDate = input.readUShortLE()
|
||||
val crc32 = input.readUIntLE()
|
||||
val compressedSize = input.readUIntLE()
|
||||
val uncompressedSize = input.readUIntLE()
|
||||
val fileNameLength = input.readUShortLE()
|
||||
var fileName = ""
|
||||
val extraFieldLength = input.readUShortLE()
|
||||
var extraField = ByteArray(extraFieldLength.toInt())
|
||||
val fileCommentLength = input.readUShortLE()
|
||||
var fileComment = ""
|
||||
val diskNumber = input.readUShortLE()
|
||||
val internalAttributes = input.readUShortLE()
|
||||
val externalAttributes = input.readUIntLE()
|
||||
val localHeaderOffset = input.readUIntLE()
|
||||
|
||||
val variableFieldsLength =
|
||||
fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
|
||||
|
||||
if (variableFieldsLength > 0) {
|
||||
val fileNameBytes = ByteArray(fileNameLength.toInt())
|
||||
input.readFully(fileNameBytes)
|
||||
fileName = fileNameBytes.toString(Charsets.UTF_8)
|
||||
|
||||
input.readFully(extraField)
|
||||
|
||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
||||
input.readFully(fileCommentBytes)
|
||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
flags = (flags and 0b1000u.inv()
|
||||
.toUShort()) //disable data descriptor flag as they are not used
|
||||
|
||||
return ZipEntry(
|
||||
version,
|
||||
versionNeeded,
|
||||
flags,
|
||||
compression,
|
||||
modificationTime,
|
||||
modificationDate,
|
||||
crc32,
|
||||
compressedSize,
|
||||
uncompressedSize,
|
||||
diskNumber,
|
||||
internalAttributes,
|
||||
externalAttributes,
|
||||
localHeaderOffset,
|
||||
fileName,
|
||||
extraField,
|
||||
fileComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun readLocalExtra(buffer: ByteBuffer) {
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
localExtraField = ByteArray(buffer.getUShort().toInt())
|
||||
}
|
||||
|
||||
fun toLFH(): ByteBuffer {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
|
||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(LFH_SIGNATURE)
|
||||
buffer.putUShort(versionNeeded)
|
||||
buffer.putUShort(flags)
|
||||
buffer.putUShort(compression)
|
||||
buffer.putUShort(modificationTime)
|
||||
buffer.putUShort(modificationDate)
|
||||
buffer.putUInt(crc32)
|
||||
buffer.putUInt(compressedSize)
|
||||
buffer.putUInt(uncompressedSize)
|
||||
buffer.putUShort(nameBytes.size.toUShort())
|
||||
buffer.putUShort(localExtraField.size.toUShort())
|
||||
|
||||
buffer.put(nameBytes)
|
||||
buffer.put(localExtraField)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
|
||||
fun toCDE(): ByteBuffer {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer =
|
||||
ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
|
||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(CDE_SIGNATURE)
|
||||
buffer.putUShort(version)
|
||||
buffer.putUShort(versionNeeded)
|
||||
buffer.putUShort(flags)
|
||||
buffer.putUShort(compression)
|
||||
buffer.putUShort(modificationTime)
|
||||
buffer.putUShort(modificationDate)
|
||||
buffer.putUInt(crc32)
|
||||
buffer.putUInt(compressedSize)
|
||||
buffer.putUInt(uncompressedSize)
|
||||
buffer.putUShort(nameBytes.size.toUShort())
|
||||
buffer.putUShort(extraField.size.toUShort())
|
||||
buffer.putUShort(commentBytes.size.toUShort())
|
||||
buffer.putUShort(diskNumber)
|
||||
buffer.putUShort(internalAttributes)
|
||||
buffer.putUInt(externalAttributes)
|
||||
buffer.putUInt(localHeaderOffset)
|
||||
|
||||
buffer.put(nameBytes)
|
||||
buffer.put(extraField)
|
||||
buffer.put(commentBytes)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package app.revanced.manager.patcher.signing
|
||||
|
||||
import com.android.apksig.ApkSigner
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
internal class Signer(
|
||||
private val cn: String, password: String
|
||||
) {
|
||||
private val passwordCharArray = password.toCharArray()
|
||||
private fun newKeystore(out: File) {
|
||||
val (publicKey, privateKey) = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
privateKS.load(null, passwordCharArray)
|
||||
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
|
||||
privateKS.store(FileOutputStream(out), passwordCharArray)
|
||||
}
|
||||
|
||||
private fun createKey(): Pair<X509Certificate, PrivateKey> {
|
||||
val gen = KeyPairGenerator.getInstance("RSA")
|
||||
gen.initialize(2048)
|
||||
val pair = gen.generateKeyPair()
|
||||
var serialNumber: BigInteger
|
||||
do serialNumber =
|
||||
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
||||
val x500Name = X500Name("CN=$cn")
|
||||
val builder = X509v3CertificateBuilder(
|
||||
x500Name,
|
||||
serialNumber,
|
||||
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
|
||||
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
|
||||
Locale.ENGLISH,
|
||||
x500Name,
|
||||
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
|
||||
)
|
||||
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||
}
|
||||
|
||||
fun signApk(input: File, output: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val ks = File(input.parent, "revanced-cli.keystore")
|
||||
if (!ks.exists()) newKeystore(ks)
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
|
||||
val config = ApkSigner.SignerConfig.Builder(
|
||||
cn,
|
||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||
).build()
|
||||
|
||||
val signer = ApkSigner.Builder(listOf(config))
|
||||
signer.setCreatedBy(cn)
|
||||
signer.setInputApk(input)
|
||||
signer.setOutputApk(output)
|
||||
|
||||
signer.build().sign()
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package app.revanced.manager.patcher.worker
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
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.selectedPatches
|
||||
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.ui.Resource
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import java.io.File
|
||||
|
||||
class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
||||
CoroutineWorker(context, parameters) {
|
||||
val tag = "ReVanced Manager"
|
||||
private val workdir = File(inputData.getString("workdir")!!)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (runAttemptCount > 0) {
|
||||
return Result.failure(
|
||||
androidx.work.Data.Builder()
|
||||
.putString("error", "Android requested retrying but retrying is disabled")
|
||||
.build()
|
||||
) // don't retry
|
||||
}
|
||||
|
||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val channel = NotificationChannel(
|
||||
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val notificationManager =
|
||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
notificationManager!!.createNotificationChannel(channel)
|
||||
val notification: Notification = Notification.Builder(applicationContext, channel.id)
|
||||
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
|
||||
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
|
||||
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
|
||||
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
|
||||
.setContentIntent(pendingIntent).build()
|
||||
|
||||
setForeground(ForegroundInfo(1, notification))
|
||||
return try {
|
||||
runPatcher(workdir)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Error while patching", e)
|
||||
Result.failure(
|
||||
androidx.work.Data.Builder()
|
||||
.putString("error", "Error while patching: ${e.message ?: e::class.simpleName}")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runPatcher(
|
||||
workdir: File
|
||||
): Boolean {
|
||||
val aaptPath = Aapt.binary(applicationContext).absolutePath
|
||||
val frameworkPath =
|
||||
applicationContext.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
Log.d(tag, "Checking prerequisites")
|
||||
val patches = findPatchesByIds(selectedPatches)
|
||||
if (patches.isEmpty()) return true
|
||||
|
||||
|
||||
Log.d(tag, "Creating directories")
|
||||
|
||||
File(inputData.getString("input")!!).copyTo(
|
||||
applicationContext.filesDir.resolve("base.apk"),
|
||||
true
|
||||
)
|
||||
|
||||
val inputFile = File(applicationContext.filesDir, "base.apk")
|
||||
val patchedFile = File(workdir, "patched.apk")
|
||||
val outputFile = File(applicationContext.filesDir, "out.apk")
|
||||
val cacheDirectory = workdir.resolve("cache")
|
||||
val integrations = workdir.resolve("integrations.apk")
|
||||
try {
|
||||
Log.d(tag, "Creating patcher")
|
||||
val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
inputFile,
|
||||
cacheDirectory.absolutePath,
|
||||
patchResources = false,
|
||||
aaptPath = aaptPath,
|
||||
frameworkFolderLocation = frameworkPath,
|
||||
logger = object : Logger {
|
||||
override fun error(msg: String) {
|
||||
Log.e(tag, msg)
|
||||
}
|
||||
|
||||
override fun warn(msg: String) {
|
||||
Log.w(tag, msg)
|
||||
}
|
||||
|
||||
override fun info(msg: String) {
|
||||
Log.i(tag, msg)
|
||||
}
|
||||
|
||||
override fun trace(msg: String) {
|
||||
Log.v(tag, msg)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Log.d(tag, "Adding ${patches.size} patch(es)")
|
||||
patcher.addPatches(patches)
|
||||
|
||||
|
||||
Log.d(tag, "Applying patches")
|
||||
patcher.applyPatches().forEach { (patch, result) ->
|
||||
if (result.isSuccess) {
|
||||
Log.i(tag, "[success] $patch")
|
||||
return@forEach
|
||||
}
|
||||
Log.e(tag, "[error] $patch:", result.exceptionOrNull()!!)
|
||||
}
|
||||
Log.d(tag, "Saving file")
|
||||
|
||||
val result = patcher.save() // this function uses quite a bit of resources
|
||||
ZipFile(patchedFile).use { fs ->
|
||||
result.dexFiles.forEach { it ->
|
||||
Log.d(tag, "Writing dex file ${it.name}")
|
||||
fs.addEntryCompressData(
|
||||
ZipEntry.createWithName(it.name),
|
||||
it.stream.readBytes()
|
||||
)
|
||||
result.resourceFile?.let {
|
||||
fs.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment)
|
||||
}
|
||||
fs.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Log.d(tag, "Signing apk")
|
||||
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outputFile)
|
||||
Log.i(tag, "Successfully patched into $outputFile")
|
||||
} finally {
|
||||
Log.d(tag, "Deleting workdir")
|
||||
workdir.deleteRecursively()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<Data>>> {
|
||||
val (patches) = patches.value as? Resource.Success ?: return listOf()
|
||||
return patches.filter { patch -> ids.any { it == patch.patchName } }
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ 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
|
||||
@ -13,7 +14,7 @@ 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)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package app.revanced.manager.repository
|
||||
|
||||
import app.revanced.manager.dto.github.ApiCommit
|
||||
import app.revanced.manager.dto.github.ApiContributor
|
||||
import app.revanced.manager.dto.github.ApiRelease
|
||||
import app.revanced.manager.dto.github.APICommit
|
||||
import app.revanced.manager.dto.github.APIContributor
|
||||
import app.revanced.manager.dto.github.APIRelease
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
@ -11,20 +11,19 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class GitHubRepository(private val client: HttpClient) {
|
||||
suspend fun getLatestRelease(repo: String) = withContext(Dispatchers.IO) {
|
||||
val res: List<ApiRelease> = client.get("$baseUrl/$repo/releases") {
|
||||
val res: List<APIRelease> = client.get("$baseUrl/$repo/releases") {
|
||||
parameter("per_page", 1)
|
||||
}.body()
|
||||
res.first()
|
||||
}
|
||||
|
||||
suspend fun getLatestCommit(repo: String, ref: String) = withContext(Dispatchers.IO) {
|
||||
client.get("$baseUrl/$repo/commits/$ref") {
|
||||
parameter("per_page", 1)
|
||||
}.body() as ApiCommit
|
||||
}.body() as APICommit
|
||||
}
|
||||
|
||||
suspend fun getContributors(org: String, repo: String) = withContext(Dispatchers.IO) {
|
||||
client.get("$baseUrl/$org/$repo/contributors").body() as List<ApiContributor>
|
||||
client.get("$baseUrl/$org/$repo/contributors").body() as List<APIContributor>
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
10
app/src/main/java/app/revanced/manager/ui/Resource.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package app.revanced.manager.ui
|
||||
|
||||
sealed class Resource<out T> {
|
||||
object Loading : Resource<Nothing>()
|
||||
data class Success<out T>(val data: T) : Resource<T>()
|
||||
|
||||
companion object {
|
||||
fun <T> success(value: T) = Success(value)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
|
||||
@Composable
|
||||
fun AppIcon(drawable: Drawable?, contentDescription: String?) {
|
||||
Image(
|
||||
rememberDrawablePainter(drawable),
|
||||
contentDescription,
|
||||
Modifier.size(48.dp)
|
||||
)
|
||||
}
|
@ -1,16 +1,12 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun ApplicationItem(
|
||||
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.ripple.LocalRippleTheme
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material.ripple.RippleTheme
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun FloatingActionButton(
|
||||
text: @Composable () -> Unit,
|
||||
icon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// TODO: set icon color:
|
||||
// tint = if (enabled) LocalContentColor.current.copy(alpha = LocalContentAlpha.current) else else DarkGray
|
||||
CompositionLocalProvider(
|
||||
LocalRippleTheme provides if (enabled) {
|
||||
LocalRippleTheme.current
|
||||
} else NoRippleTheme
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = text,
|
||||
icon = icon,
|
||||
onClick = {
|
||||
if (!enabled) {
|
||||
Toast.makeText(context, "Please select an application.", Toast.LENGTH_SHORT)
|
||||
}
|
||||
if (enabled) onClick()
|
||||
},
|
||||
containerColor = if (enabled) MaterialTheme.colorScheme.primaryContainer else Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object NoRippleTheme : RippleTheme {
|
||||
@Composable
|
||||
override fun defaultColor() = Color.Unspecified
|
||||
|
||||
@Composable
|
||||
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(
|
||||
title: String,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = color,
|
||||
fontSize = LocalTextStyle.current.fontSize.times(0.95f),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun LoadingIndicator(id: Int? = null) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.loading_body))
|
||||
if (id != null) Text(stringResource(id))
|
||||
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
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.ui.viewmodel.PatchClass
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import app.revanced.patcher.annotation.Package
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@Composable
|
||||
fun PatchCompatibilityDialog(
|
||||
patchClass: PatchClass, pvm: PatcherViewModel = getViewModel(), onClose: () -> Unit
|
||||
) {
|
||||
val patch = patchClass.patch
|
||||
val packageName = pvm.getSelectedPackageInfo()?.packageName
|
||||
AlertDialog(onDismissRequest = onClose, shape = RoundedCornerShape(12.dp), title = {
|
||||
Text(stringResource(id = R.string.unsupported), textAlign = TextAlign.Center)
|
||||
}, text = {
|
||||
(patch.compatiblePackages!!.forEach { p: Package ->
|
||||
if (p.name == packageName) {
|
||||
Text(
|
||||
stringResource(id = R.string.only_compatible) + p.versions.reversed()
|
||||
.joinToString(", ")
|
||||
)
|
||||
}
|
||||
})
|
||||
}, confirmButton = {
|
||||
TextButton(onClick = onClose) {
|
||||
Text(text = "Dismiss")
|
||||
}
|
||||
})
|
||||
}
|
@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.util.ghOrganization
|
||||
import app.revanced.manager.util.openUrl
|
||||
@ -34,7 +33,6 @@ fun SocialItem(@StringRes label: Int, vec: ImageVector, fn: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SocialItemPreview() {
|
||||
val ctx = LocalContext.current.applicationContext
|
||||
|
@ -15,6 +15,12 @@ import kotlinx.parcelize.RawValue
|
||||
sealed interface AppDestination : Destination {
|
||||
@Parcelize
|
||||
object Dashboard : AppDestination
|
||||
|
||||
@Parcelize
|
||||
object AppSelector : AppDestination
|
||||
|
||||
@Parcelize
|
||||
object PatchSelector : AppDestination
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
@ -91,46 +91,24 @@ fun DashboardScreen(viewModel: DashboardViewModel = getViewModel()) {
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ApplicationItem(
|
||||
name = "ReVanced",
|
||||
released = "com.google.android.youtube",
|
||||
name = "Youtube ReVanced",
|
||||
released = "Released [who knows] centuries ago",
|
||||
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
|
||||
) {
|
||||
ChangelogText(
|
||||
"""
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
cossal will explode
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
ApplicationItem(
|
||||
name = "ReReddit",
|
||||
released = "Released 1 month ago",
|
||||
name = "Reddit ReVanced",
|
||||
released = "Released [REDACTED] month ago",
|
||||
icon = { Icon(Icons.Default.Build, "ReReddit") }
|
||||
) {
|
||||
ChangelogText(
|
||||
"""
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
hi ushie
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.with
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -19,8 +22,8 @@ import com.xinto.taxi.rememberNavigator
|
||||
@Composable
|
||||
fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
decayAnimationSpec = rememberSplineBasedDecay(),
|
||||
state = rememberTopAppBarState()
|
||||
state = rememberTopAppBarState(),
|
||||
canScroll = { true }
|
||||
)
|
||||
|
||||
val mainRootNavigator = rememberNavigator(DashboardDestination.DASHBOARD)
|
||||
@ -31,7 +34,7 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
MediumTopAppBar(
|
||||
LargeTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(mainRootNavigator.currentDestination.label),
|
||||
@ -64,7 +67,10 @@ fun MainDashboardScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
DashboardDestination.DASHBOARD -> DashboardScreen()
|
||||
DashboardDestination.PATCHER -> PatcherScreen()
|
||||
DashboardDestination.PATCHER -> PatcherScreen(
|
||||
onClickAppSelector = { navigator.push(AppDestination.AppSelector) },
|
||||
onClickPatchSelector = { navigator.push(AppDestination.PatchSelector) }
|
||||
)
|
||||
DashboardDestination.SETTINGS -> SettingsScreen()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FolderZip
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.PatcherViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewPatcherScreen(
|
||||
onClickAppSelector: () -> Unit,
|
||||
onClickPatchSelector: () -> Unit,
|
||||
viewModel: PatcherViewModel = getViewModel()
|
||||
) {
|
||||
var validBundle = false
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
ElevatedCard(
|
||||
onClick = onClickAppSelector,
|
||||
modifier = Modifier
|
||||
.padding(16.dp, 4.dp)
|
||||
.animateContentSize(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp, 8.dp, 12.dp, 8.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(modifier = Modifier.padding(0.dp, 12.dp)) {
|
||||
Icon(
|
||||
Icons.Default.FolderZip,
|
||||
"Patch Bundle",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
if (!validBundle) {
|
||||
Text(
|
||||
text = "Select a patch bundle",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(text = "(not selected)", fontSize = 13.sp)
|
||||
} else {
|
||||
Text(
|
||||
text = "Selected patch bundle",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = viewModel.getSelectedPackageInfo()!!.applicationInfo.name,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,155 +1,96 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Dashboard
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ApplicationItem
|
||||
import app.revanced.manager.ui.component.HeadlineWithCard
|
||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||
import app.revanced.manager.Variables.patches
|
||||
import app.revanced.manager.Variables.selectedAppPackage
|
||||
import app.revanced.manager.Variables.selectedPatches
|
||||
import app.revanced.manager.ui.Resource
|
||||
import app.revanced.manager.ui.component.FloatingActionButton
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatcherScreen(viewModel: DashboardViewModel = getViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val padHoriz = 16.dp
|
||||
val padVert = 10.dp
|
||||
fun PatcherScreen(
|
||||
onClickAppSelector: () -> Unit,
|
||||
onClickPatchSelector: () -> Unit,
|
||||
viewModel: PatcherViewModel = getViewModel()
|
||||
) {
|
||||
val selectedAmount = selectedPatches.size
|
||||
val selectedAppPackage by selectedAppPackage
|
||||
val hasAppSelected = selectedAppPackage.isPresent
|
||||
val patchesLoaded = patches.value is Resource.Success
|
||||
|
||||
Scaffold(floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
enabled = hasAppSelected && viewModel.anyPatchSelected(),
|
||||
onClick = { viewModel.startPatcher() },
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
|
||||
text = { Text(text = "Patch") }
|
||||
)
|
||||
}) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 18.dp)
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
HeadlineWithCard(R.string.updates) {
|
||||
Row(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = padHoriz, vertical = padVert)
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
enabled = patchesLoaded,
|
||||
onClick = onClickAppSelector
|
||||
) {
|
||||
Column {
|
||||
CommitDate(
|
||||
label = R.string.patcher,
|
||||
date = viewModel.patcherCommitDate
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.card_application_header),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
CommitDate(
|
||||
label = R.string.manager,
|
||||
date = viewModel.managerCommitDate
|
||||
Text(
|
||||
text = if (patchesLoaded) {
|
||||
selectedAppPackage.orElse(stringResource(R.string.card_application_not_selected))
|
||||
} else {
|
||||
stringResource(R.string.card_application_not_loaded)},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(0.dp, 8.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Button(
|
||||
enabled = false, // needs update
|
||||
onClick = {
|
||||
Toast.makeText(context, "Already up-to-date!", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
enabled = hasAppSelected,
|
||||
onClick = onClickPatchSelector
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.card_patches_header),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = if (!hasAppSelected) {
|
||||
"Select an application first."
|
||||
} else if (viewModel.anyPatchSelected()) {
|
||||
"$selectedAmount patches selected."
|
||||
} else {
|
||||
stringResource(R.string.card_patches_body_patches)
|
||||
},
|
||||
) { Text(stringResource(R.string.update_patch_bundle)) }
|
||||
Text(
|
||||
text = "No updates available",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HeadlineWithCard(R.string.patched_apps) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = padHoriz, vertical = padVert)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column {
|
||||
val amount = 2 // TODO
|
||||
Text(
|
||||
text = "${stringResource(R.string.updates_available)}: $amount",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Button(
|
||||
enabled = true, // needs update
|
||||
onClick = {
|
||||
Toast.makeText(context, "Already up-to-date!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
) { Text(stringResource(R.string.update_all)) }
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = padHoriz)
|
||||
.padding(bottom = padVert)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ApplicationItem(
|
||||
name = "ReVanced",
|
||||
released = "Released 2 days ago",
|
||||
icon = { Icon(Icons.Default.Dashboard, "ReVanced") }
|
||||
) {
|
||||
ChangelogText(
|
||||
"""
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
fix: aaaaaa
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
ApplicationItem(
|
||||
name = "ReReddit",
|
||||
released = "Released 1 month ago",
|
||||
icon = { Icon(Icons.Default.Build, "ReReddit") }
|
||||
) {
|
||||
ChangelogText(
|
||||
"""
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
fix: bbbbbb
|
||||
""".trimIndent()
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(0.dp, 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,27 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.NavigateBefore
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.Style
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.SocialItem
|
||||
import app.revanced.manager.ui.navigation.AppDestination
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import com.xinto.taxi.BackstackNavigator
|
||||
import org.koin.androidx.compose.get
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@ -35,9 +36,26 @@ fun SettingsScreen(viewModel: SettingsViewModel = getViewModel()) {
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (viewModel.showThemePicker) {
|
||||
ThemePicker(
|
||||
onDismissRequest = viewModel::dismissThemePicker,
|
||||
onConfirm = viewModel::setTheme
|
||||
)
|
||||
}
|
||||
GroupHeader(title = "Appearance")
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { viewModel.showThemePicker() },
|
||||
headlineText = { Text(stringResource(R.string.theme)) },
|
||||
leadingContent = { Icon(Icons.Default.Style, contentDescription = null) },
|
||||
trailingContent = { FilledTonalButton(onClick = { viewModel.showThemePicker() }) {
|
||||
Text(text = prefs.theme.displayName)
|
||||
} }
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { prefs.dynamicColor = !prefs.dynamicColor },
|
||||
headlineText = { Text(stringResource(R.string.dynamic_color)) },
|
||||
leadingContent = { Icon(Icons.Default.Palette, contentDescription = null) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = prefs.dynamicColor,
|
||||
@ -45,8 +63,55 @@ fun SettingsScreen(viewModel: SettingsViewModel = getViewModel()) {
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
SocialItem(R.string.github, Icons.Default.Code, viewModel::openGitHub)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemePicker(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: (Theme) -> Unit,
|
||||
prefs: PreferencesManager = get()
|
||||
) {
|
||||
var selectedTheme by remember { mutableStateOf(prefs.theme) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(stringResource(R.string.theme)) },
|
||||
text = {
|
||||
Column {
|
||||
Theme.values().forEach { theme ->
|
||||
Row(
|
||||
modifier = Modifier.clickable { selectedTheme = theme },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
theme.displayName,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f, true))
|
||||
|
||||
RadioButton(
|
||||
selected = theme == selectedTheme,
|
||||
onClick = { selectedTheme = theme }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(selectedTheme)
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package app.revanced.manager.ui.screen.subscreens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.Variables.filteredApps
|
||||
import app.revanced.manager.Variables.patchesState
|
||||
import app.revanced.manager.ui.Resource
|
||||
import app.revanced.manager.ui.component.AppIcon
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.navigation.AppDestination
|
||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||
import com.xinto.taxi.BackstackNavigator
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@Composable
|
||||
fun AppSelectorSubscreen(
|
||||
navigator: BackstackNavigator<AppDestination>,
|
||||
vm: AppSelectorViewModel = getViewModel(),
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MediumTopAppBar(
|
||||
title = { Text(stringResource(R.string.app_selector_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
when (patchesState) {
|
||||
is Resource.Success -> {
|
||||
LazyColumn(modifier = Modifier.padding(paddingValues)) {
|
||||
items(count = filteredApps.size) {
|
||||
val app = filteredApps[it]
|
||||
val label = vm.applicationLabel(app)
|
||||
val packageName = app.packageName
|
||||
|
||||
val same = packageName == label
|
||||
ListItem(modifier = Modifier.clickable {
|
||||
vm.setSelectedAppPackage(app.packageName)
|
||||
navigator.pop()
|
||||
}, leadingContent = {
|
||||
AppIcon(vm.loadIcon(app), packageName)
|
||||
}, headlineText = {
|
||||
if (same) {
|
||||
Text(packageName)
|
||||
} else {
|
||||
Text(label)
|
||||
}
|
||||
}, supportingText = {
|
||||
if (!same) {
|
||||
Text(packageName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> LoadingIndicator(null)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,244 @@
|
||||
package app.revanced.manager.ui.screen.subscreens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.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.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.PatcherViewModel
|
||||
import app.revanced.patcher.extensions.PatchExtensions.description
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.extensions.PatchExtensions.version
|
||||
import com.xinto.taxi.BackstackNavigator
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatchesSelectorSubscreen(
|
||||
navigator: BackstackNavigator<AppDestination>,
|
||||
pvm: PatcherViewModel = getViewModel(),
|
||||
) {
|
||||
val patches = rememberSaveable { pvm.getFilteredPatchesAndCheckOptions() }
|
||||
var query by mutableStateOf("")
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MediumTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.card_patches_header),
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
pvm.selectAllPatches(patches, !pvm.anyPatchSelected())
|
||||
}) {
|
||||
if (!pvm.anyPatchSelected()) Icon(
|
||||
Icons.Default.SelectAll,
|
||||
contentDescription = null
|
||||
) else Icon(Icons.Default.Deselect, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (patchesState) {
|
||||
is Resource.Success -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp, 4.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
value = query,
|
||||
onValueChange = { newValue ->
|
||||
query = newValue
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Search, "Search")
|
||||
},
|
||||
trailingIcon = {
|
||||
if (query.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
query = ""
|
||||
}) {
|
||||
Icon(Icons.Default.Clear, "Clear")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(Modifier.padding(0.dp, 2.dp)) {
|
||||
|
||||
if (query.isEmpty() || query.isBlank()) {
|
||||
items(count = patches.size) {
|
||||
val patch = patches[it]
|
||||
val name = patch.patch.patchName
|
||||
PatchCard(patch, pvm.isPatchSelected(name)) {
|
||||
pvm.selectPatch(name, !pvm.isPatchSelected(name))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(count = patches.size) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> LoadingIndicator(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatchCard(patchClass: PatchClass, isSelected: Boolean, onSelected: () -> Unit) {
|
||||
val patch = patchClass.patch
|
||||
val name = patch.patchName
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.padding(16.dp, 4.dp),
|
||||
enabled = !patchClass.unsupported,
|
||||
onClick = onSelected
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp, 0.dp, 12.dp, 12.dp)) {
|
||||
Row {
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = name.replace("-", " ").split(" ")
|
||||
.joinToString(" ") { it.replaceFirstChar(Char::uppercase) },
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Row(
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = patch.version ?: "unknown",
|
||||
style = Typography.bodySmall
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.weight(1f, true))
|
||||
Column(modifier = Modifier.padding(0.dp, 6.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
if (patchClass.hasPatchOptions) {
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
IconButton(onClick = { }, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
Icons.Outlined.Settings,
|
||||
contentDescription = "Patch Options"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
Checkbox(
|
||||
enabled = !patchClass.unsupported,
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onSelected() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
patch.description?.let { desc ->
|
||||
Text(
|
||||
text = desc,
|
||||
modifier = Modifier
|
||||
.padding(0.dp, 8.dp, 22.dp, 8.dp)
|
||||
.clickable { isExpanded = !isExpanded },
|
||||
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
if (patchClass.unsupported) {
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
Column {
|
||||
Row {
|
||||
if (showDialog) {
|
||||
PatchCompatibilityDialog(
|
||||
onClose = { showDialog = false },
|
||||
patchClass = patchClass,
|
||||
)
|
||||
}
|
||||
InputChip(
|
||||
selected = false,
|
||||
onClick = { showDialog = true },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(id = R.string.unsupported_version)
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.unsupported_version)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -49,3 +49,8 @@ fun ReVancedManagerTheme(
|
||||
content = content
|
||||
)
|
||||
}
|
||||
enum class Theme(val displayName: String) {
|
||||
SYSTEM("System"),
|
||||
LIGHT("Light"),
|
||||
DARK("Dark");
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.Variables
|
||||
import app.revanced.manager.Variables.filteredApps
|
||||
import app.revanced.manager.Variables.patches
|
||||
import app.revanced.manager.Variables.selectedAppPackage
|
||||
import app.revanced.manager.ui.Resource
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import java.util.*
|
||||
|
||||
class AppSelectorViewModel(
|
||||
val app: Application,
|
||||
) : ViewModel() {
|
||||
|
||||
init {
|
||||
filterApps()
|
||||
}
|
||||
|
||||
private fun filterApps(): List<ApplicationInfo> {
|
||||
try {
|
||||
val (patches) = patches.value as Resource.Success
|
||||
patches.forEach patch@{ patch ->
|
||||
patch.compatiblePackages?.forEach { pkg ->
|
||||
try {
|
||||
val appInfo = app.packageManager.getApplicationInfo(pkg.name, 0)
|
||||
if (appInfo !in filteredApps) {
|
||||
filteredApps.add(appInfo)
|
||||
return@forEach
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ReVanced Manager", "An error occurred while filtering", e)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun applicationLabel(info: ApplicationInfo): String {
|
||||
return app.packageManager.getApplicationLabel(info).toString()
|
||||
}
|
||||
|
||||
fun loadIcon(info: ApplicationInfo): Drawable? {
|
||||
return info.loadIcon(app.packageManager)
|
||||
}
|
||||
|
||||
fun setSelectedAppPackage(appId: String) {
|
||||
selectedAppPackage.value.ifPresent { s ->
|
||||
if (s != appId) Variables.selectedPatches.clear()
|
||||
}
|
||||
selectedAppPackage.value = Optional.of(appId)
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class NewPatcherScreenViewModel(
|
||||
val app: Application,
|
||||
) : ViewModel() {
|
||||
fun loadIcon(info: ApplicationInfo): Drawable? {
|
||||
return info.loadIcon(app.packageManager)
|
||||
}
|
||||
}
|
@ -1,40 +1,164 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.repository.GitHubRepository
|
||||
import app.revanced.manager.util.ghManager
|
||||
import app.revanced.manager.util.ghPatcher
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkManager
|
||||
import app.revanced.manager.Variables.patches
|
||||
import app.revanced.manager.Variables.selectedAppPackage
|
||||
import app.revanced.manager.Variables.selectedPatches
|
||||
import app.revanced.manager.api.API
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.ui.Resource
|
||||
import app.revanced.patcher.data.Data
|
||||
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.impl.DexPatchBundle
|
||||
import dalvik.system.DexClassLoader
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
|
||||
class PatcherViewModel(private val repository: GitHubRepository) : ViewModel() {
|
||||
var patcherCommitDate by mutableStateOf("")
|
||||
private set
|
||||
var managerCommitDate by mutableStateOf("")
|
||||
private set
|
||||
class PatcherViewModel(private val app: Application, private val api: API) : ViewModel() {
|
||||
private val workdir = createWorkDir()
|
||||
private lateinit var patchBundleFile: String
|
||||
private val tag = "ReVanced Manager"
|
||||
|
||||
init {
|
||||
runBlocking {
|
||||
patcherCommitDate = commitDateOf(ghPatcher)
|
||||
managerCommitDate = commitDateOf(ghManager)
|
||||
loadPatches()
|
||||
downloadIntegrations()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commitDateOf(repo: String, ref: String = "HEAD"): String {
|
||||
val commit = repository.getLatestCommit(repo, ref).commit
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
formatter.parse(commit.committer.date)!!.time,
|
||||
Calendar.getInstance().timeInMillis,
|
||||
DateUtils.MINUTE_IN_MILLIS
|
||||
).toString()
|
||||
fun selectPatch(patchId: String, state: Boolean) {
|
||||
if (state) selectedPatches.add(patchId)
|
||||
else selectedPatches.remove(patchId)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
|
||||
private suspend fun downloadIntegrations() {
|
||||
api.downloadIntegrations(workdir).renameTo(File(workdir,"integrations.apk"))
|
||||
}
|
||||
|
||||
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() =
|
||||
if (selectedAppPackage.value.isPresent)
|
||||
app.packageManager.getPackageInfo(
|
||||
selectedAppPackage.value.get(),
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
else null
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPatches() = viewModelScope.launch {
|
||||
try {
|
||||
val file = api.downloadPatchBundle(app.filesDir)
|
||||
patchBundleFile = file.absolutePath
|
||||
loadPatches0(file.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ReVancedManager", "An error occurred while loading patches", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun loadPatches0(path: String) {
|
||||
val patchClasses = DexPatchBundle(
|
||||
path, DexClassLoader(
|
||||
path,
|
||||
app.codeCacheDir.absolutePath,
|
||||
null,
|
||||
javaClass.classLoader
|
||||
)
|
||||
).loadPatches()
|
||||
patches.value = Resource.Success(patchClasses)
|
||||
Log.d("ReVanced Manager", "Finished loading patches")
|
||||
}
|
||||
|
||||
fun startPatcher() {
|
||||
WorkManager
|
||||
.getInstance(app)
|
||||
.enqueueUniqueWork(
|
||||
"patching",
|
||||
ExistingWorkPolicy.KEEP,
|
||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.setInputData(
|
||||
androidx.work.Data.Builder()
|
||||
.putString("workdir", workdir.toString())
|
||||
.put("input",
|
||||
getSelectedPackageInfo()?.applicationInfo?.publicSourceDir
|
||||
)
|
||||
.build()).build()
|
||||
)
|
||||
}
|
||||
private fun createWorkDir(): File {
|
||||
return app.filesDir.resolve("tmp-${System.currentTimeMillis()}")
|
||||
.also { it.mkdirs() }
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class PatchClass(
|
||||
val patch: Class<out Patch<Data>>,
|
||||
val unsupported: Boolean,
|
||||
val hasPatchOptions: Boolean,
|
||||
) : Parcelable
|
@ -1,8 +1,12 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
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.ui.theme.Theme
|
||||
import app.revanced.manager.util.ghOrganization
|
||||
import app.revanced.manager.util.openUrl
|
||||
|
||||
@ -10,5 +14,19 @@ class SettingsViewModel(
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
var showThemePicker by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun showThemePicker() {
|
||||
showThemePicker = true
|
||||
}
|
||||
|
||||
fun dismissThemePicker() {
|
||||
showThemePicker = false
|
||||
}
|
||||
|
||||
fun setTheme(theme: Theme) {
|
||||
prefs.theme = theme
|
||||
}
|
||||
fun openGitHub() = app.openUrl(ghOrganization)
|
||||
}
|
BIN
app/src/main/jniLibs/arm64-v8a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/armeabi-v7a/libaapt2.so
Normal file
BIN
app/src/main/jniLibs/x86/libaapt2.so
Normal file
@ -2,29 +2,48 @@
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
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:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:startX="45.43"
|
||||
android:startY="132.25"
|
||||
android:endX="87.19"
|
||||
android:endY="221.04"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
<item android:offset="0" android:color="#FF353DFF"/>
|
||||
<item android:offset="1" android:color="#FF3DDCFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
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>
|
44
app/src/main/res/drawable-v24/manager.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<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>
|
@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
49
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<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>
|
44
app/src/main/res/drawable/manager.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<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>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 7.6 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#111623</color>
|
||||
</resources>
|
@ -13,4 +13,66 @@
|
||||
<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="patcher_contributors">Patcher Contributors</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 Contributors</string>
|
||||
<string name="patches_contributors">Patches Contributors</string>
|
||||
<string name="manager_contributors">Manager Contributors</string>
|
||||
<string name="integrations_contributors">Integrations Contributors</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>
|
||||
<string name="apply">Apply</string>
|
||||
</resources>
|
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ReVancedManager" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.ReVancedManager" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,22 +1,21 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
extra.apply {
|
||||
// Global variable for some dependencies
|
||||
set("compose_version", "1.3.0-rc02")
|
||||
set("ktor_version", "2.0.1")
|
||||
set("room_version", "2.4.2")
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.4.0-alpha10")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle.kts files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
plugins {
|
||||
id("com.android.application") version "7.4.0-alpha10" apply false
|
||||
id("com.android.library") version "7.4.0-alpha10" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.7.10" apply false
|
||||
id("com.google.devtools.ksp") version "1.7.10-+" apply false
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
}
|
@ -1,2 +1,15 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "ReVanced Manager"
|
||||
include(":app")
|