diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index e4a04351f1..4ca4a56580 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -23,8 +23,8 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle env: @@ -38,7 +38,7 @@ jobs: run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk - name: Upload build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: revanced-manager path: revanced-manager-${{ env.COMMIT_HASH }}.apk diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 8e273987cb..c057a644ca 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -20,10 +20,8 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - cache-disabled: true + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle env: diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index 77097e2fe6..541a7aa5b5 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -11,7 +11,7 @@ jobs: name: Dispatch event to documentation repository if: github.ref == 'refs/heads/main' steps: - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} repository: revanced/revanced-documentation diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..b8c6fd144b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,78 @@ +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ +# 🔒 Security Policy + +This document describes how to report security vulnerabilities for ReVanced Manager. + +## 🚨 Reporting a Vulnerability + +Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced). + +If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role. + +### ⏳ Supported Versions + +| Version | Branch | Supported | +| ------- | ------------|------------------- | +| v1.18.0 | main | :white_check_mark: | +| latest | dev | :white_check_mark: | +| latest | compose-dev | :white_check_mark: | + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36daff7468..ee85588414 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,12 @@ +import kotlin.random.Random + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.devtools) alias(libs.plugins.about.libraries) id("kotlin-parcelize") - kotlin("plugin.serialization") version "1.9.10" + kotlin("plugin.serialization") version "1.9.23" } android { @@ -18,16 +20,15 @@ android { targetSdk = 34 versionCode = 1 versionName = "0.0.1" - resourceConfigurations.addAll(listOf( - "en", - )) vectorDrawables.useSupportLibrary = true } buildTypes { debug { applicationIdSuffix = ".debug" - resValue("string", "app_name", "ReVanced Manager Debug") + resValue("string", "app_name", "ReVanced Manager (dev)") + + buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") } release { @@ -42,6 +43,8 @@ android { resValue("string", "app_name", "ReVanced Manager Debug") signingConfig = signingConfigs.getByName("debug") } + + buildConfigField("long", "BUILD_ID", "0L") } } @@ -54,7 +57,7 @@ android { includeInApk = false includeInBundle = false } - + packaging { resources.excludes.addAll(listOf( "/prebuilt/**", @@ -80,8 +83,21 @@ android { buildFeatures.compose = true buildFeatures.aidl = true + buildFeatures.buildConfig=true + + android { + androidResources { + generateLocaleConfig = true + } + } - composeOptions.kotlinCompilerExtensionVersion = "1.5.3" + composeOptions.kotlinCompilerExtensionVersion = "1.5.10" + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } } kotlin { @@ -104,14 +120,16 @@ dependencies { implementation(platform(libs.compose.bom)) implementation(libs.compose.ui) implementation(libs.compose.ui.preview) + implementation(libs.compose.ui.tooling) implementation(libs.compose.livedata) implementation(libs.compose.material.icons.extended) implementation(libs.compose.material3) // Accompanist implementation(libs.accompanist.drawablepainter) - implementation(libs.accompanist.webview) - implementation(libs.accompanist.placeholder) + + // Placeholder + implementation(libs.placeholder.material3) // HTML Scraper implementation(libs.skrapeit.dsl) @@ -135,6 +153,13 @@ dependencies { implementation(libs.revanced.patcher) implementation(libs.revanced.library) + // Native processes + implementation(libs.kotlin.process) + + // HiddenAPI + compileOnly(libs.hidden.api.stub) + + // LibSU implementation(libs.libsu.core) implementation(libs.libsu.service) implementation(libs.libsu.nio) @@ -162,4 +187,13 @@ dependencies { // Fading Edges implementation(libs.fading.edges) + + // Scrollbars + implementation(libs.scrollbars) + + // Reorderable lists + implementation(libs.reorderable) + + // Compose Icons + implementation(libs.compose.icons.fontawesome) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f3984a94d6..f284b52a20 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,6 +26,10 @@ kotlinx.serialization.KSerializer serializer(...); } +# This required for the process runtime. +-keep class app.revanced.manager.patcher.runtime.process.* { + *; +} # Required for the patcher to function correctly -keep class app.revanced.patcher.** { *; @@ -45,6 +49,7 @@ -keep class com.android.** { *; } +-dontwarn com.google.auto.value.** -dontwarn java.awt.** -dontwarn javax.** -dontwarn org.slf4j.** diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json index 0fb6425d6d..e9c0fd3ae4 100644 --- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "802fa2fda94b930bf0ebb85d195f1022", + "identityHash": "1dd9d5c0201fdf3cfef3ae669fd65e46", "entities": [ { "tableName": "patch_bundles", @@ -51,17 +51,7 @@ "uid" ] }, - "indices": [ - { - "name": "index_patch_bundles_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)" - } - ], + "indices": [], "foreignKeys": [] }, { @@ -231,7 +221,7 @@ }, { "tableName": "applied_patch", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "packageName", @@ -285,7 +275,7 @@ }, { "table": "patch_bundles", - "onDelete": "NO ACTION", + "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "bundle" @@ -407,7 +397,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '802fa2fda94b930bf0ebb85d195f1022')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dd9d5c0201fdf3cfef3ae669fd65e46')" ] } } \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl new file mode 100644 index 0000000000..27a4f61b2a --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl @@ -0,0 +1,11 @@ +// IPatcherEvents.aidl +package app.revanced.manager.patcher.runtime.process; + +// Interface for sending events back to the main app process. +oneway interface IPatcherEvents { + void log(String level, String msg); + void patchSucceeded(); + void progress(String name, String state, String msg); + // The patching process has ended. The exceptionStackTrace is null if it finished successfully. + void finished(String exceptionStackTrace); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl new file mode 100644 index 0000000000..f938ca6235 --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl @@ -0,0 +1,14 @@ +// IPatcherProcess.aidl +package app.revanced.manager.patcher.runtime.process; + +import app.revanced.manager.patcher.runtime.process.Parameters; +import app.revanced.manager.patcher.runtime.process.IPatcherEvents; + +interface IPatcherProcess { + // Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code. + long buildId(); + // Makes the patcher process exit with code 0 + oneway void exit(); + // Starts patching. + oneway void start(in Parameters parameters, IPatcherEvents events); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl new file mode 100644 index 0000000000..a1e8bee78d --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl @@ -0,0 +1,4 @@ +// Parameters.aidl +package app.revanced.manager.patcher.runtime.process; + +parcelable Parameters; \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..64793f8fe5 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,38 @@ + +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("prop_override") + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + prop_override.cpp) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + android + log) diff --git a/app/src/main/cpp/prop_override.cpp b/app/src/main/cpp/prop_override.cpp new file mode 100644 index 0000000000..b314ccd117 --- /dev/null +++ b/app/src/main/cpp/prop_override.cpp @@ -0,0 +1,62 @@ +// Library for overriding Android system properties via environment variables. +// +// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize +// Output: 123M +#include +#include +#include +#include + +// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h +#define PROP_VALUE_MAX 92 +// This is the mangled name of "android::base::GetProperty". +#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_" + +extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *); +typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &); + +char *GetPropOverride(const std::string &key) { + auto envKey = "PROP_" + key; + + return getenv(envKey.c_str()); +} + +// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp +extern "C" int property_get(const char *key, char *value, const char *default_value) { + auto replacement = GetPropOverride(std::string(key)); + if (replacement) { + int len = strnlen(replacement, PROP_VALUE_MAX); + + strncpy(value, replacement, len); + return len; + } + + static property_get_ptr original = NULL; + if (!original) { + // Get the address of the original function. + original = reinterpret_cast(dlsym(RTLD_NEXT, "property_get")); + } + + return original(key, value, default_value); +} + +// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library. +// We can get around this by forcing the function to adopt a specific name using the asm keyword. +std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME); + + +// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp +// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future. +std::string GetProperty(const std::string &key, const std::string &default_value) { + auto replacement = GetPropOverride(key); + if (replacement) { + return std::string(replacement); + } + + static GetProperty_ptr original = NULL; + if (!original) { + original = reinterpret_cast(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME)); + } + + return original(key, default_value); +} diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 5c714a9396..4c8d9ef765 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -5,22 +5,14 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Update -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.getValue -import androidx.compose.ui.res.stringResource import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.InstalledAppInfoScreen -import app.revanced.manager.ui.screen.InstallerScreen +import app.revanced.manager.ui.screen.PatcherScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SettingsScreen import app.revanced.manager.ui.screen.VersionSelectorScreen @@ -35,7 +27,7 @@ import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.rememberNavController import org.koin.core.parameter.parametersOf -import org.koin.androidx.compose.getViewModel as getComposeViewModel +import org.koin.androidx.compose.koinViewModel as getComposeViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel class MainActivity : ComponentActivity() { @@ -46,7 +38,6 @@ class MainActivity : ComponentActivity() { installSplashScreen() val vm: MainViewModel = getAndroidViewModel() - vm.importLegacySettings(this) setContent { @@ -59,37 +50,8 @@ class MainActivity : ComponentActivity() { ) { val navController = rememberNavController(startDestination = Destination.Dashboard) - NavBackHandler(navController) - val firstLaunch by vm.prefs.firstLaunch.getAsState() - - if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs) - - vm.updatedManagerVersion?.let { - AlertDialog( - onDismissRequest = vm::dismissUpdateDialog, - confirmButton = { - TextButton( - onClick = { - vm.dismissUpdateDialog() - navController.navigate(Destination.Settings(SettingsDestination.Update(false))) - } - ) { - Text(stringResource(R.string.update)) - } - }, - dismissButton = { - TextButton(onClick = vm::dismissUpdateDialog) { - Text(stringResource(R.string.dismiss_temporary)) - } - }, - icon = { Icon(Icons.Outlined.Update, null) }, - title = { Text(stringResource(R.string.update_available_dialog_title)) }, - text = { Text(stringResource(R.string.update_available_dialog_description, it)) } - ) - } - AnimatedNavHost( controller = navController ) { destination -> @@ -97,6 +59,9 @@ class MainActivity : ComponentActivity() { is Destination.Dashboard -> DashboardScreen( onSettingsClick = { navController.navigate(Destination.Settings()) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, + onUpdateClick = { navController.navigate( + Destination.Settings(SettingsDestination.Update()) + ) }, onAppClick = { installedApp -> navController.navigate( Destination.InstalledApplicationInfo( @@ -107,11 +72,11 @@ class MainActivity : ComponentActivity() { ) is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen( - onPatchClick = { packageName, patchesSelection -> + onPatchClick = { packageName, patchSelection -> navController.navigate( Destination.VersionSelector( packageName, - patchesSelection + patchSelection ) ) }, @@ -142,14 +107,14 @@ class MainActivity : ComponentActivity() { navController.navigate( Destination.SelectedApplicationInfo( selectedApp, - destination.patchesSelection, + destination.patchSelection, ) ) }, viewModel = getComposeViewModel { parametersOf( destination.packageName, - destination.patchesSelection + destination.patchSelection ) } ) @@ -157,7 +122,7 @@ class MainActivity : ComponentActivity() { is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen( onPatchClick = { app, patches, options -> navController.navigate( - Destination.Installer( + Destination.Patcher( app, patches, options ) ) @@ -167,13 +132,13 @@ class MainActivity : ComponentActivity() { parametersOf( SelectedAppInfoViewModel.Params( destination.selectedApp, - destination.patchesSelection + destination.patchSelection ) ) } ) - is Destination.Installer -> InstallerScreen( + is Destination.Patcher -> PatcherScreen( onBackClick = { navController.popUpTo { it is Destination.Dashboard } }, vm = getComposeViewModel { parametersOf(destination) } ) diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 8a2811bd9c..66ab2483eb 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -1,23 +1,18 @@ package app.revanced.manager import android.app.Application -import android.content.Intent import app.revanced.manager.di.* import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.service.ManagerRootService -import app.revanced.manager.service.RootConnection import kotlinx.coroutines.Dispatchers import coil.Coil import coil.ImageLoader import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.BuilderImpl -import com.topjohnwu.superuser.ipc.RootService import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer -import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -61,9 +56,6 @@ class ManagerApplication : Application() { val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) Shell.setDefaultBuilder(shellBuilder) - val intent = Intent(this, ManagerRootService::class.java) - RootService.bind(intent, get()) - scope.launch { prefs.preload() } diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt index ec01f09ba8..3afbe6e8e5 100644 --- a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -1,10 +1,11 @@ package app.revanced.manager.data.platform +import android.Manifest import android.app.Application +import android.content.Context +import android.content.pm.PackageManager import android.os.Build import android.os.Environment -import android.Manifest -import android.content.pm.PackageManager import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import app.revanced.manager.util.RequestManageStorageContract @@ -16,7 +17,7 @@ class Filesystem(private val app: Application) { * A directory that gets cleared when the app restarts. * Do not store paths to this directory in a parcel. */ - val tempDir = app.cacheDir.resolve("ephemeral").apply { + val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { deleteRecursively() mkdirs() } diff --git a/app/src/main/java/app/revanced/manager/data/room/Converters.kt b/app/src/main/java/app/revanced/manager/data/room/Converters.kt index 7de50382f2..a9437f86e2 100644 --- a/app/src/main/java/app/revanced/manager/data/room/Converters.kt +++ b/app/src/main/java/app/revanced/manager/data/room/Converters.kt @@ -2,7 +2,7 @@ package app.revanced.manager.data.room import androidx.room.TypeConverter import app.revanced.manager.data.room.bundles.Source -import io.ktor.http.* +import app.revanced.manager.data.room.options.Option.SerializedValue import java.io.File class Converters { @@ -17,4 +17,10 @@ class Converters { @TypeConverter fun fileToString(file: File): String = file.path + + @TypeConverter + fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value) + + @TypeConverter + fun serializedOptionToString(value: SerializedValue) = value.toJsonString() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt index 6feb04ed49..d2a498a3a0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt @@ -22,7 +22,8 @@ import kotlinx.parcelize.Parcelize ForeignKey( PatchBundleEntity::class, parentColumns = ["uid"], - childColumns = ["bundle"] + childColumns = ["bundle"], + onDelete = ForeignKey.CASCADE ) ], indices = [Index(value = ["bundle"], unique = false)] diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt index 90d40b9fbd..c290cc5e9e 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt @@ -3,7 +3,7 @@ package app.revanced.manager.data.room.apps.installed import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert -import androidx.room.MapInfo +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Transaction import androidx.room.Upsert @@ -17,12 +17,13 @@ interface InstalledAppDao { @Query("SELECT * FROM installed_app WHERE current_package_name = :packageName") suspend fun get(packageName: String): InstalledApp? - @MapInfo(keyColumn = "bundle", valueColumn = "patch_name") @Query( "SELECT bundle, patch_name FROM applied_patch" + " WHERE package_name = :packageName" ) - suspend fun getPatchesSelection(packageName: String): Map> + suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn( + "patch_name" + ) String>> @Transaction suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List) { diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt index 28f54e5c07..77de9b0311 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -9,7 +9,7 @@ interface PatchBundleDao { suspend fun all(): List @Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid") - fun getPropsById(uid: Int): Flow + fun getPropsById(uid: Int): Flow @Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid") suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) @@ -17,6 +17,9 @@ interface PatchBundleDao { @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") suspend fun setAutoUpdate(uid: Int, value: Boolean) + @Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid") + suspend fun setName(uid: Int, value: String) + @Query("DELETE FROM patch_bundles WHERE uid != 0") suspend fun purgeCustomBundles() diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index e9869de9f4..d120abf5b9 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -21,7 +21,7 @@ sealed class Source { } companion object { - fun from(value: String) = when(value) { + fun from(value: String) = when (value) { Local.SENTINEL -> Local API.SENTINEL -> API else -> Remote(Url(value)) @@ -34,7 +34,7 @@ data class VersionInfo( @ColumnInfo(name = "integrations_version") val integrations: String? = null, ) -@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)]) +@Entity(tableName = "patch_bundles") data class PatchBundleEntity( @PrimaryKey val uid: Int, @ColumnInfo(name = "name") val name: String, diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt index 3a70a9a56e..b59dbd165d 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt @@ -3,6 +3,23 @@ package app.revanced.manager.data.room.options import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import app.revanced.manager.patcher.patch.Option +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.reflect.KClass @Entity( tableName = "options", @@ -19,5 +36,74 @@ data class Option( @ColumnInfo(name = "patch_name") val patchName: String, @ColumnInfo(name = "key") val key: String, // Encoded as Json. - @ColumnInfo(name = "value") val value: String, -) \ No newline at end of file + @ColumnInfo(name = "value") val value: SerializedValue, +) { + @Serializable + data class SerializedValue(val raw: JsonElement) { + fun toJsonString() = json.encodeToString(raw) + fun deserializeFor(option: Option<*>): Any? { + if (raw is JsonNull) return null + + val errorMessage = "Cannot deserialize value as ${option.type}" + try { + if (option.type.endsWith("Array")) { + val elementType = option.type.removeSuffix("Array") + return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) } + } + + return deserializeBasicType(option.type, raw.jsonPrimitive) + } catch (e: IllegalArgumentException) { + throw SerializationException(errorMessage, e) + } catch (e: IllegalStateException) { + throw SerializationException(errorMessage, e) + } catch (e: kotlinx.serialization.SerializationException) { + throw SerializationException(errorMessage, e) + } + } + + companion object { + private val json = Json { + // Patcher does not forbid the use of these values, so we should support them. + allowSpecialFloatingPointValues = true + } + + private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) { + "Boolean" -> value.boolean + "Int" -> value.int + "Long" -> value.long + "Float" -> value.float + "String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") } + else -> throw SerializationException("Unknown type: $type") + } + + fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value)) + fun fromValue(value: Any?) = SerializedValue(when (value) { + null -> JsonNull + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is List<*> -> buildJsonArray { + var elementClass: KClass? = null + + value.forEach { + when (it) { + null -> throw SerializationException("List elements must not be null") + is Number -> add(it) + is Boolean -> add(it) + is String -> add(it) + else -> throw SerializationException("Unknown element type: ${it::class.simpleName}") + } + + if (elementClass == null) elementClass = it::class + else if (elementClass != it::class) throw SerializationException("List elements must have the same type") + } + } + + else -> throw SerializationException("Unknown type: ${value::class.simpleName}") + }) + } + } + + class SerializationException(message: String, cause: Throwable? = null) : + Exception(message, cause) +} diff --git a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt index fa343a6db6..5a147f6f3b 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt @@ -2,7 +2,7 @@ package app.revanced.manager.data.room.options import androidx.room.Dao import androidx.room.Insert -import androidx.room.MapInfo +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Transaction import kotlinx.coroutines.flow.Flow @@ -10,13 +10,12 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class OptionDao { @Transaction - @MapInfo(keyColumn = "patch_bundle") @Query( "SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" + " LEFT JOIN options ON uid = options.`group`" + " WHERE package_name = :packageName" ) - abstract suspend fun getOptions(packageName: String): Map> + abstract suspend fun getOptions(packageName: String): Map<@MapColumn("patch_bundle") Int, List