Compare commits

...

13 Commits

Author SHA1 Message Date
semantic-release-bot
af564eab1b chore: Release v1.26.0-dev.28 [skip ci]
# app [1.26.0-dev.28](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.27...v1.26.0-dev.28) (2026-02-28)

### Features

* Add patch selection filters ([#2956](https://github.com/ReVanced/revanced-manager/issues/2956)) ([0be493d](0be493da66))
2026-02-28 04:04:06 +00:00
AunAli K.
0be493da66 feat: Add patch selection filters (#2956)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 06:55:03 +03:00
semantic-release-bot
8f23114bc3 chore: Release v1.26.0-dev.27 [skip ci]
# app [1.26.0-dev.27](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.26...v1.26.0-dev.27) (2026-02-28)

### Features

* Show patch bundle version used to patch an app ([#2966](https://github.com/ReVanced/revanced-manager/issues/2966)) ([11bd46c](11bd46ce26))
2026-02-28 02:09:32 +00:00
AunAli K.
11bd46ce26 feat: Show patch bundle version used to patch an app (#2966)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 05:01:23 +03:00
semantic-release-bot
e03f3666c8 chore: Release v1.26.0-dev.26 [skip ci]
# app [1.26.0-dev.26](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.25...v1.26.0-dev.26) (2026-02-27)

### Features

* improve InstalledAppsScreen caching/perf ([#2963](https://github.com/ReVanced/revanced-manager/issues/2963)) ([323d217](323d2170ef))
2026-02-27 23:09:46 +00:00
rushii
323d2170ef feat: improve InstalledAppsScreen caching/perf (#2963)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 02:00:38 +03:00
semantic-release-bot
940669b3e4 chore: Release v1.26.0-dev.25 [skip ci]
# app [1.26.0-dev.25](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.24...v1.26.0-dev.25) (2026-02-27)

### Features

* improve AppSelectorScreen caching/perf ([#2961](https://github.com/ReVanced/revanced-manager/issues/2961)) ([81e5a93](81e5a9315d))
2026-02-27 22:08:17 +00:00
rushii
81e5a9315d feat: improve AppSelectorScreen caching/perf (#2961)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 00:59:47 +03:00
semantic-release-bot
b38d0b7022 chore: Release v1.26.0-dev.24 [skip ci]
# app [1.26.0-dev.24](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.23...v1.26.0-dev.24) (2026-02-27)

### Bug Fixes

* Incorrect patcher string value ([f19080a](f19080a986))
* Never show again toggle doing nothing for Update dialog ([6f4219c](6f4219c01b))
* Release builds not working ([00b3c59](00b3c59572))
* Remove quotes from strings ([fc30ba5](fc30ba5e83))
* we actually don't want to exclude reflection symbols ([3222840](32228404c4))

### Features

* Add announcements ([#2948](https://github.com/ReVanced/revanced-manager/issues/2948)) ([813df46](813df46847))
* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](8667051283))
2026-02-27 15:59:57 +00:00
semantic-release-bot
b838851ebe chore: Release API v1.0.0-dev.2 [skip ci]
# api [1.0.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.1...api@1.0.0-dev.2) (2026-02-27)

### Features

* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](8667051283))
2026-02-27 15:59:00 +00:00
Ax333l
00b3c59572 fix: Release builds not working 2026-02-27 16:50:49 +01:00
Ushie
fc30ba5e83 fix: Remove quotes from strings 2026-02-27 08:52:59 +03:00
Plance Shimney
32228404c4 fix: we actually don't want to exclude reflection symbols 2026-02-25 20:36:58 +03:00
25 changed files with 1171 additions and 141 deletions

View File

@@ -1,3 +1,10 @@
# api [1.0.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.1...api@1.0.0-dev.2) (2026-02-27)
### Features
* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](https://github.com/ReVanced/revanced-manager/commit/8667051283f934a32ac7b7cc76178397dc45a0e6))
# api 1.0.0-dev.1 (2026-02-19)

View File

@@ -1 +1 @@
version = 1.0.0-dev.1
version = 1.0.0-dev.2

View File

@@ -1,3 +1,48 @@
# app [1.26.0-dev.28](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.27...v1.26.0-dev.28) (2026-02-28)
### Features
* Add patch selection filters ([#2956](https://github.com/ReVanced/revanced-manager/issues/2956)) ([0be493d](https://github.com/ReVanced/revanced-manager/commit/0be493da662ed2b46d4b8616942357eb6a111d17))
# app [1.26.0-dev.27](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.26...v1.26.0-dev.27) (2026-02-28)
### Features
* Show patch bundle version used to patch an app ([#2966](https://github.com/ReVanced/revanced-manager/issues/2966)) ([11bd46c](https://github.com/ReVanced/revanced-manager/commit/11bd46ce26d4fbc7f126bee5e254d4f6b5e0cc43))
# app [1.26.0-dev.26](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.25...v1.26.0-dev.26) (2026-02-27)
### Features
* improve InstalledAppsScreen caching/perf ([#2963](https://github.com/ReVanced/revanced-manager/issues/2963)) ([323d217](https://github.com/ReVanced/revanced-manager/commit/323d2170efa0e3f2c8d2245ff08a2e92c52cb9d7))
# app [1.26.0-dev.25](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.24...v1.26.0-dev.25) (2026-02-27)
### Features
* improve AppSelectorScreen caching/perf ([#2961](https://github.com/ReVanced/revanced-manager/issues/2961)) ([81e5a93](https://github.com/ReVanced/revanced-manager/commit/81e5a9315db4c858fa29163d15457af97163967a))
# app [1.26.0-dev.24](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.23...v1.26.0-dev.24) (2026-02-27)
### Bug Fixes
* Incorrect patcher string value ([f19080a](https://github.com/ReVanced/revanced-manager/commit/f19080a9862be4600860b67cd981b9a3a56b21e7))
* Never show again toggle doing nothing for Update dialog ([6f4219c](https://github.com/ReVanced/revanced-manager/commit/6f4219c01ba43b70e130f210c8d48978a6056f68))
* Release builds not working ([00b3c59](https://github.com/ReVanced/revanced-manager/commit/00b3c59572760993f69f2663456237faafff0283))
* Remove quotes from strings ([fc30ba5](https://github.com/ReVanced/revanced-manager/commit/fc30ba5e83f27329b05dc81f484bb8f7471b6a99))
* we actually don't want to exclude reflection symbols ([3222840](https://github.com/ReVanced/revanced-manager/commit/32228404c4ef38bb8931d5cfdf6a7aefde23a89c))
### Features
* Add announcements ([#2948](https://github.com/ReVanced/revanced-manager/issues/2948)) ([813df46](https://github.com/ReVanced/revanced-manager/commit/813df46847620bd854dccc52ff244388b3385e17))
* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](https://github.com/ReVanced/revanced-manager/commit/8667051283f934a32ac7b7cc76178397dc45a0e6))
# app [1.26.0-dev.23](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.22...v1.26.0-dev.23) (2026-02-19)

View File

@@ -257,9 +257,6 @@ android {
// Desktop AAPT binaries
excludes += "/prebuilt/**"
// Reflection symbol list (https://stackoverflow.com/a/41073782/13964629)
excludes += "/**/*.kotlin_builtins"
}
jniLibs {
// 32-bit x86 is dead

View File

@@ -1 +1 @@
version = 1.26.0-dev.23
version = 1.26.0-dev.28

View File

@@ -11,4 +11,7 @@
-dontwarn com.google.j2objc.annotations.*
-dontwarn java.awt.**
-dontwarn javax.**
-dontwarn org.slf4j.**
-dontwarn org.slf4j.**
-dontwarn org.immutables.value.Generated
-dontwarn reactor.blockhound.integration.BlockHoundIntegration

View File

@@ -0,0 +1,466 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "d9350f0e779a1bde46227a29884a7865",
"entities": [
{
"tableName": "patch_bundles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionHash",
"columnName": "version",
"affinity": "TEXT"
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "autoUpdate",
"columnName": "auto_update",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
}
},
{
"tableName": "patch_selections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchBundle",
"columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_patch_selections_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"patch_bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "selected_patches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "selection",
"columnName": "selection",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"selection",
"patch_name"
]
},
"foreignKeys": [
{
"table": "patch_selections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"selection"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "downloaded_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "directory",
"columnName": "directory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUsed",
"columnName": "last_used",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"version"
]
}
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
}
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "installed_patch_bundle",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle_uid` INTEGER NOT NULL, `bundle_name` TEXT NOT NULL, `bundle_version` TEXT, PRIMARY KEY(`package_name`, `bundle_uid`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundleUid",
"columnName": "bundle_uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bundleName",
"columnName": "bundle_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundleVersion",
"columnName": "bundle_version",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle_uid"
]
},
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
}
]
},
{
"tableName": "option_groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchBundle",
"columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_option_groups_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"patch_bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "options",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "group",
"columnName": "group",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group",
"patch_name",
"key"
]
},
"foreignKeys": [
{
"table": "option_groups",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"group"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "trusted_downloaders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name"
]
}
}
],
"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, 'd9350f0e779a1bde46227a29884a7865')"
]
}
}

View File

@@ -1,13 +1,15 @@
package app.revanced.manager.data.room
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
@@ -21,8 +23,9 @@ import app.revanced.manager.data.room.downloader.TrustedDownloaderDao
import kotlin.random.Random
@Database(
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloader::class],
version = 1
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, InstalledPatchBundle::class, OptionGroup::class, Option::class, TrustedDownloader::class],
version = 2,
autoMigrations = [AutoMigration(from = 1, to = 2)]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -25,11 +25,20 @@ interface InstalledAppDao {
"patch_name"
) String>>
@Query("SELECT * FROM installed_patch_bundle WHERE package_name = :packageName")
suspend fun getInstalledPatchBundles(packageName: String): List<InstalledPatchBundle>
@Transaction
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
suspend fun upsertApp(
installedApp: InstalledApp,
appliedPatches: List<AppliedPatch>,
patchBundles: List<InstalledPatchBundle>
) {
upsertApp(installedApp)
deleteAppliedPatches(installedApp.currentPackageName)
deleteInstalledPatchBundles(installedApp.currentPackageName)
insertAppliedPatches(appliedPatches)
insertInstalledPatchBundles(patchBundles)
}
@Upsert
@@ -38,9 +47,15 @@ interface InstalledAppDao {
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Insert
suspend fun insertInstalledPatchBundles(patchBundles: List<InstalledPatchBundle>)
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
suspend fun deleteAppliedPatches(packageName: String)
@Query("DELETE FROM installed_patch_bundle WHERE package_name = :packageName")
suspend fun deleteInstalledPatchBundles(packageName: String)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@@ -0,0 +1,24 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "installed_patch_bundle",
primaryKeys = ["package_name", "bundle_uid"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
)
]
)
data class InstalledPatchBundle(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle_uid") val bundleUid: Int,
@ColumnInfo(name = "bundle_name") val bundleName: String,
@ColumnInfo(name = "bundle_version") val bundleVersion: String?
)

View File

@@ -4,6 +4,8 @@ import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -19,12 +21,16 @@ class InstalledAppRepository(
suspend fun getAppliedPatches(packageName: String): PatchSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun getInstalledPatchBundles(packageName: String) =
dao.getInstalledPatchBundles(packageName)
suspend fun addOrUpdate(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchSelection: PatchSelection
patchSelection: PatchSelection,
bundleInfo: Map<Int, PatchBundleInfo.Global> = emptyMap()
) {
dao.upsertApp(
InstalledApp(
@@ -41,6 +47,15 @@ class InstalledAppRepository(
patchName = patch
)
}
},
patchSelection.keys.mapNotNull { uid ->
val info = bundleInfo[uid] ?: return@mapNotNull null
InstalledPatchBundle(
packageName = currentPackageName,
bundleUid = uid,
bundleName = info.name,
bundleVersion = info.version
)
}
)
}

View File

@@ -0,0 +1,31 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.CheckboxColors
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.state.ToggleableState
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticTriStateCheckbox(
state: ToggleableState,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: CheckboxColors = CheckboxDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
TriStateCheckbox(
state = state,
onClick = onClick?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK),
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource
)
}

View File

@@ -24,9 +24,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -68,20 +68,11 @@ fun AppSelectorScreen(
uri?.let(vm::handleStorageResult)
}
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle(emptyMap())
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle()
var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) }
val appList by vm.appList.collectAsStateWithLifecycle(initialValue = emptyList())
val filteredAppList = remember(appList, filterText) {
appList.filter { app ->
(vm.loadLabel(app.packageInfo)).contains(
filterText,
true
) or app.packageName.contains(filterText, true)
}
}
val appList by vm.apps.collectAsStateWithLifecycle()
val appsListFiltered by vm.filteredApps.collectAsStateWithLifecycle()
vm.nonSuggestedVersionDialogSubject?.let {
NonSuggestedVersionDialog(
@@ -90,17 +81,20 @@ fun AppSelectorScreen(
)
}
if (search)
if (search) {
val filterText by vm.filterText.collectAsState()
SearchView(
query = filterText,
onQueryChange = { filterText = it },
onQueryChange = vm::setFilterText,
onActiveChange = { search = it },
placeholder = { Text(stringResource(R.string.search_apps)) }
) {
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
val appsFiltered = appsListFiltered
if (!appsFiltered.isNullOrEmpty() && filterText.isNotEmpty()) {
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
items(
items = filteredAppList,
items = appsFiltered,
key = { it.packageName }
) { app ->
ListItem(
@@ -109,9 +103,9 @@ fun AppSelectorScreen(
},
leadingContent = {
AppIcon(
app.packageInfo,
null,
Modifier.size(36.dp)
packageInfo = app.packageInfo,
contentDescription = null,
modifier = Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(app.packageInfo) },
@@ -152,6 +146,7 @@ fun AppSelectorScreen(
}
}
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -198,16 +193,33 @@ fun AppSelectorScreen(
HorizontalDivider()
}
if (appList.isNotEmpty()) {
val apps = appList
if (apps == null) {
item(key = "LOADING") {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
}
}
} else if (apps.isNotEmpty()) {
items(
items = appList,
key = { it.packageName }
items = apps,
key = { "APP-" + it.packageName },
contentType = { "APP" },
) { app ->
ListItem(
modifier = Modifier.clickable {
onSelect(app.packageName)
},
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
leadingContent = {
AppIcon(
packageInfo = app.packageInfo,
contentDescription = null,
modifier = Modifier.size(36.dp)
)
},
headlineContent = {
AppLabel(
app.packageInfo,
@@ -231,15 +243,6 @@ fun AppSelectorScreen(
}
}
)
}
} else {
item {
Box(
modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center
) {
LoadingIndicator()
}
}
}
}

View File

@@ -0,0 +1,74 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.transparentListItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppliedPatchesDialog(
onDismissRequest: () -> Unit,
appliedPatches: PatchSelection?,
patchBundles: List<InstalledPatchBundle>
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
FullscreenDialog(onDismissRequest = onDismissRequest) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.applied_patches),
scrollBehavior = scrollBehavior,
onBackClick = onDismissRequest
)
},
) { paddingValues ->
LazyColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
val patches = appliedPatches ?: return@LazyColumnWithScrollbar
val bundleMap = patchBundles.associateBy { it.bundleUid }
patches.forEach { (bundleUid, patchNames) ->
val bundle = bundleMap[bundleUid]
item(key = "header_$bundleUid") {
ListHeader(title = bundle?.let {
it.bundleVersion?.let { version ->
"${it.bundleName} v$version"
} ?: it.bundleName
} ?: "${stringResource(R.string.patches)} $bundleUid")
}
items(
items = patchNames.sorted(),
key = { "${bundleUid}_$it" }
) { patchName ->
ListItem(
headlineContent = { Text(patchName) },
colors = transparentListItemColors
)
}
}
}
}
}
}

View File

@@ -32,7 +32,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -44,7 +43,6 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
import app.revanced.manager.util.toast
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -53,19 +51,26 @@ fun InstalledAppInfoScreen(
onBackClick: () -> Unit,
viewModel: InstalledAppInfoViewModel
) {
val context = LocalContext.current
SideEffect {
viewModel.onBackClick = onBackClick
}
var showUninstallDialog by rememberSaveable { mutableStateOf(false) }
var showAppliedPatchesDialog by rememberSaveable { mutableStateOf(false) }
if (showUninstallDialog)
UninstallDialog(
onDismiss = { showUninstallDialog = false },
onConfirm = { viewModel.uninstall() }
)
if (showAppliedPatchesDialog) {
AppliedPatchesDialog(
onDismissRequest = { showAppliedPatchesDialog = false },
appliedPatches = viewModel.appliedPatches,
patchBundles = viewModel.patchBundles
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -152,7 +157,7 @@ fun InstalledAppInfoScreen(
modifier = Modifier.padding(vertical = 16.dp)
) {
SettingsListItem(
modifier = Modifier.clickable { context.toast("Not implemented yet!") },
modifier = Modifier.clickable(onClick = { showAppliedPatchesDialog = true }),
headlineContent = stringResource(R.string.applied_patches),
supportingContent =
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {

View File

@@ -10,6 +10,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -30,7 +31,7 @@ fun InstalledAppsScreen(
onAppClick: (InstalledApp) -> Unit,
viewModel: InstalledAppsViewModel = koinViewModel()
) {
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
val installedApps by viewModel.apps.collectAsStateWithLifecycle()
Column {
LazyColumnWithScrollbar(
@@ -38,38 +39,40 @@ fun InstalledAppsScreen(
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) }
)
}
}
} else {
item {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
val apps = installedApps
if (apps == null) {
item(key = "LOADING") {
LoadingIndicator()
}
} else if (apps.isNotEmpty()) {
items(
apps,
key = { "APP-" + it.currentPackageName },
contentType = { "APP" },
) { installedApp ->
val packageInfo = viewModel.packageInfoMap[installedApp.currentPackageName]
} ?: item { LoadingIndicator() }
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(key = "NONE") {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
}
}
}
}

View File

@@ -15,9 +15,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@@ -27,13 +30,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Deselect
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
@@ -60,7 +66,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.patcher.patch.Option
@@ -74,6 +82,7 @@ import app.revanced.manager.ui.component.SearchBar
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.haptics.HapticTriStateCheckbox
import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
@@ -124,7 +133,39 @@ fun PatchesSelectorScreen(
val patchLazyListStates = remember(bundles) { List(bundles.size) { LazyListState() } }
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
var pendingScopeAction by remember { mutableStateOf<((Int?) -> Unit)?>(null) }
fun executeScopedAction(action: (Int?) -> Unit) {
if (bundles.size > 1) {
pendingScopeAction = action
} else {
action(bundles.firstOrNull()?.uid)
}
}
pendingScopeAction?.let { action ->
val currentBundle = bundles.getOrNull(pagerState.currentPage) ?: return@let
ScopeDialog(
bundleName = currentBundle.name,
onDismissRequest = { pendingScopeAction = null },
onAllPatches = {
action(null)
pendingScopeAction = null
},
onBundleOnly = {
action(currentBundle.uid)
pendingScopeAction = null
}
)
}
if (showBottomSheet) {
val currentBundle = bundles.getOrNull(pagerState.currentPage)
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
@@ -161,6 +202,71 @@ fun PatchesSelectorScreen(
label = { Text(stringResource(R.string.universal)) },
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text(
text = stringResource(R.string.patch_selector_sheet_actions_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 8.dp)
)
fun guardedAction(action: () -> Unit) {
showBottomSheet = false
if (viewModel.selectionWarningEnabled) {
showSelectionWarning = true
} else {
action()
}
}
ActionItem(
icon = Icons.Outlined.Restore,
text = stringResource(R.string.restore_default_selection),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.restoreDefaults(uid)
}
}
}
)
ActionItem(
icon = Icons.Outlined.Deselect,
text = stringResource(R.string.deselect_all),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.deselectAll(bundles, uid)
}
}
}
)
ActionItem(
icon = Icons.Outlined.SwapHoriz,
text = stringResource(R.string.invert_selection),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.invertSelection(bundles, uid)
}
}
}
)
if (bundles.size > 1 && currentBundle != null) {
ActionItem(
icon = Icons.Outlined.Deselect,
text = stringResource(R.string.deselect_all_except, currentBundle.name),
onClick = {
guardedAction {
viewModel.deselectAllExcept(bundles, currentBundle.uid)
}
}
)
}
}
}
}
@@ -191,9 +297,6 @@ fun PatchesSelectorScreen(
)
}
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
if (showSelectionWarning)
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
@@ -426,16 +529,38 @@ fun PatchesSelectorScreen(
}
},
text = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = bundle.version.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
Row(verticalAlignment = Alignment.CenterVertically) {
val selectionState = viewModel.getBundleSelectionState(bundle)
val toggleableState = when (selectionState) {
true -> ToggleableState.On
false -> ToggleableState.Off
null -> ToggleableState.Indeterminate
}
HapticTriStateCheckbox(
state = toggleableState,
onClick = {
when {
viewModel.selectionWarningEnabled -> showSelectionWarning = true
selectionState == false -> viewModel.restoreDefaults(bundle.uid)
else -> viewModel.deselectAll(bundles, bundle.uid)
}
}
)
Spacer(modifier = Modifier.width(4.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = bundle.version.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
},
selectedContentColor = MaterialTheme.colorScheme.primary,
@@ -612,6 +737,41 @@ private fun IncompatiblePatchDialog(
}
)
@Composable
private fun ActionItem(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
leadingContent = { Icon(icon, contentDescription = null) },
headlineContent = { Text(text) },
colors = transparentListItemColors
)
}
@Composable
private fun ScopeDialog(
bundleName: String,
onDismissRequest: () -> Unit,
onAllPatches: () -> Unit,
onBundleOnly: () -> Unit
) = AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(R.string.scope_dialog_title)) },
confirmButton = {
TextButton(onClick = onAllPatches) {
Text(stringResource(R.string.scope_all_patches))
}
},
dismissButton = {
TextButton(onClick = onBundleOnly) {
Text(stringResource(R.string.scope_bundle_patches, bundleName))
}
}
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun OptionsDialog(

View File

@@ -19,8 +19,12 @@ import app.revanced.manager.util.PM
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@@ -40,16 +44,51 @@ class AppSelectorViewModel(
"input.apk"
).also(File::delete)
}
val appList = pm.appList
val filterTextFlow = MutableStateFlow("")
val filterText: StateFlow<String> = filterTextFlow
val apps = pm.appList.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
val filteredApps = filterText
.combine(apps) { filter, apps ->
if (apps == null || filter.isBlank()) {
apps
} else {
apps.filter { app ->
app.packageName.contains(filter, true) ||
loadLabel(app.packageInfo).contains(filter)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
stopTimeoutMillis = 0,
replayExpirationMillis = 10_000,
),
initialValue = null,
)
private val storageSelectionChannel = Channel<SelectedApp.Local>()
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
val suggestedAppVersions = patchBundleRepository.suggestedVersions.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyMap(),
)
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
private set
fun setFilterText(filter: String) {
filterTextFlow.value = filter
}
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
fun dismissNonSuggestedVersionDialog() {

View File

@@ -0,0 +1,37 @@
package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AppliedPatchesViewModel(
packageName: String
) : ViewModel(), KoinComponent {
private val installedAppRepository: InstalledAppRepository by inject()
var appliedPatches: PatchSelection? by mutableStateOf(null)
private set
var patchBundles: List<InstalledPatchBundle> by mutableStateOf(emptyList())
private set
init {
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(packageName)
}
patchBundles = withContext(Dispatchers.IO) {
installedAppRepository.getInstalledPatchBundles(packageName)
}
}
}
}

View File

@@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
@@ -41,6 +42,8 @@ class InstalledAppInfoViewModel(
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchSelection? by mutableStateOf(null)
var patchBundles: List<InstalledPatchBundle> by mutableStateOf(emptyList())
private set
var isMounted by mutableStateOf(false)
private set
@@ -54,6 +57,9 @@ class InstalledAppInfoViewModel(
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(it.currentPackageName)
}
patchBundles = withContext(Dispatchers.IO) {
installedAppRepository.getInstalledPatchBundles(it.currentPackageName)
}
}
}
}

View File

@@ -5,13 +5,16 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.installer.RootServiceException
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.installer.RootServiceException
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
import app.revanced.manager.util.collectEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -20,32 +23,42 @@ class InstalledAppsViewModel(
private val pm: PM,
private val rootInstaller: RootInstaller
) : ViewModel() {
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
val apps = installedAppsRepository.getAll()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
val packageInfoMap = mutableStateMapOf<String, PackageInfo?>()
init {
viewModelScope.launch {
apps.collectEach { installedApp ->
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
try {
if (
installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
) {
installedAppsRepository.delete(installedApp)
return@withContext null
}
} catch (_: RootServiceException) { }
apps.filterNotNull().collectLatest(::fetchPackageInfos)
}
}
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
if (packageInfo == null && installedApp.installType != InstallType.MOUNT) {
installedAppsRepository.delete(installedApp)
private suspend fun fetchPackageInfos(apps: List<InstalledApp>) {
for (app in apps) {
packageInfoMap[app.currentPackageName] = withContext(Dispatchers.IO) {
try {
if (app.installType == InstallType.MOUNT &&
!rootInstaller.isAppInstalled(app.currentPackageName)
) {
installedAppsRepository.delete(app)
return@withContext null
}
packageInfo
} catch (_: RootServiceException) {
}
val packageInfo = pm.getPackageInfo(app.currentPackageName)
if (packageInfo == null && app.installType != InstallType.MOUNT) {
installedAppsRepository.delete(app)
return@withContext null
}
packageInfo
}
}
}

View File

@@ -28,6 +28,7 @@ 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
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
@@ -59,6 +60,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
@@ -88,6 +90,7 @@ class PatcherViewModel(
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get()
private val ackpineInstaller: PackageInstaller = get()
@@ -378,13 +381,15 @@ class PatcherViewModel(
Session.State.Succeeded -> {
app.toast(app.getString(R.string.install_app_success))
installedPackageName = installerPkgName
val bundleInfo = patchBundleRepository.bundleInfoFlow.first()
installedAppRepository.addOrUpdate(
installerPkgName,
packageName,
input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT,
input.selectedPatches
input.selectedPatches,
bundleInfo
)
}
}
@@ -457,12 +462,14 @@ class PatcherViewModel(
label
)
val bundleInfo = patchBundleRepository.bundleInfoFlow.first()
installedAppRepository.addOrUpdate(
currentPackageInfo.packageName,
packageName,
inputVersion,
InstallType.MOUNT,
input.selectedPatches
input.selectedPatches,
bundleInfo
)
rootInstaller.mount(packageName)

View File

@@ -209,6 +209,74 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
filter = filter xor flag
}
fun getBundleSelectionState(bundle: PatchBundleInfo.Scoped): Boolean? {
val patches = bundle.patchSequence(allowIncompatiblePatches).toList()
if (patches.isEmpty()) return false
val selectedCount = patches.count { isSelected(bundle.uid, it) }
return when (selectedCount) {
patches.size -> true
0 -> false
else -> null
}
}
private suspend fun currentSelection(): PersistentPatchSelection =
customPatchSelection ?: defaultPatchSelection.first()
private suspend fun updateSelection(
update: (PersistentPatchSelection) -> PersistentPatchSelection
) {
hasModifiedSelection = true
customPatchSelection = update(currentSelection())
}
fun deselectAll(bundles: List<PatchBundleInfo.Scoped>, bundleUid: Int?) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundleUid != null && bundle.uid != bundleUid) return@fold acc
acc.put(bundle.uid, persistentSetOf())
}
}
}
fun invertSelection(bundles: List<PatchBundleInfo.Scoped>, bundleUid: Int?) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundleUid != null && bundle.uid != bundleUid) return@fold acc
val currentSelected = acc[bundle.uid] ?: persistentSetOf()
val inverted = bundle.patchSequence(allowIncompatiblePatches)
.filter { it.name !in currentSelected }
.map { it.name }
.toPersistentSet()
acc.put(bundle.uid, inverted)
}
}
}
fun restoreDefaults(bundleUid: Int?) = viewModelScope.launch {
if (bundleUid == null) {
customPatchSelection = null
hasModifiedSelection = false
return@launch
}
val defaults = defaultPatchSelection.first()
updateSelection { selection ->
selection.put(bundleUid, defaults[bundleUid] ?: persistentSetOf())
}
}
fun deselectAllExcept(bundles: List<PatchBundleInfo.Scoped>, keepBundleUid: Int) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundle.uid == keepBundleUid) return@fold acc
acc.put(bundle.uid, persistentSetOf())
}
}
}
companion object {
const val SHOW_INCOMPATIBLE = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1

View File

@@ -113,24 +113,24 @@ Second \"item\" text"</string>
<string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string>
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
<string name="patch_compat_check_confirmation">"Selecting incompatible patches can result in a broken app.
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.
Do you want to proceed anyways?"</string>
Do you want to proceed anyways?</string>
<string name="suggested_version_safeguard">Require suggested app version</string>
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
<string name="suggested_version_safeguard_confirmation">"Selecting an app that is not the suggested version may cause unexpected issues.
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.
Do you want to proceed anyways?"</string>
Do you want to proceed anyways?</string>
<string name="patch_selection_safeguard">Allow changing patch selection and options</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches and customization of options</string>
<string name="patch_selection_safeguard_confirmation">"Changing the selection of patches may cause unexpected issues.
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.
Enable anyways?"</string>
Enable anyways?</string>
<string name="universal_patches_safeguard">Allow using universal patches</string>
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
<string name="universal_patches_safeguard_confirmation">"Universal patches are not as well tested as those that target specific apps.
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.
Enable anyways?"</string>
Enable anyways?</string>
<string name="import_keystore">Import keystore</string>
<string name="import_keystore_description">Import a custom keystore</string>
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
@@ -146,9 +146,9 @@ Enable anyways?"</string>
<string name="export_keystore_success">Exported keystore</string>
<string name="regenerate_keystore">Regenerate keystore</string>
<string name="regenerate_keystore_description">Generate a new keystore</string>
<string name="regenerate_keystore_dialog_description">"You are about to regenerate your keystore the manager will use during the patching process.
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.
You will not be able to update the previously installed apps from this source."</string>
You will not be able to update the previously installed apps from this source.</string>
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
<string name="import_patch_selection">Import patch selection</string>
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
@@ -273,23 +273,23 @@ You will not be able to update the previously installed apps from this source."<
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string>
<string name="non_suggested_version_warning_title">Non suggested version</string>
<string name="non_suggested_version_warning_description">"The version of the app you have selected does not match the suggested version.
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.
Please use the suggested version: %s
To continue anyway, disable \"Require suggested app version\" in the advanced settings."</string>
To continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
<string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">"It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches."</string>
<string name="universal_patch_warning_description">"Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches.</string>
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches."</string>
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches.</string>
<string name="this_version">This version</string>
<string name="universal">Any app</string>
<string name="search_patches">Search patches</string>
<string name="app_version_not_compatible">"This patch is not compatible with the selected app version (%1$s)
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s)
It is only compatible with the following version(s): %2$s"</string>
It is only compatible with the following version(s): %2$s</string>
<string name="continue_with_version">Continue with this version?</string>
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
<string name="download_application">Download application?</string>
@@ -329,6 +329,15 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="patch_selector_sheet_filter_title">Filter</string>
<string name="patch_selector_sheet_filter_compat_title">Compatibility</string>
<string name="patch_selector_sheet_actions_title">Actions</string>
<string name="restore_default_selection">Restore default selection</string>
<string name="deselect_all">Deselect all</string>
<string name="invert_selection">Invert selection</string>
<string name="deselect_all_except">Deselect all except %s</string>
<string name="scope_dialog_title">Apply to</string>
<string name="scope_all_patches">All patches</string>
<string name="scope_bundle_patches">%s only</string>
<string name="string_option_menu_description">More options</string>
<string name="option_preset_custom_value">Custom value</string>
@@ -434,8 +443,8 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="save_with_count">Save (%1$s)</string>
<string name="update">Update</string>
<string name="empty">Empty</string>
<string name="installing_message">"Tap on <b>Update</b> when prompted.
ReVanced Manager will close when updating."</string>
<string name="installing_message">Tap on <b>Update</b> when prompted.
ReVanced Manager will close when updating.</string>
<string name="no_changelogs_found">No changelogs found</string>
<string name="just_now">Just now</string>
<string name="minutes_ago">%sm ago</string>
@@ -455,9 +464,9 @@ ReVanced Manager will close when updating."</string>
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
<string name="failed_to_download_update">Failed to download update: %s</string>
<string name="download">Download</string>
<string name="download_confirmation_metered">"You are currently on a metered connection, and data charges from your service provider may apply.
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.
Do you still want to continue?"</string>
Do you still want to continue?</string>
<string name="download_update_confirmation">Download update?</string>
<string name="no_contributors_found">No contributors found</string>
<string name="select">Select</string>
@@ -495,9 +504,9 @@ Do you still want to continue?"</string>
<string name="patches_prereleases">Use pre-releases</string>
<string name="patches_prereleases_description">Use pre-release versions of %s</string>
<string name="patches_url">Patches URL</string>
<string name="incompatible_patches_dialog">"These patches are not compatible with the selected app version (%1$s).
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).
Click on the patches to see more details."</string>
Click on the patches to see more details.</string>
<string name="incompatible_patch">Incompatible patch</string>
<string name="any_version">Any</string>
<string name="never_show_again">Never show again</string>

View File

@@ -23,7 +23,7 @@ koin = "4.1.1"
ktor = "3.3.3"
markdown-renderer = "0.39.0"
fading-edges = "1.0.4"
kotlin = "2.3.0"
kotlin = "2.3.10"
android-gradle-plugin = "8.13.2"
dev-tools-gradle-plugin = "2.3.4"
about-libraries = "13.2.1"