diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..174508c --- /dev/null +++ b/.github/dependabot.yml @@ -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" diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..6734c27 --- /dev/null +++ b/.github/workflows/android.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3e1a6af --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98b0eba..55d494f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") -} \ No newline at end of file + + // 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") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ff59496..17059ed 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file +# 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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be63a29..129d5e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,26 +2,46 @@ + + + + + + + + + + + + + + + + + tools:targetApi="33"> + - diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..4f3eabe Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index b002462..b648f94 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -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.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) } } } diff --git a/app/src/main/java/app/revanced/manager/Variables.kt b/app/src/main/java/app/revanced/manager/Variables.kt new file mode 100644 index 0000000..1c41755 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/Variables.kt @@ -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()) + val selectedPatches = mutableStateListOf() + val patches = mutableStateOf>>>>(Resource.Loading) + val patchesState by patches + val filteredApps = mutableListOf() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/api/API.kt b/app/src/main/java/app/revanced/manager/api/API.kt new file mode 100644 index 0000000..288b0b0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/api/API.kt @@ -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.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 { + 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() \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt index 6671f75..f5edaa8 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index d6698d1..3bc6759 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/dto/github/ApiCommit.kt b/app/src/main/java/app/revanced/manager/dto/github/ApiCommit.kt index 7e1a34f..78a6eb7 100644 --- a/app/src/main/java/app/revanced/manager/dto/github/ApiCommit.kt +++ b/app/src/main/java/app/revanced/manager/dto/github/ApiCommit.kt @@ -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 ) { diff --git a/app/src/main/java/app/revanced/manager/dto/github/ApiContributor.kt b/app/src/main/java/app/revanced/manager/dto/github/ApiContributor.kt index ab62670..c42d680 100644 --- a/app/src/main/java/app/revanced/manager/dto/github/ApiContributor.kt +++ b/app/src/main/java/app/revanced/manager/dto/github/ApiContributor.kt @@ -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, diff --git a/app/src/main/java/app/revanced/manager/dto/github/ApiRelease.kt b/app/src/main/java/app/revanced/manager/dto/github/ApiRelease.kt index d25c7b7..a878e06 100644 --- a/app/src/main/java/app/revanced/manager/dto/github/ApiRelease.kt +++ b/app/src/main/java/app/revanced/manager/dto/github/ApiRelease.kt @@ -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, diff --git a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt new file mode 100644 index 0000000..f0fea12 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt @@ -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()) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aligning/ZipAligner.kt b/app/src/main/java/app/revanced/manager/patcher/aligning/ZipAligner.kt new file mode 100644 index 0000000..a6954e6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aligning/ZipAligner.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aligning/zip/Extensions.kt b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/Extensions.kt new file mode 100644 index 0000000..7e9d5ec --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/Extensions.kt @@ -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()) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aligning/zip/ZipFile.kt b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/ZipFile.kt new file mode 100644 index 0000000..eb26091 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/ZipFile.kt @@ -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 = 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 { + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEndRecord.kt b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEndRecord.kt new file mode 100644 index 0000000..6348c8a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEndRecord.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEntry.kt b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEntry.kt new file mode 100644 index 0000000..3c08f3e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aligning/zip/structures/ZipEntry.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/signing/Signer.kt b/app/src/main/java/app/revanced/manager/patcher/signing/Signer.kt new file mode 100644 index 0000000..9057ca5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/signing/Signer.kt @@ -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 { + 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() + } +} diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt new file mode 100644 index 0000000..1c515fb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -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): List>> { + val (patches) = patches.value as? Resource.Success ?: return listOf() + return patches.filter { patch -> ids.any { it == patch.patchName } } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/preferences/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/preferences/PreferencesManager.kt index f6bda4b..8a6b576 100644 --- a/app/src/main/java/app/revanced/manager/preferences/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/preferences/PreferencesManager.kt @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/repository/GitHubRepository.kt b/app/src/main/java/app/revanced/manager/repository/GitHubRepository.kt index 1bc6e0b..bc24b38 100644 --- a/app/src/main/java/app/revanced/manager/repository/GitHubRepository.kt +++ b/app/src/main/java/app/revanced/manager/repository/GitHubRepository.kt @@ -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 = client.get("$baseUrl/$repo/releases") { + val res: List = 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 + client.get("$baseUrl/$org/$repo/contributors").body() as List } private companion object { diff --git a/app/src/main/java/app/revanced/manager/ui/Resource.kt b/app/src/main/java/app/revanced/manager/ui/Resource.kt new file mode 100644 index 0000000..f022cb1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/Resource.kt @@ -0,0 +1,10 @@ +package app.revanced.manager.ui + +sealed class Resource { + object Loading : Resource() + data class Success(val data: T) : Resource() + + companion object { + fun success(value: T) = Success(value) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt new file mode 100644 index 0000000..9d6590a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ApplicationItem.kt b/app/src/main/java/app/revanced/manager/ui/component/ApplicationItem.kt index c80c1f6..ad820ec 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ApplicationItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ApplicationItem.kt @@ -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( diff --git a/app/src/main/java/app/revanced/manager/ui/component/FloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/FloatingActionButton.kt new file mode 100644 index 0000000..6006f60 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/FloatingActionButton.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt b/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt new file mode 100644 index 0000000..de56581 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt new file mode 100644 index 0000000..2b10436 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/PatchCompatibilityDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/PatchCompatibilityDialog.kt new file mode 100644 index 0000000..46a12b9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/PatchCompatibilityDialog.kt @@ -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") + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SocialItem.kt b/app/src/main/java/app/revanced/manager/ui/component/SocialItem.kt index 5dc936d..cff15f1 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/SocialItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/SocialItem.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/navigation/AppDestination.kt b/app/src/main/java/app/revanced/manager/ui/navigation/AppDestination.kt index 28e4e0b..20a493c 100644 --- a/app/src/main/java/app/revanced/manager/ui/navigation/AppDestination.kt +++ b/app/src/main/java/app/revanced/manager/ui/navigation/AppDestination.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 2ea0d25..8d52f32 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -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() ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/MainDashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/MainDashboardScreen.kt index 9c4b206..8c79b2b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/MainDashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/MainDashboardScreen.kt @@ -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) { 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) { .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - MediumTopAppBar( + LargeTopAppBar( title = { Text( text = stringResource(mainRootNavigator.currentDestination.label), @@ -64,7 +67,10 @@ fun MainDashboardScreen(navigator: BackstackNavigator) { ) { 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() } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/NewPatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/NewPatcherScreen.kt new file mode 100644 index 0000000..fd1b664 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/NewPatcherScreen.kt @@ -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)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 7d2c28b..654d011 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -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 - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 18.dp) - .verticalScroll(state = rememberScrollState()), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - HeadlineWithCard(R.string.updates) { - Row( + 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(paddingValues) + .padding(16.dp), + ) { + 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) ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index b57280f..e331c43 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -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,18 +36,82 @@ 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 { prefs.dynamicColor = !prefs.dynamicColor }, - headlineText = { Text(stringResource(R.string.dynamic_color)) }, - trailingContent = { - Switch( - checked = prefs.dynamicColor, - onCheckedChange = { prefs.dynamicColor = it } - ) - } + 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, + onCheckedChange = { prefs.dynamicColor = it } + ) + } + ) + } 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)) + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/subscreens/AppSelectorSubscreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/AppSelectorSubscreen.kt new file mode 100644 index 0000000..c7ba613 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/AppSelectorSubscreen.kt @@ -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, + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchesSelectorSubscreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchesSelectorSubscreen.kt new file mode 100644 index 0000000..d2a26c2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchesSelectorSubscreen.kt @@ -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, + 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)) } + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt index c443757..9b11194 100644 --- a/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt +++ b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt @@ -48,4 +48,9 @@ fun ReVancedManagerTheme( typography = Typography, content = content ) +} +enum class Theme(val displayName: String) { + SYSTEM("System"), + LIGHT("Light"), + DARK("Dark"); } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt new file mode 100644 index 0000000..f809f9c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -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 { + 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) + + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/NewPatcherScreenViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/NewPatcherScreenViewModel.kt new file mode 100644 index 0000000..ba81047 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/NewPatcherScreenViewModel.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 33388c6..dae569f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -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")) } -} \ No newline at end of file + + fun selectAllPatches(patchList: List, 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 { + 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>, + val unsupported: Boolean, + val hasPatchOptions: Boolean, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt index b152ab0..67fb078 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/jniLibs/arm64-v8a/libaapt2.so b/app/src/main/jniLibs/arm64-v8a/libaapt2.so new file mode 100644 index 0000000..4014ef0 Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libaapt2.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libaapt2.so b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so new file mode 100644 index 0000000..a43e091 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so differ diff --git a/app/src/main/jniLibs/x86/libaapt2.so b/app/src/main/jniLibs/x86/libaapt2.so new file mode 100644 index 0000000..097b817 Binary files /dev/null and b/app/src/main/jniLibs/x86/libaapt2.so differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 2b068d1..56c982f 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -2,29 +2,48 @@ xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" - android:viewportWidth="108" - android:viewportHeight="108"> - - - - - - - + android:viewportWidth="278" + android:viewportHeight="278"> + + + + + + + + + + - \ No newline at end of file + 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"> + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/manager.xml b/app/src/main/res/drawable-v24/manager.xml new file mode 100644 index 0000000..ea8eca4 --- /dev/null +++ b/app/src/main/res/drawable-v24/manager.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..56c982f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/manager.xml b/app/src/main/res/drawable/manager.xml new file mode 100644 index 0000000..ea8eca4 --- /dev/null +++ b/app/src/main/res/drawable/manager.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..69daed6 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..11efc56 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..2714744 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..7e82744 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..ca2e35b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..19e678e Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..63b16c6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..bdd2ceb Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..84f5dd9 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c5bd9af Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..1650e59 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #111623 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8508529..88ca5e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,4 +13,66 @@ Available updates Expand Update + Select an app… + Discord + GitHub + Announcement + Latest updates + Logs + Select Application + Select Patches + Options + Contributors + Patcher: + Manager: + This is an example text for previewing ReVanced Manager design that will be replaced by dynamic announcements in the future. + Root + Use installed YouTube app + Loading applications. + No application selected. + No patches source selected. + No patches selected. + Selected: + Click here to view patcher logs. + Click here to view people who have contributed to the ReVanced Project. + One moment, please… + Fetching patches + Unsupported version + This patch is only compatible with version: + Changelog + Patch + Dashboard + Patcher + Logs + Contributors + ReVanced Manager + Website + Team + Translators + Patcher + Help + Help translate + What\'s New + No patches are selected! + Patcher Contributors + Contributor image + No contributors + Settings + About + About + More + CLI Contributors + Patches Contributors + Manager Contributors + Integrations Contributors + Dropdown Button + Version + FAQ + Version info + Unsupported version + Compatible app versions + Patching + ReVanced Manager is patching + Theme + Apply \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 66a08e1..25bde82 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,8 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e2b9c4f..b4dcb57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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() } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031e..2cbd6d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true diff --git a/settings.gradle.kts b/settings.gradle.kts index f7093de..092bd07 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} rootProject.name = "ReVanced Manager" -include(":app") +include(":app") \ No newline at end of file