diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
new file mode 100644
index 0000000000..ec1e0714e9
--- /dev/null
+++ b/.github/workflows/release-build.yml
@@ -0,0 +1,43 @@
+name: Release Build
+
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set env
+ run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+
+ - name: Set up Java
+ run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
+
+ - name: Build with Gradle
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./gradlew assembleRelease --no-daemon
+
+ - name: Sign APK
+ id: sign_apk
+ uses: ilharp/sign-android-release@v1
+ with:
+ releaseDir: ./app/build/outputs/apk/release/
+ signingKey: ${{ secrets.SIGNING_KEYSTORE }}
+ keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
+ keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
+ keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
+
+ - name: Add version to APK
+ run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
+
+ - name: Publish release APK
+ uses: "marvinpinto/action-automatic-releases@latest"
+ with:
+ repo_token: "${{ secrets.GITHUB_TOKEN }}"
+ prerelease: false
+ files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml
new file mode 100644
index 0000000000..77097e2fe6
--- /dev/null
+++ b/.github/workflows/update-documentation.yml
@@ -0,0 +1,19 @@
+name: Update documentation
+
+on:
+ push:
+ paths:
+ - docs/**
+
+jobs:
+ trigger:
+ runs-on: ubuntu-latest
+ name: Dispatch event to documentation repository
+ if: github.ref == 'refs/heads/main'
+ steps:
+ - uses: peter-evans/repository-dispatch@v2
+ with:
+ token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
+ repository: revanced/revanced-documentation
+ event-type: update-documentation
+ client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
diff --git a/.gitignore b/.gitignore
index 154ff109d8..05c971486d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,7 @@
*.iml
.gradle
/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-/.idea/deploymentTargetDropDown.xml
-/.idea/misc.xml
-/.idea/gradle.xml
+/.idea
.DS_Store
/build
/captures
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d33521af..0000000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index 897c96afe8..0000000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-ReVanced Manager
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b589d56e9f..0000000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
deleted file mode 100644
index d8e9561668..0000000000
--- a/.idea/discord.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 01940c92e0..0000000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index ed76bea38e..0000000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index 217e5c51fb..0000000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index d673ade909..0000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4c..0000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index c1b3706aaf..1a48684176 100644
--- a/README.md
+++ b/README.md
@@ -32,3 +32,24 @@ By using Material 3 and Material You, we are ensuring that the app's user interf
* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
+## ๐ฝ Download
+
+You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
+
+## ๐ Prerequisites
+
+For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
+
+## ๐ด Issues
+
+For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
+
+## ๐ Translation
+
+[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
+
+We're accepting translations on [Crowdin](https://translate.revanced.app)
+
+## ๐ ๏ธ Building Manager from source
+
+For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0e7ea14e77..e5c3f4842a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -131,11 +131,8 @@ dependencies {
ksp(libs.room.compiler)
// ReVanced
- implementation(libs.patcher)
-
- // Signing
- implementation(libs.apksign)
- implementation(libs.bcpkix.jdk18on)
+ implementation(libs.revanced.patcher)
+ implementation(libs.revanced.library)
implementation(libs.libsu.core)
implementation(libs.libsu.service)
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 543d7a70ad..041d1dea4e 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": "5515d164bc8f713201506d42a02d337f",
+ "identityHash": "371c7a84b122a2de8b660b35e6e9ce14",
"entities": [
{
"tableName": "patch_bundles",
@@ -160,7 +160,7 @@
},
{
"tableName": "downloaded_app",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `file` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
"fields": [
{
"fieldPath": "packageName",
@@ -175,8 +175,8 @@
"notNull": true
},
{
- "fieldPath": "file",
- "columnName": "file",
+ "fieldPath": "directory",
+ "columnName": "directory",
"affinity": "TEXT",
"notNull": true
}
@@ -300,7 +300,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, '5515d164bc8f713201506d42a02d337f')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '371c7a84b122a2de8b660b35e6e9ce14')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt
index 162ecff6eb..944656e32d 100644
--- a/app/src/main/java/app/revanced/manager/MainActivity.kt
+++ b/app/src/main/java/app/revanced/manager/MainActivity.kt
@@ -1,24 +1,36 @@
package app.revanced.manager
+import android.content.ActivityNotFoundException
+import android.content.Intent
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
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.screen.AppInfoScreen
-import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstallerScreen
import app.revanced.manager.ui.screen.PatchesSelectorScreen
import app.revanced.manager.ui.screen.SettingsScreen
+import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
+import app.revanced.manager.util.tag
+import app.revanced.manager.util.toast
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
@@ -51,9 +63,48 @@ class MainActivity : ComponentActivity() {
NavBackHandler(navController)
- val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
- if (showAutoUpdatesDialog) {
- AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
+ val firstLaunch by vm.prefs.firstLaunch.getAsState()
+
+ if (firstLaunch) {
+ var legacyActivityState by rememberSaveable { mutableStateOf(LegacyActivity.NOT_LAUNCHED) }
+ if (legacyActivityState == LegacyActivity.NOT_LAUNCHED) {
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK) {
+ if (result.data != null) {
+ val jsonData = result.data!!.getStringExtra("data")!!
+ vm.applyLegacySettings(jsonData)
+ }
+ } else {
+ legacyActivityState = LegacyActivity.FAILED
+ toast(getString(R.string.legacy_import_failed))
+ }
+ }
+
+ val intent = Intent().apply {
+ setClassName(
+ "app.revanced.manager.flutter",
+ "app.revanced.manager.flutter.ExportSettingsActivity"
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ try {
+ launcher.launch(intent)
+ } catch (e: Exception) {
+ if (e !is ActivityNotFoundException) {
+ toast(getString(R.string.legacy_import_failed))
+ Log.e(tag, "Failed to launch legacy import activity: $e")
+ }
+ legacyActivityState = LegacyActivity.FAILED
+ }
+ }
+
+ legacyActivityState = LegacyActivity.LAUNCHED
+ } else if (legacyActivityState == LegacyActivity.FAILED){
+ AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
+ }
}
AnimatedNavHost(
@@ -120,4 +171,10 @@ class MainActivity : ComponentActivity() {
}
}
}
-}
\ No newline at end of file
+
+ private enum class LegacyActivity {
+ NOT_LAUNCHED,
+ LAUNCHED,
+ FAILED
+ }
+}
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
similarity index 81%
rename from app/src/main/java/app/revanced/manager/data/platform/FileSystem.kt
rename to app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt
index f037e8a6e6..ec01f09ba8 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
@@ -9,9 +9,18 @@ import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract
-class FileSystem(private val app: Application) {
+class Filesystem(private val app: Application) {
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
+ /**
+ * 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 {
+ deleteRecursively()
+ mkdirs()
+ }
+
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
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 f8aa073d73..7de50382f2 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
@@ -16,5 +16,5 @@ class Converters {
fun fileFromString(value: String) = File(value)
@TypeConverter
- fun fileToString(file: File): String = file.absolutePath
+ fun fileToString(file: File): String = file.path
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
index a30063ffa4..60d1561df8 100644
--- a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
@@ -11,5 +11,5 @@ import java.io.File
data class DownloadedApp(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "version") val version: String,
- @ColumnInfo(name = "file") val file: File,
+ @ColumnInfo(name = "directory") val directory: File,
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
index 2e288d9793..630c5d66e8 100644
--- a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
@@ -35,6 +35,9 @@ abstract class SelectionDao {
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int)
+ @Query("DELETE FROM patch_selections WHERE package_name = :packageName")
+ abstract suspend fun clearForPackage(packageName: String)
+
@Query("DELETE FROM patch_selections")
abstract suspend fun reset()
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 37c152e54c..a5420a5caa 100644
--- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
@@ -1,17 +1,19 @@
package app.revanced.manager.di
-import app.revanced.manager.data.platform.FileSystem
+import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.*
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ReVancedAPI
+import org.koin.core.module.dsl.createdAtStart
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedAPI)
- singleOf(::GithubRepository)
- singleOf(::FileSystem)
+ singleOf(::Filesystem) {
+ createdAtStart()
+ }
singleOf(::NetworkInfo)
singleOf(::PatchBundlePersistenceRepository)
singleOf(::PatchSelectionRepository)
diff --git a/app/src/main/java/app/revanced/manager/di/ServiceModule.kt b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
index 78b99c2725..c30a711f67 100644
--- a/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
@@ -1,21 +1,11 @@
package app.revanced.manager.di
-import app.revanced.manager.network.service.GithubService
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.service.ReVancedService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
- fun provideReVancedService(
- client: HttpService,
- ): ReVancedService {
- return ReVancedService(
- client = client,
- )
- }
-
- single { provideReVancedService(get()) }
+ singleOf(::ReVancedService)
singleOf(::HttpService)
- singleOf(::GithubService)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
index 4362caa523..826d1de541 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
@@ -2,19 +2,19 @@ package app.revanced.manager.domain.manager
import android.app.Application
import android.content.Context
-import app.revanced.manager.util.signing.Signer
-import app.revanced.manager.util.signing.SigningOptions
+import app.revanced.library.ApkSigner
+import app.revanced.library.ApkUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import java.io.ByteArrayInputStream
import java.io.File
+import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.StandardCopyOption
-import kotlin.io.path.exists
+import java.security.UnrecoverableKeyException
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
- companion object {
+ companion object Constants {
/**
* Default alias and password for the keystore.
*/
@@ -22,37 +22,55 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
}
private val keystorePath =
- app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
+ app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
prefs.keystoreCommonName.value = cn
prefs.keystorePass.value = pass
}
+ private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
+ keyStore = path,
+ keyStorePassword = null,
+ alias = prefs.keystoreCommonName.get(),
+ signer = prefs.keystoreCommonName.get(),
+ password = prefs.keystorePass.get()
+ )
+
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
- Signer(
- SigningOptions(
- prefs.keystoreCommonName.get(),
- prefs.keystorePass.get(),
- keystorePath
- )
- ).signApk(
- input,
- output
- )
+ ApkUtils.sign(input, output, signingOptions())
}
suspend fun regenerate() = withContext(Dispatchers.Default) {
- Signer(SigningOptions(DEFAULT, DEFAULT, keystorePath)).regenerateKeystore()
+ val ks = ApkSigner.newKeyStore(
+ listOf(
+ ApkSigner.KeyStoreEntry(
+ DEFAULT, DEFAULT
+ )
+ )
+ )
+ keystorePath.outputStream().use {
+ ks.store(it, null)
+ }
+
updatePrefs(DEFAULT, DEFAULT)
}
- suspend fun import(cn: String, pass: String, keystore: Path): Boolean {
- if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
+ suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
+ val keystoreData = keystore.readBytes()
+
+ try {
+ val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
+
+ ApkSigner.readKeyCertificatePair(ks, cn, pass)
+ } catch (_: UnrecoverableKeyException) {
+ return false
+ } catch (_: IllegalArgumentException) {
return false
}
+
withContext(Dispatchers.IO) {
- Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
+ Files.write(keystorePath.toPath(), keystoreData)
}
updatePrefs(cn, pass)
@@ -63,7 +81,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
suspend fun export(target: OutputStream) {
withContext(Dispatchers.IO) {
- Files.copy(keystorePath, target)
+ Files.copy(keystorePath.toPath(), target)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
index 34d7af60e0..44f617940f 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
@@ -19,6 +19,9 @@ class PreferencesManager(
val preferSplits = booleanPreference("prefer_splits", false)
- val showAutoUpdatesDialog = booleanPreference("show_auto_updates_dialog", true)
+ val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
+
+ val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
+ val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true)
}
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
index 7035c6c4ba..fe339a2eda 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
@@ -1,34 +1,61 @@
package app.revanced.manager.domain.repository
+import android.app.Application
+import android.content.Context
import app.revanced.manager.data.room.AppDatabase
+import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
+import app.revanced.manager.network.downloader.AppDownloader
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File
class DownloadedAppRepository(
+ app: Application,
db: AppDatabase
) {
+ private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()
- suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
+ fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
+ private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
+
+ suspend fun download(
+ app: AppDownloader.App,
+ preferSplits: Boolean,
+ onDownload: suspend (downloadProgress: Pair?) -> Unit = {},
+ ): File {
+ this.get(app.packageName, app.version)?.let { downloaded ->
+ return getApkFileForApp(downloaded)
+ }
+
+ // Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
+ val relativePath = File(generateUid().toString())
+ val savePath = dir.resolve(relativePath).also { it.mkdirs() }
+
+ try {
+ app.download(savePath, preferSplits, onDownload)
- suspend fun add(
- packageName: String,
- version: String,
- file: File
- ) = dao.insert(
- DownloadedApp(
- packageName = packageName,
- version = version,
- file = file
- )
- )
+ dao.insert(DownloadedApp(
+ packageName = app.packageName,
+ version = app.version,
+ directory = relativePath,
+ ))
+ } catch (e: Exception) {
+ savePath.deleteRecursively()
+ throw e
+ }
+
+ // Return the Apk file.
+ return getApkFileForDir(savePath)
+ }
+
+ suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
suspend fun delete(downloadedApps: Collection) {
downloadedApps.forEach {
- it.file.deleteRecursively()
+ dir.resolve(it.directory).deleteRecursively()
}
dao.delete(downloadedApps)
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/GithubRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/GithubRepository.kt
deleted file mode 100644
index 79587dfca4..0000000000
--- a/app/src/main/java/app/revanced/manager/domain/repository/GithubRepository.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package app.revanced.manager.domain.repository
-
-import app.revanced.manager.network.service.GithubService
-
-// TODO: delete this when the revanced api adds download count.
-class GithubRepository(private val service: GithubService) {
- suspend fun getChangelog(repo: String) = service.getChangelog(repo)
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt
index cade429164..c34e5efd6b 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt
@@ -25,6 +25,10 @@ class PatchSelectionRepository(db: AppDatabase) {
)
})
+ suspend fun clearSelection(packageName: String) {
+ dao.clearForPackage(packageName)
+ }
+
suspend fun reset() = dao.reset()
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt b/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
index 3a055ef576..30c6fbee62 100644
--- a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
+++ b/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
@@ -171,7 +171,7 @@ class APKMirror : AppDownloader, KoinComponent {
saveDirectory: File,
preferSplit: Boolean,
onDownload: suspend (downloadProgress: Pair?) -> Unit
- ): File {
+ ) {
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
.div {
withClass = "variants-table"
@@ -246,18 +246,10 @@ class APKMirror : AppDownloader, KoinComponent {
}
}
- val saveLocation = if (variant.apkType == APKType.BUNDLE)
- saveDirectory.resolve(version).also { it.mkdirs() }
- else
- saveDirectory.resolve("$version.apk")
+ val targetFile = saveDirectory.resolve("base.apk")
try {
- val downloadLocation = if (variant.apkType == APKType.BUNDLE)
- saveLocation.resolve("temp.zip")
- else
- saveLocation
-
- httpClient.download(downloadLocation) {
+ httpClient.download(targetFile) {
url(apkMirror + downloadLink)
onDownload { bytesSentTotal, contentLength ->
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
@@ -267,16 +259,11 @@ class APKMirror : AppDownloader, KoinComponent {
if (variant.apkType == APKType.BUNDLE) {
// TODO: Extract temp.zip
- downloadLocation.delete()
+ targetFile.delete()
}
- } catch (e: Exception) {
- saveLocation.deleteRecursively()
- throw e
} finally {
onDownload(null)
}
-
- return saveLocation
}
}
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt b/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
index a6a17622f4..dcefa26e49 100644
--- a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
+++ b/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
@@ -22,7 +22,6 @@ interface AppDownloader {
saveDirectory: File,
preferSplit: Boolean,
onDownload: suspend (downloadProgress: Pair?) -> Unit = {}
- ): File
+ )
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
index d97b335ce2..416e06295e 100644
--- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
+++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
@@ -21,7 +21,8 @@ data class ReVancedReleaseMeta(
val draft: Boolean,
val prerelease: Boolean,
@SerialName("created_at") val createdAt: String,
- @SerialName("published_at") val publishedAt: String
+ @SerialName("published_at") val publishedAt: String,
+ val body: String,
)
@Serializable
diff --git a/app/src/main/java/app/revanced/manager/network/service/GithubService.kt b/app/src/main/java/app/revanced/manager/network/service/GithubService.kt
deleted file mode 100644
index 2c293848ee..0000000000
--- a/app/src/main/java/app/revanced/manager/network/service/GithubService.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package app.revanced.manager.network.service
-
-import app.revanced.manager.network.dto.GithubChangelog
-import app.revanced.manager.network.utils.APIResponse
-import io.ktor.client.request.url
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-class GithubService(private val client: HttpService) {
- suspend fun getChangelog(repo: String): APIResponse = withContext(Dispatchers.IO) {
- client.request {
- url("https://api.github.com/repos/revanced/$repo/releases/latest")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/Aligning.kt b/app/src/main/java/app/revanced/manager/patcher/Aligning.kt
deleted file mode 100644
index d8a672d61e..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/Aligning.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package app.revanced.manager.patcher
-
-import app.revanced.manager.patcher.alignment.ZipAligner
-import app.revanced.manager.patcher.alignment.zip.ZipFile
-import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
-import app.revanced.patcher.PatcherResult
-import java.io.File
-
-// This is the same aligner used by the CLI.
-// It will be removed eventually.
-object Aligning {
- fun align(result: PatcherResult, inputFile: File, outputFile: File) {
- // logger.info("Aligning ${inputFile.name} to ${outputFile.name}")
-
- if (outputFile.exists()) outputFile.delete()
-
- ZipFile(outputFile).use { file ->
- result.dexFiles.forEach {
- file.addEntryCompressData(
- ZipEntry.createWithName(it.name),
- it.stream.readBytes()
- )
- }
-
- result.resourceFile?.let {
- file.copyEntriesFromFileAligned(
- ZipFile(it),
- ZipAligner::getEntryAlignment
- )
- }
-
- file.copyEntriesFromFileAligned(
- ZipFile(inputFile, readonly = true),
- ZipAligner::getEntryAlignment
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt
index 54e028fad4..35b80e5e37 100644
--- a/app/src/main/java/app/revanced/manager/patcher/Session.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt
@@ -1,9 +1,10 @@
package app.revanced.manager.patcher
+import app.revanced.library.ApkUtils
import app.revanced.manager.ui.viewmodel.ManagerLogger
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
-import app.revanced.patcher.patch.PatchClass
+import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -13,7 +14,7 @@ import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.logging.Logger
-internal typealias PatchList = List
+internal typealias PatchList = List>
class Session(
cacheDir: String,
@@ -23,16 +24,17 @@ class Session(
private val input: File,
private val onStepSucceeded: suspend () -> Unit
) : Closeable {
- private val temporary = File(cacheDir).resolve("manager").also { it.mkdirs() }
+ private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher(
PatcherOptions(
inputFile = input,
- resourceCachePath = temporary.resolve("aapt-resources"),
+ resourceCachePath = tempDir.resolve("aapt-resources"),
frameworkFileDirectory = frameworkDir,
aaptBinaryPath = aaptPath
)
)
+
private suspend fun Patcher.applyPatchesVerbose() {
this.apply(true).collect { (patch, exception) ->
if (exception == null) {
@@ -69,7 +71,8 @@ class Session(
logger.info("Writing patched files...")
val result = patcher.get()
- val aligned = temporary.resolve("aligned.apk").also { Aligning.align(result, input, it) }
+ val aligned = tempDir.resolve("aligned.apk")
+ ApkUtils.copyAligned(input, aligned, result)
logger.info("Patched apk saved to $aligned")
@@ -80,12 +83,12 @@ class Session(
}
override fun close() {
- temporary.delete()
+ tempDir.deleteRecursively()
patcher.close()
}
companion object {
- operator fun PatchResult.component1() = patchName
+ operator fun PatchResult.component1() = patch.name
operator fun PatchResult.component2() = exception
}
}
\ No newline at end of file
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
index 25e26be448..959768e62b 100644
--- a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt
@@ -1,9 +1,14 @@
package app.revanced.manager.patcher.aapt
import android.content.Context
+import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
import java.io.File
object Aapt {
+ private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
+
+ fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
+
fun binary(context: Context): File? {
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
}
diff --git a/app/src/main/java/app/revanced/manager/patcher/alignment/ZipAlign.kt b/app/src/main/java/app/revanced/manager/patcher/alignment/ZipAlign.kt
deleted file mode 100644
index 4f8504c86c..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/alignment/ZipAlign.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package app.revanced.manager.patcher.alignment
-
-import app.revanced.manager.patcher.alignment.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/alignment/zip/Extensions.kt b/app/src/main/java/app/revanced/manager/patcher/alignment/zip/Extensions.kt
deleted file mode 100644
index c022005fe1..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/alignment/zip/Extensions.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package app.revanced.manager.patcher.alignment.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/alignment/zip/ZipFile.kt b/app/src/main/java/app/revanced/manager/patcher/alignment/zip/ZipFile.kt
deleted file mode 100644
index a49ffed170..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/alignment/zip/ZipFile.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-package app.revanced.manager.patcher.alignment.zip
-
-import app.revanced.manager.patcher.alignment.zip.structures.ZipEndRecord
-import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
-
-import java.io.Closeable
-import java.io.File
-import java.io.IOException
-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(file: File, private val readonly: Boolean = false) : Closeable {
- var entries: MutableList = mutableListOf()
-
- private val filePointer: RandomAccessFile = RandomAccessFile(file, if (readonly) "r" else "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 assertWritable() {
- if (readonly) throw IOException("Archive is read-only")
- }
-
- 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) {
- assertWritable()
-
- 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)
- }
-
- private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
- assertWritable()
-
- alignment?.let {
- //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?) {
- assertWritable()
-
- 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/alignment/zip/structures/ZipEndRecord.kt b/app/src/main/java/app/revanced/manager/patcher/alignment/zip/structures/ZipEndRecord.kt
deleted file mode 100644
index 06555cd3fc..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/alignment/zip/structures/ZipEndRecord.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package app.revanced.manager.patcher.alignment.zip.structures
-
-import app.revanced.manager.patcher.alignment.zip.putUInt
-import app.revanced.manager.patcher.alignment.zip.putUShort
-import app.revanced.manager.patcher.alignment.zip.readUIntLE
-import app.revanced.manager.patcher.alignment.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/alignment/zip/structures/ZipEntry.kt b/app/src/main/java/app/revanced/manager/patcher/alignment/zip/structures/ZipEntry.kt
deleted file mode 100644
index f070386c2f..0000000000
--- a/app/src/main/java/app/revanced/manager/patcher/alignment/zip/structures/ZipEntry.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package app.revanced.manager.patcher.alignment.zip.structures
-
-import app.revanced.manager.patcher.alignment.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()
- val 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/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
index 08b3c33ddf..7c5b2ffd1e 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
@@ -3,16 +3,15 @@ package app.revanced.manager.patcher.patch
import android.util.Log
import app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader
-import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
-import app.revanced.patcher.patch.PatchClass
+import app.revanced.patcher.patch.Patch
import java.io.File
-class PatchBundle(private val loader: Iterable, val integrations: File?) {
+class PatchBundle(private val loader: Iterable>, val integrations: File?) {
constructor(bundleJar: File, integrations: File?) : this(
- object : Iterable {
- private fun load(): List = PatchBundleLoader.Dex(bundleJar)
+ object : Iterable> {
+ private fun load(): Iterable> = PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
- override fun iterator() = load().iterator()
+ override fun iterator(): Iterator> = load().iterator()
},
integrations
) {
diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
index 20d1c88677..8002fa99df 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
@@ -1,50 +1,70 @@
package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable
-import app.revanced.patcher.annotation.Package
-import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
-import app.revanced.patcher.extensions.PatchExtensions.dependencies
-import app.revanced.patcher.extensions.PatchExtensions.description
-import app.revanced.patcher.extensions.PatchExtensions.include
-import app.revanced.patcher.extensions.PatchExtensions.options
-import app.revanced.patcher.extensions.PatchExtensions.patchName
-import app.revanced.patcher.patch.PatchClass
-import app.revanced.patcher.patch.PatchOption
+import app.revanced.patcher.patch.Patch
+import app.revanced.patcher.patch.options.PatchOption
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
data class PatchInfo(
val name: String,
val description: String?,
- val dependencies: ImmutableList?,
val include: Boolean,
val compatiblePackages: ImmutableList?,
val options: ImmutableList?
) {
- constructor(patch: PatchClass) : this(
- patch.patchName,
+ constructor(patch: Patch<*>) : this(
+ patch.name.orEmpty(),
patch.description,
- patch.dependencies?.map { it.java.patchName }?.toImmutableList(),
- patch.include,
+ patch.use,
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
- patch.options?.map { Option(it) }?.toImmutableList())
+ patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
+ )
- fun compatibleWith(packageName: String) = compatiblePackages?.any { it.packageName == packageName } ?: true
+ fun compatibleWith(packageName: String) =
+ compatiblePackages?.any { it.packageName == packageName } ?: true
- fun supportsVersion(versionName: String) =
- compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } }
- ?: true
+ fun supportsVersion(packageName: String, versionName: String): Boolean {
+ val packages = compatiblePackages ?: return true // Universal patch
+
+ return packages.any { pkg ->
+ if (pkg.packageName != packageName) {
+ return@any false
+ }
+
+ pkg.versions == null || pkg.versions.contains(versionName)
+ }
+ }
}
@Immutable
data class CompatiblePackage(
val packageName: String,
- val versions: ImmutableList
+ val versions: ImmutableSet?
) {
- constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList())
+ constructor(pkg: Patch.CompatiblePackage) : this(
+ pkg.name,
+ pkg.versions?.toImmutableSet()
+ )
}
@Immutable
-data class Option(val title: String, val key: String, val description: String, val required: Boolean, val type: Class>, val defaultValue: Any?) {
- constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required, option::class.java, option.value)
+data class Option(
+ val title: String,
+ val key: String,
+ val description: String,
+ val required: Boolean,
+ val type: Class>,
+ val defaultValue: Any?
+) {
+ constructor(option: PatchOption<*>) : this(
+ option.title ?: option.key,
+ option.key,
+ option.description.orEmpty(),
+ option.required,
+ option::class.java,
+ option.value
+ )
}
\ No newline at end of file
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
index fc4faf6afa..b6a3363715 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
+import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
@@ -30,8 +31,6 @@ import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag
-import app.revanced.patcher.extensions.PatchExtensions.options
-import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@@ -51,6 +50,7 @@ class PatcherWorker(
private val prefs: PreferencesManager by inject()
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val pm: PM by inject()
+ private val fs: Filesystem by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
@@ -59,13 +59,12 @@ class PatcherWorker(
val output: String,
val selectedPatches: PatchesSelection,
val options: Options,
- val packageName: String,
- val packageVersion: String,
val progress: MutableStateFlow>,
val logger: ManagerLogger,
- val selectedApp: SelectedApp,
val setInputFile: (File) -> Unit
- )
+ ) {
+ val packageName get() = input.packageName
+ }
companion object {
private const val logPrefix = "[Worker]:"
@@ -155,7 +154,7 @@ class PatcherWorker(
return try {
- if (args.selectedApp is SelectedApp.Installed) {
+ if (args.input is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let {
if (it.installType == InstallType.ROOT) {
rootInstaller.unmount(args.packageName)
@@ -172,39 +171,30 @@ class PatcherWorker(
args.options.forEach { (bundle, configuredPatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
configuredPatchOptions.forEach { (patchName, options) ->
- patches.single { it.patchName == patchName }.options?.let {
- options.forEach { (key, value) ->
- it[key] = value
- }
+ val patchOptions = patches.single { it.name == patchName }.options
+ options.forEach { (key, value) ->
+ patchOptions[key] = value
}
}
}
val patches = args.selectedPatches.flatMap { (bundle, selected) ->
- allPatches[bundle]?.filter { selected.contains(it.patchName) }
+ allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
}
// Ensure they are in the correct order so we can track progress properly.
- progressManager.replacePatchesList(patches.map { it.patchName })
+ progressManager.replacePatchesList(patches.map { it.name.orEmpty() })
updateProgress() // Loading patches
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
- val savePath = applicationContext.filesDir.resolve("downloaded-apps")
- .resolve(args.input.packageName).also { it.mkdirs() }
-
- selectedApp.app.download(
- savePath,
+ downloadedAppRepository.download(
+ selectedApp.app,
prefs.preferSplits.get(),
onDownload = { downloadProgress.emit(it) }
).also {
- downloadedAppRepository.add(
- args.input.packageName,
- args.input.version,
- it
- )
args.setInputFile(it)
updateProgress() // Downloading
}
@@ -215,7 +205,7 @@ class PatcherWorker(
}
Session(
- applicationContext.cacheDir.absolutePath,
+ fs.tempDir.absolutePath,
frameworkPath,
aaptPath,
args.logger,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt
index 2c1e3c19aa..3762ae2539 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt
@@ -13,15 +13,17 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
-fun ArrowButton(expanded: Boolean, onClick: () -> Unit) {
+fun ArrowButton(modifier: Modifier = Modifier, expanded: Boolean,onClick: () -> Unit) {
IconButton(onClick = onClick) {
val description = if (expanded) R.string.collapse_content else R.string.expand_content
- val rotation by animateFloatAsState(targetValue = if (expanded) 0f else 180f)
+ val rotation by animateFloatAsState(targetValue = if (expanded) 0f else 180f, label = "rotation")
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
contentDescription = stringResource(description),
- modifier = Modifier.rotate(rotation)
+ modifier = Modifier
+ .rotate(rotation)
+ .then(modifier)
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt b/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt
new file mode 100644
index 0000000000..0fb23c2728
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt
@@ -0,0 +1,26 @@
+package app.revanced.manager.ui.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import kotlinx.coroutines.delay
+
+@Composable
+fun Countdown(start: Int, content: @Composable (Int) -> Unit) {
+ var timer by rememberSaveable(start) {
+ mutableStateOf(start)
+ }
+ LaunchedEffect(timer) {
+ if (timer == 0) {
+ return@LaunchedEffect
+ }
+
+ delay(1000L)
+ timer -= 1
+ }
+
+ content(timer)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt
index ec8e5bfe12..6773b15a0d 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt
@@ -44,6 +44,7 @@ fun Markdown(
.background(Color.Transparent)
.then(modifier),
client = client,
+ captureBackPresses = false,
onCreated = {
it.setBackgroundColor(android.graphics.Color.TRANSPARENT)
it.isVerticalScrollBarEnabled = false
diff --git a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt
index 4c23afd477..a4e2129716 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt
@@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -24,13 +26,13 @@ fun NotificationCard(
color: Color,
icon: ImageVector,
text: String,
- content: @Composable () -> Unit
+ content: (@Composable () -> Unit)? = null,
) {
Card(
+ colors = CardDefaults.cardColors(containerColor = color),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(28.dp))
- .background(color)
) {
Row(
modifier = Modifier
@@ -47,11 +49,11 @@ fun NotificationCard(
contentDescription = null,
)
Text(
- modifier = Modifier.width(220.dp),
+ modifier = if (content != null) Modifier.width(220.dp) else Modifier,
text = text,
style = MaterialTheme.typography.bodyMedium
)
- content()
+ content?.invoke()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
index 042449d241..33eb2d69d5 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
@@ -22,10 +22,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.TextInputDialog
+import app.revanced.manager.util.isDebuggable
@Composable
fun BaseBundleDialog(
@@ -159,22 +161,18 @@ fun BaseBundleDialog(
)
}
- if (patchCount > 0) {
- BundleListItem(
- headlineText = stringResource(R.string.patches),
- supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
- else stringResource(R.string.patches_available, patchCount),
- trailingContent = {
- if (patchCount > 0) {
- IconButton(onClick = onPatchesClick) {
- Icon(
- Icons.Outlined.ArrowRight,
- stringResource(R.string.patches)
- )
- }
- }
- }
- )
+ val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
+ BundleListItem(
+ headlineText = stringResource(R.string.patches),
+ supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
+ else stringResource(R.string.patches_available, patchCount),
+ modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick)
+ ) {
+ if (patchesClickable)
+ Icon(
+ Icons.Outlined.ArrowRight,
+ stringResource(R.string.patches)
+ )
}
version?.let {
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
index bd822ad0ed..e8fb4e4ade 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
@@ -26,10 +26,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
-import app.revanced.manager.data.platform.FileSystem
+import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.util.toast
-import app.revanced.patcher.patch.PatchOption
+import app.revanced.patcher.patch.options.types.*
import org.koin.compose.rememberKoinInject
// Composable functions do not support function references, so we have to use composable lambdas instead.
@@ -61,7 +61,7 @@ private fun StringOptionDialog(
mutableStateOf(value.orEmpty())
}
- val fs: FileSystem = rememberKoinInject()
+ val fs: Filesystem = rememberKoinInject()
val (contract, permissionName) = fs.permissionContract()
val permissionLauncher = rememberLauncherForActivityResult(contract = contract) {
showFileDialog = it
@@ -195,8 +195,8 @@ fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) {
when (option.type) {
// These are the only two types that are currently used by the official patches.
- PatchOption.StringOption::class.java -> StringOption
- PatchOption.BooleanOption::class.java -> BooleanOption
+ StringPatchOption::class.java -> StringOption
+ BooleanPatchOption::class.java -> BooleanOption
else -> UnknownOption
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
index 424d07e7cd..f5e1b5f842 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
@@ -13,7 +13,7 @@ sealed class SelectedApp : Parcelable {
data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp()
@Parcelize
- data class Local(override val packageName: String, override val version: String, val file: File) : SelectedApp()
+ data class Local(override val packageName: String, override val version: String, val file: File, val shouldDelete: Boolean) : SelectedApp()
@Parcelize
data class Installed(override val packageName: String, override val version: String) : SelectedApp()
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
index f1a71e59db..10f2aa4593 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
@@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Storage
-import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -155,9 +154,6 @@ fun AppSelectorScreen(
title = stringResource(R.string.select_app),
onBackClick = onBackClick,
actions = {
- IconButton(onClick = { }) {
- Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
- }
IconButton(onClick = { search = true }) {
Icon(Icons.Outlined.Search, stringResource(R.string.search))
}
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 3ab5032409..be71514be3 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
@@ -11,7 +11,6 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.DeleteOutline
-import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source
@@ -132,9 +131,6 @@ fun DashboardScreen(
AppTopBar(
title = stringResource(R.string.app_name),
actions = {
- IconButton(onClick = {}) {
- Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
- }
IconButton(onClick = onSettingsClick) {
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt
index 82bd6f6bb1..2a2a2b8951 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt
@@ -2,10 +2,15 @@ package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -13,14 +18,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
+import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.LoadingIndicator
+import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
import org.koin.androidx.compose.getViewModel
@@ -31,43 +39,56 @@ fun InstalledAppsScreen(
) {
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
- ) {
- installedApps?.let { installedApps ->
+ Column {
+ if (!Aapt.supportsDevice())
+ Box(modifier = Modifier.padding(16.dp)) {
+ NotificationCard(
+ color = MaterialTheme.colorScheme.errorContainer,
+ icon = Icons.Outlined.WarningAmber,
+ text = stringResource(
+ R.string.unsupported_architecture_warning
+ ),
+ )
+ }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = if (installedApps.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
+ ) {
+ installedApps?.let { installedApps ->
- if (installedApps.isNotEmpty()) {
- items(
- installedApps,
- key = { it.currentPackageName }
- ) { installedApp ->
- viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
- ListItem(
- modifier = Modifier.clickable { onAppClick(installedApp) },
- leadingContent = {
- AppIcon(
- packageInfo,
- contentDescription = null,
- Modifier.size(36.dp)
- )
- },
- headlineContent = { AppLabel(packageInfo, defaultText = null) },
- supportingContent = { Text(installedApp.currentPackageName) }
- )
+ if (installedApps.isNotEmpty()) {
+ items(
+ installedApps,
+ key = { it.currentPackageName }
+ ) { installedApp ->
+ viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
+ ListItem(
+ modifier = Modifier.clickable { onAppClick(installedApp) },
+ leadingContent = {
+ AppIcon(
+ packageInfo,
+ contentDescription = null,
+ Modifier.size(36.dp)
+ )
+ },
+ headlineContent = { AppLabel(packageInfo, defaultText = null) },
+ supportingContent = { Text(installedApp.currentPackageName) }
+ )
+ }
+ }
+ } else {
+ item {
+ Text(
+ text = stringResource(R.string.no_patched_apps_found),
+ style = MaterialTheme.typography.titleLarge
+ )
}
}
- } else {
- item {
- Text(
- text = stringResource(R.string.no_patched_apps_found),
- style = MaterialTheme.typography.titleLarge
- )
- }
- }
- } ?: item { LoadingIndicator() }
+ } ?: item { LoadingIndicator() }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt
index 02fe4ee810..c2a6d30052 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt
@@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -75,9 +74,6 @@ fun InstallerScreen(
title = stringResource(R.string.installer),
onBackClick = onBackClick,
actions = {
- IconButton(onClick = {}) {
- Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
- }
IconButton(onClick = { dropdownActive = true }) {
Icon(Icons.Outlined.MoreVert, stringResource(R.string.more))
}
@@ -210,7 +206,7 @@ fun InstallStep(step: Step) {
Spacer(modifier = Modifier.weight(1f))
- ArrowButton(expanded = expanded) {
+ ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded) {
expanded = !expanded
}
}
@@ -222,7 +218,6 @@ fun InstallStep(step: Step) {
.background(MaterialTheme.colorScheme.background.copy(0.6f))
.fillMaxWidth()
.padding(16.dp)
- .padding(start = 4.dp)
) {
step.subSteps.forEach { subStep ->
var messageExpanded by rememberSaveable { mutableStateOf(true) }
@@ -233,7 +228,7 @@ fun InstallStep(step: Step) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
- StepIcon(subStep.state, downloadProgress?.value, size = 18.dp)
+ StepIcon(subStep.state, downloadProgress?.value, size = 24.dp)
Text(
text = subStep.name,
@@ -244,7 +239,10 @@ fun InstallStep(step: Step) {
)
if (stacktrace != null) {
- ArrowButton(expanded = messageExpanded) {
+ ArrowButton(
+ modifier = Modifier.size(24.dp),
+ expanded = messageExpanded
+ ) {
messageExpanded = !messageExpanded
}
} else {
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
index 12f083156f..1165e4abf5 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
@@ -14,13 +14,17 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Build
+import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilterChip
@@ -28,33 +32,45 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.SearchBar
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
+import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar
+import app.revanced.manager.ui.component.Countdown
import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
+import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.BaseSelectionMode
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.launch
+import org.koin.compose.rememberKoinInject
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
@@ -65,9 +81,86 @@ fun PatchesSelectorScreen(
) {
val pagerState = rememberPagerState()
val composableScope = rememberCoroutineScope()
+ var search: String? by rememberSaveable {
+ mutableStateOf(null)
+ }
+ var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
+ if (showBottomSheet) {
+ ModalBottomSheet(
+ onDismissRequest = {
+ showBottomSheet = false
+ }
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 24.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.patches_selector_sheet_filter_title),
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.patches_selector_sheet_filter_compat_title),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ FilterChip(
+ selected = vm.filter and SHOW_SUPPORTED != 0,
+ onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
+ label = { Text(stringResource(R.string.supported)) }
+ )
+
+ FilterChip(
+ selected = vm.filter and SHOW_UNIVERSAL != 0,
+ onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
+ label = { Text(stringResource(R.string.universal)) },
+ )
+
+ FilterChip(
+ selected = vm.filter and SHOW_UNSUPPORTED != 0,
+ onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
+ label = { Text(stringResource(R.string.unsupported)) },
+ )
+ }
+ }
+
+ Divider()
+
+ ListItem(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ enabled = vm.hasPreviousSelection,
+ onClick = vm::switchBaseSelectionMode
+ ),
+ leadingContent = {
+ Checkbox(
+ checked = vm.baseSelectionMode == BaseSelectionMode.PREVIOUS,
+ onCheckedChange = {
+ vm.switchBaseSelectionMode()
+ },
+ enabled = vm.hasPreviousSelection
+ )
+ },
+ headlineContent = {
+ Text(
+ "Use previous selection",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ )
+ }
+ }
+
if (vm.compatibleVersions.isNotEmpty())
UnsupportedDialog(
appVersion = vm.input.selectedApp.version,
@@ -85,31 +178,152 @@ fun PatchesSelectorScreen(
)
}
+ vm.pendingSelectionAction?.let {
+ SelectionWarningDialog(
+ onCancel = vm::dismissSelectionWarning,
+ onConfirm = vm::confirmSelectionWarning
+ )
+ }
+
+ val allowExperimental by vm.allowExperimental.getAsState()
+
+ fun LazyListScope.patchList(
+ uid: Int,
+ patches: List,
+ filterFlag: Int,
+ supported: Boolean,
+ header: (@Composable () -> Unit)? = null
+ ) {
+ if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
+ header?.let {
+ item {
+ it()
+ }
+ }
+
+ items(
+ items = patches,
+ key = { it.name }
+ ) { patch ->
+ PatchItem(
+ patch = patch,
+ onOptionsDialog = {
+ vm.optionsDialog = uid to patch
+ },
+ selected = supported && vm.isSelected(
+ uid,
+ patch
+ ),
+ onToggle = {
+ if (vm.selectionWarningEnabled) {
+ vm.pendingSelectionAction = {
+ vm.togglePatch(uid, patch)
+ }
+ } else {
+ vm.togglePatch(uid, patch)
+ }
+ },
+ supported = supported
+ )
+ }
+ }
+ }
+
+ search?.let { query ->
+ SearchBar(
+ query = query,
+ onQueryChange = { new ->
+ search = new
+ },
+ onSearch = {},
+ active = true,
+ onActiveChange = { new ->
+ if (new) return@SearchBar
+ search = null
+ },
+ placeholder = {
+ Text(stringResource(R.string.search_patches))
+ },
+ leadingIcon = {
+ IconButton(onClick = { search = null }) {
+ Icon(
+ Icons.Default.ArrowBack,
+ stringResource(R.string.back)
+ )
+ }
+ }
+ ) {
+ val bundle = bundles[pagerState.currentPage]
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ fun List.searched() = filter {
+ it.name.contains(query, true)
+ }
+
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.supported.searched(),
+ filterFlag = SHOW_SUPPORTED,
+ supported = true
+ )
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.universal.searched(),
+ filterFlag = SHOW_UNIVERSAL,
+ supported = true
+ ) {
+ ListHeader(
+ title = stringResource(R.string.universal_patches),
+ )
+ }
+
+ if (!allowExperimental) return@LazyColumn
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.unsupported.searched(),
+ filterFlag = SHOW_UNSUPPORTED,
+ supported = true
+ ) {
+ ListHeader(
+ title = stringResource(R.string.unsupported_patches),
+ onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
+ )
+ }
+ }
+ }
+ }
+
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.select_patches),
onBackClick = onBackClick,
actions = {
- IconButton(onClick = { }) {
- Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
+ IconButton(onClick = vm::reset) {
+ Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
+ }
+ IconButton(onClick = { showBottomSheet = true }) {
+ Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
}
- IconButton(onClick = { }) {
+ IconButton(
+ onClick = {
+ search = ""
+ }
+ ) {
Icon(Icons.Outlined.Search, stringResource(R.string.search))
}
}
)
},
floatingActionButton = {
- if (!vm.isSelectionEmpty()) {
- ExtendedFloatingActionButton(
- text = { Text(stringResource(R.string.patch)) },
- icon = { Icon(Icons.Default.Build, null) },
- onClick = {
- composableScope.launch {
- // TODO: only allow this if all required options have been set.
- onPatchClick(vm.getAndSaveSelection(), vm.getOptions())
- }
+ ExtendedFloatingActionButton(
+ text = { Text(stringResource(R.string.patch)) },
+ icon = { Icon(Icons.Default.Build, null) },
+ onClick = {
+ // TODO: only allow this if all required options have been set.
+ composableScope.launch {
+ val selection = vm.getSelection()
+ vm.saveSelection(selection).join()
+ onPatchClick(selection, vm.getOptions())
}
)
}
@@ -150,99 +364,35 @@ fun PatchesSelectorScreen(
pageContent = { index ->
val bundle = bundles[index]
- Column {
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 10.dp, vertical = 2.dp),
- horizontalArrangement = Arrangement.spacedBy(5.dp)
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.supported,
+ filterFlag = SHOW_SUPPORTED,
+ supported = true
+ )
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.universal,
+ filterFlag = SHOW_UNIVERSAL,
+ supported = true
) {
- FilterChip(
- selected = vm.filter and SHOW_SUPPORTED != 0 && bundle.supported.isNotEmpty(),
- onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
- label = { Text(stringResource(R.string.supported)) },
- enabled = bundle.supported.isNotEmpty()
- )
-
- FilterChip(
- selected = vm.filter and SHOW_UNIVERSAL != 0 && bundle.universal.isNotEmpty(),
- onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
- label = { Text(stringResource(R.string.universal)) },
- enabled = bundle.universal.isNotEmpty()
- )
-
- FilterChip(
- selected = vm.filter and SHOW_UNSUPPORTED != 0 && bundle.unsupported.isNotEmpty(),
- onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
- label = { Text(stringResource(R.string.unsupported)) },
- enabled = bundle.unsupported.isNotEmpty()
+ ListHeader(
+ title = stringResource(R.string.universal_patches),
)
}
-
- val allowExperimental by vm.allowExperimental.getAsState()
-
- LazyColumn(
- modifier = Modifier.fillMaxSize()
+ patchList(
+ uid = bundle.uid,
+ patches = bundle.unsupported,
+ filterFlag = SHOW_UNSUPPORTED,
+ supported = allowExperimental
) {
- fun LazyListScope.patchList(
- patches: List,
- filterFlag: Int,
- supported: Boolean,
- header: (@Composable () -> Unit)? = null
- ) {
- if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
- header?.let {
- item {
- it()
- }
- }
-
- items(
- items = patches,
- key = { it.name }
- ) { patch ->
- PatchItem(
- patch = patch,
- onOptionsDialog = {
- vm.optionsDialog = bundle.uid to patch
- },
- selected = supported && vm.isSelected(
- bundle.uid,
- patch
- ),
- onToggle = { vm.togglePatch(bundle.uid, patch) },
- supported = supported
- )
- }
- }
- }
-
- patchList(
- patches = bundle.supported,
- filterFlag = SHOW_SUPPORTED,
- supported = true
+ ListHeader(
+ title = stringResource(R.string.unsupported_patches),
+ onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
)
- patchList(
- patches = bundle.universal,
- filterFlag = SHOW_UNIVERSAL,
- supported = true
- ) {
- ListHeader(
- title = stringResource(R.string.universal_patches),
- onHelpClick = { }
- )
- }
- patchList(
- patches = bundle.unsupported,
- filterFlag = SHOW_UNSUPPORTED,
- supported = allowExperimental
- ) {
- ListHeader(
- title = stringResource(R.string.unsupported_patches),
- onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
- )
- }
}
}
}
@@ -251,6 +401,84 @@ fun PatchesSelectorScreen(
}
}
+@Composable
+fun SelectionWarningDialog(
+ onCancel: () -> Unit,
+ onConfirm: (Boolean) -> Unit
+) {
+ val prefs: PreferencesManager = rememberKoinInject()
+ var dismissPermanently by rememberSaveable {
+ mutableStateOf(false)
+ }
+
+ AlertDialog(
+ onDismissRequest = onCancel,
+ confirmButton = {
+ val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState()
+
+ Countdown(start = if (enableCountdown) 3 else 0) { timer ->
+ LaunchedEffect(timer) {
+ if (timer == 0) prefs.enableSelectionWarningCountdown.update(false)
+ }
+
+ TextButton(
+ onClick = { onConfirm(dismissPermanently) },
+ enabled = timer == 0
+ ) {
+ val text =
+ if (timer == 0) stringResource(R.string.continue_) else stringResource(
+ R.string.selection_warning_continue_countdown, timer
+ )
+ Text(text, color = MaterialTheme.colorScheme.error)
+ }
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onCancel) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ icon = {
+ Icon(Icons.Outlined.WarningAmber, null)
+ },
+ title = {
+ Text(
+ text = stringResource(R.string.selection_warning_title),
+ style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(
+ text = stringResource(R.string.selection_warning_description),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(0.dp),
+ modifier = Modifier.clickable {
+ dismissPermanently = !dismissPermanently
+ }
+ ) {
+ Checkbox(
+ checked = dismissPermanently,
+ onCheckedChange = {
+ dismissPermanently = it
+ }
+ )
+ Text(stringResource(R.string.permanent_dismiss))
+ }
+ }
+ }
+ )
+}
+
@Composable
fun PatchItem(
patch: PatchInfo,
@@ -266,7 +494,7 @@ fun PatchItem(
leadingContent = {
Checkbox(
checked = selected,
- onCheckedChange = null,
+ onCheckedChange = { onToggle() },
enabled = supported
)
},
@@ -296,7 +524,7 @@ fun ListHeader(
},
trailingContent = onHelpClick?.let {
{
- IconButton(onClick = onHelpClick) {
+ IconButton(onClick = it) {
Icon(
Icons.Outlined.HelpOutline,
stringResource(R.string.help)
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 59e3ffb022..3aaa7d4465 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
@@ -129,7 +129,7 @@ fun SettingsScreen(
)
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
- { navController.pop() },
+ onBackClick = { navController.pop() },
)
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
index 9e8011a902..dd52326c19 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
@@ -9,11 +9,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
@@ -70,11 +68,6 @@ fun VersionSelectorScreen(
AppTopBar(
title = stringResource(R.string.select_version),
onBackClick = onBackClick,
- actions = {
- IconButton(onClick = { }) {
- Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help))
- }
- }
)
},
floatingActionButton = {
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt
index b47e8fd519..862cd6fb8e 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt
@@ -17,12 +17,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.destination.SettingsDestination
+import app.revanced.manager.util.isDebuggable
import app.revanced.manager.util.openUrl
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import dev.olshevski.navigation.reimagined.NavController
@@ -36,7 +38,7 @@ fun AboutSettingsScreen(
onLicensesClick: () -> Unit,
) {
val context = LocalContext.current
- val icon = rememberDrawablePainter(context.packageManager.getApplicationIcon(context.packageName))
+ val icon = painterResource(R.drawable.ic_logo_ring)
val filledButton = listOf(
Triple(Icons.Outlined.FavoriteBorder, stringResource(R.string.donate)) {
@@ -56,13 +58,15 @@ fun AboutSettingsScreen(
}),
)
- val listItems = listOf(
+ val listItems = listOfNotNull(
Triple(stringResource(R.string.submit_feedback), stringResource(R.string.submit_feedback_description),
- third = { /*TODO*/ }),
+ third = {
+ context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
+ }),
Triple(stringResource(R.string.contributors), stringResource(R.string.contributors_description),
- third = onContributorsClick),
+ third = onContributorsClick).takeIf { context.isDebuggable },
Triple(stringResource(R.string.developer_options), stringResource(R.string.developer_options_description),
- third = { /*TODO*/ }),
+ third = { /*TODO*/ }).takeIf { context.isDebuggable },
Triple(stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses_description),
third = onLicensesClick)
)
@@ -191,4 +195,4 @@ fun AboutSettingsScreen(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
index e390332a2c..4af8e908d3 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
@@ -32,9 +32,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
+import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader
+import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import org.koin.androidx.compose.getViewModel
@@ -85,6 +87,14 @@ fun AdvancedSettingsScreen(
}
)
+ GroupHeader(stringResource(R.string.patcher))
+ BooleanItem(
+ preference = vm.allowExperimental,
+ coroutineScope = vm.viewModelScope,
+ headline = R.string.experimental_patches,
+ description = R.string.experimental_patches_description
+ )
+
GroupHeader(stringResource(R.string.patch_bundles_section))
ListItem(
headlineContent = { Text(stringResource(R.string.patch_bundles_redownload)) },
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
index 6838f927a1..ed25e7b231 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
@@ -1,7 +1,6 @@
package app.revanced.manager.ui.screen.settings
import android.os.Build
-import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -23,7 +22,6 @@ import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.SettingsViewModel
-import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@@ -82,14 +80,6 @@ fun GeneralSettingsScreen(
description = R.string.dynamic_color_description
)
}
-
- GroupHeader(stringResource(R.string.patcher))
- BooleanItem(
- preference = prefs.allowExperimental,
- coroutineScope = coroutineScope,
- headline = R.string.experimental_patches,
- description = R.string.experimental_patches_description
- )
}
}
}
@@ -123,10 +113,12 @@ private fun ThemePicker(
}
},
confirmButton = {
- Button(onClick = {
- onConfirm(selectedTheme)
- onDismiss()
- }) {
+ Button(
+ onClick = {
+ onConfirm(selectedTheme)
+ onDismiss()
+ }
+ ) {
Text(stringResource(R.string.apply))
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt
index a1b1f86233..5ba085e6a6 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt
@@ -91,21 +91,6 @@ fun ManagerUpdateChangelog(
color = MaterialTheme.colorScheme.outline,
)
}
- Row(
- horizontalArrangement = Arrangement.spacedBy(6.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Outlined.FileDownload,
- contentDescription = null,
- modifier = Modifier.size(16.dp)
- )
- Text(
- vm.formattedDownloadCount,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.outline,
- )
- }
}
Markdown(
vm.changelogHtml,
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt
index 6b084005c1..6d7d79b83d 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt
@@ -17,6 +17,7 @@ class AdvancedSettingsViewModel(
private val patchBundleRepository: PatchBundleRepository
) : ViewModel() {
val apiUrl = prefs.api
+ val allowExperimental = prefs.allowExperimental
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
if (value == apiUrl.get()) return@launch
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
index ff137be81c..62915e0b48 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
@@ -13,20 +13,27 @@ class AppSelectorViewModel(
private val app: Application,
private val pm: PM
) : ViewModel() {
+ private val inputFile = File(app.cacheDir, "input.apk").also {
+ it.delete()
+ }
val appList = pm.appList
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
fun loadSelectedFile(uri: Uri) =
app.contentResolver.openInputStream(uri)?.use { stream ->
- File(app.cacheDir, "input.apk").also {
- it.delete()
- Files.copy(stream, it.toPath())
- }.let { file ->
- pm.getPackageInfo(file)
- ?.let { packageInfo ->
- SelectedApp.Local(packageName = packageInfo.packageName, version = packageInfo.versionName, file = file)
- }
+ with(inputFile) {
+ delete()
+ Files.copy(stream, toPath())
+
+ pm.getPackageInfo(this)?.let { packageInfo ->
+ SelectedApp.Local(
+ packageName = packageInfo.packageName,
+ version = packageInfo.versionName,
+ file = this,
+ shouldDelete = true
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt
index 2bdee157cd..85f36fc7fe 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt
@@ -31,6 +31,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting
+import kotlin.io.path.inputStream
@OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel(
@@ -59,9 +60,11 @@ class ImportExportViewModel(
}
}
- knownPasswords.forEach {
- if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) {
- return@launch
+ aliases.forEach { alias ->
+ knownPasswords.forEach { pass ->
+ if (tryKeystoreImport(alias, pass, path)) {
+ return@launch
+ }
}
}
@@ -77,9 +80,11 @@ class ImportExportViewModel(
tryKeystoreImport(cn, pass, keystoreImportPath!!)
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
- if (keystoreManager.import(cn, pass, path)) {
- cancelKeystoreImport()
- return true
+ path.inputStream().use { stream ->
+ if (keystoreManager.import(cn, pass, stream)) {
+ cancelKeystoreImport()
+ return true
+ }
}
return false
@@ -174,6 +179,7 @@ class ImportExportViewModel(
}
private companion object {
- val knownPasswords = setOf("ReVanced", "s3cur3p@ssw0rd")
+ val knownPasswords = arrayOf("ReVanced", "s3cur3p@ssw0rd")
+ val aliases = arrayOf(KeystoreManager.DEFAULT, "alias", "ReVanced Key")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
index a1a5ebd272..1a78078a7c 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
@@ -19,6 +19,7 @@ import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import app.revanced.manager.R
+import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
@@ -57,16 +58,22 @@ class InstallerViewModel(
) : ViewModel(), KoinComponent {
private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject()
+ private val fs: Filesystem by inject()
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
val packageName: String = input.selectedApp.packageName
- private val outputFile = File(app.cacheDir, "output.apk")
- private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
+ private val tempDir = fs.tempDir.resolve("installer").also {
+ it.deleteRecursively()
+ it.mkdirs()
+ }
+
+ private val outputFile = tempDir.resolve("output.apk")
+ private val signedFile = tempDir.resolve("signed.apk")
private var hasSigned = false
- var inputFile: File? = null
+ private var inputFile: File? = null
private var installedApp: InstalledApp? = null
var isInstalling by mutableStateOf(false)
@@ -82,6 +89,8 @@ class InstallerViewModel(
private val logger = ManagerLogger()
init {
+ // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
+
viewModelScope.launch {
installedApp = installedAppRepository.get(packageName)
}
@@ -101,11 +110,8 @@ class InstallerViewModel(
outputFile.path,
patches,
options,
- packageName,
- selectedApp.version,
_progress,
logger,
- selectedApp,
setInputFile = { inputFile = it }
)
)
@@ -176,8 +182,14 @@ class InstallerViewModel(
app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId)
- outputFile.delete()
- signedFile.delete()
+ when (val selectedApp = input.selectedApp) {
+ is SelectedApp.Local -> {
+ if (selectedApp.shouldDelete) selectedApp.file.delete()
+ }
+ else -> {}
+ }
+
+ tempDir.deleteRecursively()
try {
if (input.selectedApp is SelectedApp.Installed) {
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
index 5592d3e79d..baf017b18d 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
@@ -1,21 +1,28 @@
package app.revanced.manager.ui.viewmodel
+import android.util.Base64
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
+import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
-import kotlinx.coroutines.Dispatchers
+import app.revanced.manager.domain.repository.PatchSelectionRepository
+import app.revanced.manager.domain.repository.SerializedSelection
+import app.revanced.manager.ui.theme.Theme
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
class MainViewModel(
private val patchBundleRepository: PatchBundleRepository,
+ private val patchSelectionRepository: PatchSelectionRepository,
+ private val keystoreManager: KeystoreManager,
val prefs: PreferencesManager
) : ViewModel() {
-
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
- prefs.showAutoUpdatesDialog.update(false)
+ prefs.firstLaunch.update(false)
prefs.managerAutoUpdates.update(manager)
if (patches) {
@@ -30,4 +37,66 @@ class MainViewModel(
}
}
}
-}
\ No newline at end of file
+
+ fun applyLegacySettings(data: String) = viewModelScope.launch {
+ val json = Json { ignoreUnknownKeys = true }
+ val settings = json.decodeFromString(data)
+
+ settings.themeMode?.let { theme ->
+ val themeMap = mapOf(
+ 0 to Theme.SYSTEM,
+ 1 to Theme.LIGHT,
+ 2 to Theme.DARK
+ )
+ prefs.theme.update(themeMap[theme]!!)
+ }
+ settings.useDynamicTheme?.let { dynamicColor ->
+ prefs.dynamicColor.update(dynamicColor)
+ }
+ settings.apiUrl?.let { api ->
+ prefs.api.update(api.removeSuffix("/"))
+ }
+ settings.experimentalPatchesEnabled?.let { allowExperimental ->
+ prefs.allowExperimental.update(allowExperimental)
+ }
+ settings.patchesAutoUpdate?.let { autoUpdate ->
+ with(patchBundleRepository) {
+ sources
+ .first()
+ .find { it.uid == 0 }
+ ?.asRemoteOrNull
+ ?.setAutoUpdate(autoUpdate)
+
+ updateCheck()
+ }
+ }
+ settings.patchesChangeEnabled?.let { disableSelectionWarning ->
+ prefs.disableSelectionWarning.update(disableSelectionWarning)
+ }
+ settings.keystore?.let { keystore ->
+ val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
+ keystoreManager.import(
+ "ReVanced",
+ settings.keystorePassword,
+ keystoreBytes.inputStream()
+ )
+ }
+ settings.patches?.let { selection ->
+ patchSelectionRepository.import(0, selection)
+ }
+ prefs.firstLaunch.update(false)
+ }
+
+ @Serializable
+ private data class LegacySettings(
+ val keystorePassword: String,
+ val themeMode: Int? = null,
+ val useDynamicTheme: Boolean? = null,
+ val apiUrl: String? = null,
+ val experimentalPatchesEnabled: Boolean? = null,
+ val patchesAutoUpdate: Boolean? = null,
+ val patchesChangeEnabled: Boolean? = null,
+ val keystore: String? = null,
+ val patches: SerializedSelection? = null,
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt
index e4d774007e..02d187ea67 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt
@@ -8,8 +8,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
-import app.revanced.manager.domain.repository.GithubRepository
-import app.revanced.manager.network.dto.GithubChangelog
+import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
@@ -18,30 +17,19 @@ import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
class ManagerUpdateChangelogViewModel(
- private val githubRepository: GithubRepository,
+ private val api: ReVancedAPI,
private val app: Application,
) : ViewModel() {
private val markdownFlavour = GFMFlavourDescriptor()
private val markdownParser = MarkdownParser(flavour = markdownFlavour)
var changelog by mutableStateOf(
- GithubChangelog(
+ Changelog(
"...",
app.getString(R.string.changelog_loading),
- emptyList()
)
)
private set
- val formattedDownloadCount by derivedStateOf {
- val downloadCount = changelog.assets.firstOrNull()?.downloadCount?.toDouble() ?: 0.0
- if (downloadCount > 1000) {
- val roundedValue =
- (downloadCount / 100).toInt() / 10.0 // Divide by 100 and round to one decimal place
- "${roundedValue}k"
- } else {
- downloadCount.toString()
- }
- }
val changelogHtml by derivedStateOf {
val markdown = changelog.body
val parsedTree = markdownParser.buildMarkdownTreeFromString(markdown)
@@ -51,8 +39,15 @@ class ManagerUpdateChangelogViewModel(
init {
viewModelScope.launch {
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
- changelog = githubRepository.getChangelog("revanced-manager").getOrThrow()
+ changelog = api.getRelease("revanced-manager").getOrThrow().let {
+ Changelog(it.metadata.tag, it.metadata.body)
+ }
}
}
}
+
+ data class Changelog(
+ val version: String,
+ val body: String,
+ )
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
index 4ccb11eb2c..3547e6f711 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
@@ -1,18 +1,21 @@
package app.revanced.manager.ui.viewmodel
+import android.app.Application
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
+import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
@@ -20,12 +23,9 @@ import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
-import app.revanced.manager.util.SnapshotStateSet
import app.revanced.manager.util.flatMapLatestAndCombine
-import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateMapSaver
-import app.revanced.manager.util.saver.snapshotStateSetSaver
-import app.revanced.manager.util.toMutableStateSet
+import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -39,8 +39,17 @@ import org.koin.core.component.get
class PatchesSelectorViewModel(
val input: Destination.PatchesSelector
) : ViewModel(), KoinComponent {
+ private val app: Application = get()
private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = get()
+ private val prefs: PreferencesManager = get()
+
+ private val packageName = input.selectedApp.packageName
+
+ var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null)
+
+ var selectionWarningEnabled by mutableStateOf(true)
+ private set
val allowExperimental = get().allowExperimental
val bundlesFlow = get().sources.flatMapLatestAndCombine(
@@ -54,10 +63,14 @@ class PatchesSelectorViewModel(
val unsupported = mutableListOf()
val universal = mutableListOf()
- bundle.patches.filter { it.compatibleWith(input.selectedApp.packageName) }.forEach {
+ bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
val targetList = when {
it.compatiblePackages == null -> universal
- it.supportsVersion(input.selectedApp.version) -> supported
+ it.supportsVersion(
+ input.selectedApp.packageName,
+ input.selectedApp.version
+ ) -> supported
+
else -> unsupported
}
@@ -68,38 +81,72 @@ class PatchesSelectorViewModel(
}
}
- private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(
- saver = patchesSelectionSaver,
- init = {
- val map: SnapshotStatePatchesSelection = mutableStateMapOf()
- viewModelScope.launch(Dispatchers.Default) {
- val bundles = bundlesFlow.first()
- val filteredSelection =
- (input.patchesSelection
- ?: selectionRepository.getSelection(input.selectedApp.packageName))
- .mapValues { (uid, patches) ->
- // Filter out patches that don't exist.
- val filteredPatches = bundles.singleOrNull { it.uid == uid }
- ?.let { bundle ->
- val allPatches = bundle.all.map { it.name }
- patches.filter { allPatches.contains(it) }
- }
- ?: patches
-
- filteredPatches.toMutableStateSet()
- }
-
- withContext(Dispatchers.Main) {
- map.putAll(filteredSelection)
- }
+ init {
+ viewModelScope.launch {
+ if (prefs.disableSelectionWarning.get()) {
+ selectionWarningEnabled = false
+ return@launch
}
- return@saveable map
- })
- private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(
+
+ val experimental = allowExperimental.get()
+ fun BundleInfo.hasDefaultPatches(): Boolean {
+ return if (experimental) {
+ all.asSequence()
+ } else {
+ sequence {
+ yieldAll(supported)
+ yieldAll(universal)
+ }
+ }.any { it.include }
+ }
+
+ // Don't show the warning if there are no default patches.
+ selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
+ }
+ }
+
+ var baseSelectionMode by mutableStateOf(BaseSelectionMode.DEFAULT)
+ private set
+
+ private val previousPatchesSelection: SnapshotStateMap> = mutableStateMapOf()
+
+ init {
+ viewModelScope.launch(Dispatchers.Default) { loadPreviousSelection() }
+ }
+
+ val hasPreviousSelection by derivedStateOf {
+ previousPatchesSelection.filterValues(Set::isNotEmpty).isNotEmpty()
+ }
+
+ private var hasModifiedSelection = false
+
+ private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable(
+ saver = explicitPatchesSelectionSaver,
+ init = ::mutableStateMapOf
+ )
+
+ private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
saver = optionsSaver,
init = ::mutableStateMapOf
)
+ private val selectors by derivedStateOf> {
+ arrayOf(
+ // Patches that were explicitly selected
+ { bundle, patch ->
+ explicitPatchesSelection[bundle]?.get(patch.name)
+ },
+ // The fallback selection.
+ when (baseSelectionMode) {
+ BaseSelectionMode.DEFAULT -> ({ _, patch -> patch.include })
+
+ BaseSelectionMode.PREVIOUS -> ({ bundle, patch ->
+ previousPatchesSelection[bundle]?.contains(patch.name) ?: false
+ })
+ }
+ )
+ }
+
/**
* Show the patch options dialog for this patch.
*/
@@ -107,37 +154,93 @@ class PatchesSelectorViewModel(
val compatibleVersions = mutableStateListOf()
- var filter by mutableStateOf(SHOW_SUPPORTED or SHOW_UNSUPPORTED)
+ var filter by mutableStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED)
private set
+ private suspend fun loadPreviousSelection() {
+ val selection = (input.patchesSelection ?: selectionRepository.getSelection(
+ packageName
+ )).mapValues { (_, value) -> value.toSet() }
+
+ withContext(Dispatchers.Main) {
+ previousPatchesSelection.putAll(selection)
+ }
+ }
+
+ fun isSelectionEmpty() = selectedPatches.values.all { it.isEmpty() }
+
+ fun switchBaseSelectionMode() = viewModelScope.launch {
+ baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) {
+ BaseSelectionMode.PREVIOUS
+ } else {
+ BaseSelectionMode.DEFAULT
+ }
+ }
+
private fun getOrCreateSelection(bundle: Int) =
- selectedPatches.getOrPut(bundle, ::mutableStateSetOf)
+ explicitPatchesSelection.getOrPut(bundle, ::mutableStateMapOf)
fun isSelected(bundle: Int, patch: PatchInfo) =
- selectedPatches[bundle]?.contains(patch.name) ?: false
+ selectors.firstNotNullOf { fn -> fn(bundle, patch) }
fun togglePatch(bundle: Int, patch: PatchInfo) {
- val name = patch.name
val patches = getOrCreateSelection(bundle)
- if (patches.contains(name)) patches.remove(name) else patches.add(name)
+ hasModifiedSelection = true
+ patches[patch.name] = !isSelected(bundle, patch)
}
- fun isSelectionEmpty() = selectedPatches.values.all { it.isEmpty() }
-
- suspend fun getAndSaveSelection(): PatchesSelection =
- selectedPatches.also {
- withContext(Dispatchers.Default) {
- selectionRepository.updateSelection(input.selectedApp.packageName, it)
- }
- }.mapValues { it.value.toMutableSet() }.apply {
- if (allowExperimental.get()) {
- return@apply
+ fun confirmSelectionWarning(dismissPermanently: Boolean) {
+ selectionWarningEnabled = false
+
+ pendingSelectionAction?.invoke()
+ pendingSelectionAction = null
+
+ if (!dismissPermanently) return
+
+ viewModelScope.launch {
+ prefs.disableSelectionWarning.update(true)
+ }
+ }
+
+ fun dismissSelectionWarning() {
+ pendingSelectionAction = null
+ }
+
+ fun reset() {
+ patchOptions.clear()
+ baseSelectionMode = BaseSelectionMode.DEFAULT
+ explicitPatchesSelection.clear()
+ hasModifiedSelection = false
+ app.toast(app.getString(R.string.patch_selection_reset_toast))
+ }
+
+ suspend fun getSelection(): PatchesSelection {
+ val bundles = bundlesFlow.first()
+ val removeUnsupported = !allowExperimental.get()
+
+ return bundles.associate { bundle ->
+ val included =
+ bundle.all.filter { isSelected(bundle.uid, it) }.map { it.name }.toMutableSet()
+
+ if (removeUnsupported) {
+ val unsupported = bundle.unsupported.map { it.name }.toSet()
+ included.removeAll(unsupported)
}
- // Filter out unsupported patches that may have gotten selected through the database if the setting is not enabled.
- bundlesFlow.first().forEach {
- this[it.uid]?.removeAll(it.unsupported.map { patch -> patch.name }.toSet())
+ bundle.uid to included
+ }
+ }
+
+ suspend fun saveSelection(selection: PatchesSelection) =
+ viewModelScope.launch(Dispatchers.Default) {
+ when {
+ hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
+ baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
+ packageName
+ )
+
+ else -> {}
}
}
@@ -157,17 +260,10 @@ class PatchesSelectorViewModel(
compatibleVersions.clear()
}
- fun openUnsupportedDialog(unsupportedVersions: List) {
- val set = HashSet()
-
- unsupportedVersions.forEach { patch ->
- patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
- ?.let { compatiblePackage ->
- set.addAll(compatiblePackage.versions)
- }
- }
-
- compatibleVersions.addAll(set)
+ fun openUnsupportedDialog(unsupportedPatches: List) {
+ compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
+ patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty()
+ })
}
fun toggleFlag(flag: Int) {
@@ -182,7 +278,7 @@ class PatchesSelectorViewModel(
private fun SnapshotStateMap>.getOrCreate(key: K) =
getOrPut(key, ::mutableStateMapOf)
- private val optionsSaver: Saver = snapshotStateMapSaver(
+ private val optionsSaver: Saver = snapshotStateMapSaver(
// Patch name -> Options
valueSaver = snapshotStateMapSaver(
// Option key -> Option value
@@ -190,8 +286,24 @@ class PatchesSelectorViewModel(
)
)
- private val patchesSelectionSaver: Saver =
- snapshotStateMapSaver(valueSaver = snapshotStateSetSaver())
+ private val explicitPatchesSelectionSaver: Saver =
+ snapshotStateMapSaver(valueSaver = snapshotStateMapSaver())
+ }
+
+ /**
+ * An enum for controlling the behavior of the selector.
+ */
+ enum class BaseSelectionMode {
+ /**
+ * Selection is determined by the [PatchInfo.include] field.
+ */
+ DEFAULT,
+
+ /**
+ * Selection is determined by what the user selected previously.
+ * Any patch that is not part of the previous selection will be deselected.
+ */
+ PREVIOUS
}
data class BundleInfo(
@@ -204,12 +316,9 @@ class PatchesSelectorViewModel(
)
}
-/**
- * [Options] but with observable collection types.
- */
-private typealias SnapshotStateOptions = SnapshotStateMap>>
+private typealias Selector = (Int, PatchInfo) -> Boolean?
+private typealias ExplicitPatchesSelection = Map>
-/**
- * [PatchesSelection] but with observable collection types.
- */
-private typealias SnapshotStatePatchesSelection = SnapshotStateMap>
\ No newline at end of file
+// Versions of other types, but utilizing observable collection types instead.
+private typealias SnapshotOptions = SnapshotStateMap>>
+private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap>
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
index 222d89dfd2..cae25116fd 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
@@ -54,8 +54,8 @@ class VersionSelectorViewModel(
bundle.patches.flatMap { patch ->
patch.compatiblePackages.orEmpty()
.filter { it.packageName == packageName }
- .onEach { if (it.versions.isEmpty()) patchesWithoutVersions++ }
- .flatMap { it.versions }
+ .onEach { if (it.versions == null) patchesWithoutVersions++ }
+ .flatMap { it.versions.orEmpty() }
}
}.groupingBy { it }
.eachCount()
@@ -68,7 +68,7 @@ class VersionSelectorViewModel(
}
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
- downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, it.file) }
+ downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, downloadedAppRepository.getApkFileForApp(it), false) }
}
init {
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index e8f38056e4..ed10ea6d77 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -2,6 +2,7 @@ package app.revanced.manager.util
import android.content.Context
import android.content.Intent
+import android.content.pm.ApplicationInfo
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
@@ -22,6 +23,8 @@ import java.util.Locale
typealias PatchesSelection = Map>
typealias Options = Map>>
+val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
+
fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
diff --git a/app/src/main/java/app/revanced/manager/util/signing/Signer.kt b/app/src/main/java/app/revanced/manager/util/signing/Signer.kt
deleted file mode 100644
index de875e91f2..0000000000
--- a/app/src/main/java/app/revanced/manager/util/signing/Signer.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package app.revanced.manager.util.signing
-
-import android.util.Log
-import app.revanced.manager.util.tag
-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.InputStream
-import java.math.BigInteger
-import java.nio.file.Path
-import java.security.*
-import java.security.cert.X509Certificate
-import java.util.*
-import kotlin.io.path.exists
-import kotlin.io.path.inputStream
-import kotlin.io.path.name
-import kotlin.io.path.outputStream
-
-class Signer(
- private val signingOptions: SigningOptions
-) {
- private val passwordCharArray = signingOptions.password.toCharArray()
- private fun newKeystore(out: Path) {
- val (publicKey, privateKey) = createKey()
- val privateKS = KeyStore.getInstance("BKS", "BC")
- privateKS.load(null, passwordCharArray)
- privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
- out.outputStream().use { stream -> privateKS.store(stream, passwordCharArray) }
- }
-
- fun regenerateKeystore() = newKeystore(signingOptions.keyStoreFilePath)
-
- private fun createKey(): Pair {
- val gen = KeyPairGenerator.getInstance("RSA")
- gen.initialize(4096)
- val pair = gen.generateKeyPair()
- var serialNumber: BigInteger
- do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
- val x500Name = X500Name("CN=${signingOptions.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
- }
-
- private fun loadKeystore(): KeyStore {
- val ks = signingOptions.keyStoreFilePath
- if (!ks.exists()) newKeystore(ks) else {
- Log.i(tag, "Found existing keystore: ${ks.name}")
- }
-
- Security.addProvider(BouncyCastleProvider())
- val keyStore = KeyStore.getInstance("BKS", "BC")
- ks.inputStream().use { keyStore.load(it, null) }
- return keyStore
- }
-
- fun canUnlock(): Boolean {
- val keyStore = loadKeystore()
- val alias = keyStore.aliases().nextElement()
-
- try {
- keyStore.getKey(alias, passwordCharArray)
- } catch (_: UnrecoverableKeyException) {
- return false
- }
-
- return true
- }
-
- fun signApk(input: File, output: File) {
- val keyStore = loadKeystore()
- val alias = keyStore.aliases().nextElement()
-
- val config = ApkSigner.SignerConfig.Builder(
- signingOptions.cn,
- keyStore.getKey(alias, passwordCharArray) as PrivateKey,
- listOf(keyStore.getCertificate(alias) as X509Certificate)
- ).build()
-
- val signer = ApkSigner.Builder(listOf(config))
- signer.setCreatedBy(signingOptions.cn)
- signer.setInputApk(input)
- signer.setOutputApk(output)
-
- signer.build().sign()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/util/signing/SigningOptions.kt b/app/src/main/java/app/revanced/manager/util/signing/SigningOptions.kt
deleted file mode 100644
index a0ca4d94f1..0000000000
--- a/app/src/main/java/app/revanced/manager/util/signing/SigningOptions.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package app.revanced.manager.util.signing
-
-import java.nio.file.Path
-
-data class SigningOptions(
- val cn: String,
- val password: String,
- val keyStoreFilePath: Path
-)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_logo_ring.xml b/app/src/main/res/drawable/ic_logo_ring.xml
new file mode 100644
index 0000000000..1640a24c9f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_ring.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2248759911..18b5bb7524 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,8 @@
Select an app
Select patches
+ Patching on ARMv7 devices is not yet supported and will most likely fail.
+
Import
Import patch bundle
Bundle patches
@@ -24,10 +26,12 @@
Missing
Error
+ Could not import legacy settings
+
Select updates to receive
Periodically connect to update providers to check for updates.
- Manager updates
- Patches
+ ReVanced Manager
+ ReVanced Patches
These settings can be changed later.
General
@@ -143,9 +147,14 @@
Unsupported app
Unsupported patches
Universal patches
+ Patch selection and options has been reset to recommended defaults
+ Stop using defaults?
+ You may encounter issues when not using the default patch selection and options.
+ Continue (%ds)
Supported
Universal
Unsupported
+ Patch name
Some of the patches do not support this app version (%1$s). The patches only support the following version(s): %2$s.
Continue with this version?
Not all patches support this version (%s). Do you want to continue anyway?
@@ -182,6 +191,9 @@
Downloadable versions
Already patched
+ Filter
+ Compatibility
+
Edit
More options
Value
@@ -223,6 +235,8 @@
collapse
More
+ Continue
+ Do not show this again
Donate
Website
GitHub
diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md
new file mode 100644
index 0000000000..a53b46fb9c
--- /dev/null
+++ b/docs/0_prerequisites.md
@@ -0,0 +1,16 @@
+# ๐ผ Prerequisites
+
+In order to use ReVanced Manager, certain requirements must be met.
+
+## ๐ค Requirements
+
+- An Android device running Android 8 or higher
+- Any device architecture except ARMv7[^1]
+
+[^1]: This constraint only applies to patches, that require patching APK resources which is why some patches may or may not work on ARMv7 architecture. You can find out, which architectures your device supports here: [โ๏ธ Configuring ReVanced Manager](2_4_settings.md#%E2%84%B9%EF%B8%8F-about).
+
+## โญ๏ธ What's next
+
+The next page will guide you through patching an app.
+
+Continue: [โฌ๏ธ Installation](1_installation.md)
diff --git a/docs/1_installation.md b/docs/1_installation.md
new file mode 100644
index 0000000000..d4c08984af
--- /dev/null
+++ b/docs/1_installation.md
@@ -0,0 +1,14 @@
+# โฌ๏ธ Installation
+
+In order to use ReVanced on your Android device, ReVanced Manager must be installed.
+
+## โ
Installation steps
+
+1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
+2. Install ReVanced Manager
+
+## โญ๏ธ What's next
+
+The next page will guide you through using ReVanced Manager.
+
+Continue: [๐ ๏ธ Usage](2_usage.md)
diff --git a/docs/2_1_patching.md b/docs/2_1_patching.md
new file mode 100644
index 0000000000..7203affa2a
--- /dev/null
+++ b/docs/2_1_patching.md
@@ -0,0 +1,25 @@
+# ๐งฉ Patching apps
+
+The following pages will guide you through using ReVanced Manager to patch apps.
+
+## โ
Steps to patch apps
+
+1. Navigate to the Apps tab from the top navigation bar
+2. Tap the + button in the bottom right corner
+3. Choose an app to patch[^1]
+4. Tap on the version of the app you want to patch[^2]
+5. Select the patches you want to apply
+6. Tap the Patch button
+7. Tap on the **Install** button
+ > **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
+ > Optionally, you may export the patched app to storage using the options in the top right corner.
+
+[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
+[^2]: It is suggested to use the version with the most patches to get the most out of ReVanced.
+[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 4. above.
+
+## โญ๏ธ What's next
+
+The next page will bring you back to the usage page.
+
+Continue: [๐ ๏ธ Usage](2_usage.md)
diff --git a/docs/2_2_managing.md b/docs/2_2_managing.md
new file mode 100644
index 0000000000..29ec56fc4f
--- /dev/null
+++ b/docs/2_2_managing.md
@@ -0,0 +1,15 @@
+# ๐งฐ Managing patched apps
+
+After patching an app, you may want to manage it. This page will guide you through managing patched apps.
+
+## โ
Steps to manage patched apps
+
+1. Navigate to the Apps tab from the top navigation bar
+2. Select the app you want to manage
+3.
+## โญ๏ธ What's next
+
+The next page will bring you back to the usage page.
+
+Continue: [๐ ๏ธ Usage](2_usage.md)
+
diff --git a/docs/2_3_updating.md b/docs/2_3_updating.md
new file mode 100644
index 0000000000..2b42104cf1
--- /dev/null
+++ b/docs/2_3_updating.md
@@ -0,0 +1,13 @@
+# ๐ Updating ReVanced Manager
+
+In order to keep up with the latest features and bug fixes, it is recommended to keep ReVanced Manager up to date.
+
+## โ
Updating steps
+
+> Currently not implemented
+
+## โญ๏ธ What's next
+
+The next page will bring you back to the usage page.
+
+Continue: [๐ ๏ธ Usage](2_usage.md)
diff --git a/docs/2_4_settings.md b/docs/2_4_settings.md
new file mode 100644
index 0000000000..008cda463b
--- /dev/null
+++ b/docs/2_4_settings.md
@@ -0,0 +1,39 @@
+# โ๏ธ Configuring ReVanced Manager
+
+ReVanced Manager has settings that can be configured to your liking.
+
+## โญ Essential settings
+
+- ### ๐ API URL
+
+ Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
+
+- ### ๐งฌ Sources
+
+ Override the API and change the source of ReVanced Patches.
+
+- ### ๐งช Experimental ReVanced Patches support
+
+ Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
+
+- ### ๐งโ๐ฌ Experimental universal support
+
+ This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
+
+- ### ๐ Export, import or delete keystore
+
+ Manage the keystore used to sign patched apps.
+
+- ### ๐ Export, import or reset ReVanced Patches selection
+
+ Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
+
+- ### โน๏ธ About
+
+ View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
+
+## โญ๏ธ What's next
+
+The next page will bring you back to the usage page.
+
+Continue: [๐ ๏ธ Usage](2_usage.md)
diff --git a/docs/2_usage.md b/docs/2_usage.md
new file mode 100644
index 0000000000..f079782f9c
--- /dev/null
+++ b/docs/2_usage.md
@@ -0,0 +1,16 @@
+# ๐ ๏ธ Usage
+
+The following pages will guide you through using ReVanced Manager to patch apps, manage patched apps, and update ReVanced Manager.
+
+## ๐ Table of contents
+
+1. [๐งฉ Patching apps](2_1_patching.md)
+2. [๐งฐ Managing patched apps](2_2_managing.md)
+3. [๐ Updating ReVanced Manager](2_3_updating.md)
+4. [โ๏ธ Configuring ReVanced Manager](2_4_settings.md)
+
+## โญ๏ธ What's next
+
+The next page will guide you through troubleshooting ReVanced Manager.
+
+Continue: [โ Troubleshooting](3_troubleshooting.md)
diff --git a/docs/3_troubleshooting.md b/docs/3_troubleshooting.md
new file mode 100644
index 0000000000..5a860c6b12
--- /dev/null
+++ b/docs/3_troubleshooting.md
@@ -0,0 +1,31 @@
+# โ Troubleshooting
+
+In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
+
+- ๐ Patching fails with an error
+
+ Make sure ReVanced Manager is up to date by following [๐ Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
+
+- ๐ฅ App not installed as package conflicts with an existing package
+
+ An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
+
+- โ๏ธ Error code `135`, `139` or `1` when patching the app
+
+ Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices.
+
+ Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
+
+- ๐ซ Non-root install is not possible with the current patches selection
+
+ Select the **Default** button when choosing patches.
+
+- ๐จ Patched app crashes on launch
+
+ Select the **Default** button when choosing patches.
+
+## โญ๏ธ What's next
+
+The next page will teach you how to build ReVanced Manager from source.
+
+Continue: [๐จ Building from source](4_building.md)
diff --git a/docs/4_building.md b/docs/4_building.md
new file mode 100644
index 0000000000..56917e5fad
--- /dev/null
+++ b/docs/4_building.md
@@ -0,0 +1,38 @@
+# ๐ ๏ธ Building from source
+
+This page will guide you through building ReVanced Manager from source.
+
+1. Download Java SDK 17 ([Azul JDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu) or [OpenJDK](https://jdk.java.net/java-se-ri/17)) and add it to path
+
+2. Clone the repository
+
+ ```sh
+ git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
+ ```
+
+3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
+
+4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
+
+ ```properties
+ gpr.user = YourUsername
+ gpr.key = ghp_longrandomkey
+ ```
+
+5. Set the `sdk.dir` property in `local.properties` to your Android SDK location
+
+ ```properties
+ sdk.dir = /path/to/android/sdk
+ ```
+
+6. Build the APK
+
+ Debug:
+ ```sh
+ ./gradlew assembleDebug
+ ```
+
+ Release:
+ ```sh
+ ./gradlew assembleRelease -Psign
+ ```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000000..af2926b6c1
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,21 @@
+# ๐ ReVanced Manager
+
+This documentation explains how to use [ReVanced Manager](https://github.com/revanced/revanced-manager).
+
+## ๐ Table of contents
+
+0. [๐ผ Prerequisites](0_prerequisites.md)
+1. [โฌ๏ธ Installation](1_installation.md)
+2. [๐ ๏ธ Usage](2_usage.md)
+ 1. [๐งฉ Patching apps](2_1_patching.md)
+ 2. [๐งฐ Managing patched apps](2_2_managing.md)
+ 3. [๐ Updating ReVanced Manager](2_3_updating.md)
+ 4. [โ๏ธ Configuring ReVanced Manager](2_4_settings.md)
+3. [โ Troubleshooting](3_troubleshooting.md)
+4. [๐จ Building from source](4_building.md)
+
+## โญ๏ธ Start here
+
+The next page will tell you about the prerequisites for using ReVanced Manager.
+
+Continue: [๐ผ Prerequisites](0_prerequisites.md)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4d4d984e6c..0c7b5e3b81 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,9 +11,8 @@ accompanist = "0.30.1"
serialization = "1.6.0"
collection = "0.3.5"
room-version = "2.5.2"
-patcher = "14.2.1"
-apksign = "8.1.1"
-bcpkix-jdk18on = "1.76"
+revanced-patcher = "16.0.1"
+revanced-library = "1.1.1"
koin-version = "3.4.3"
koin-version-compose = "3.4.6"
reimagined-navigation = "1.4.0"
@@ -68,11 +67,8 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-ver
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" }
# Patcher
-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "patcher" }
-
-# Signing
-apksign = { group = "com.android.tools.build", name = "apksig", version.ref = "apksign" }
-bcpkix-jdk18on = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
+revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
+revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7906ef55cf..fbde0be122 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,53 +1,59 @@
pluginManagement {
- // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
- val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
- File(".gradle/gradle.properties").inputStream().use {
- java.util.Properties().apply { load(it) }.let {
- it.getProperty("gpr.user") to it.getProperty("gpr.key")
+ repositories {
+ // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
+ val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
+ File(".gradle/gradle.properties").inputStream().use {
+ java.util.Properties().apply { load(it) }.let {
+ it.getProperty("gpr.user") to it.getProperty("gpr.key")
+ }
}
+ } else {
+ null to null
}
- } else {
- null to null
- }
- repositories {
- gradlePluginPortal()
- google()
- mavenCentral()
- maven("https://jitpack.io")
- maven {
- url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
+ fun RepositoryHandler.githubPackages(name: String) = maven {
+ url = uri(name)
credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
+
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ maven("https://jitpack.io")
+ githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
+ githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
}
}
dependencyResolutionManagement {
- // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
- val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
- File(".gradle/gradle.properties").inputStream().use {
- java.util.Properties().apply { load(it) }.let {
- it.getProperty("gpr.user") to it.getProperty("gpr.key")
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
+ val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
+ File(".gradle/gradle.properties").inputStream().use {
+ java.util.Properties().apply { load(it) }.let {
+ it.getProperty("gpr.user") to it.getProperty("gpr.key")
+ }
}
+ } else {
+ null to null
}
- } else {
- null to null
- }
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- maven("https://jitpack.io")
- maven {
- url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
+ fun RepositoryHandler.githubPackages(name: String) = maven {
+ url = uri(name)
credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
+
+ google()
+ mavenCentral()
+ maven("https://jitpack.io")
+ githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
+ githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
}
}
rootProject.name = "ReVanced Manager"