Compare commits

...

9 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
20 changed files with 1134 additions and 111 deletions

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

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

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

@@ -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>