From f6f72387b92f73b5e3b36f3096928fa83e7614a1 Mon Sep 17 00:00:00 2001
From: alieRN <45766489+aliernfrog@users.noreply.github.com>
Date: Thu, 19 Sep 2024 22:17:38 +0300
Subject: [PATCH 1/9] feat(patcher): Improve installation (#2185)
---
.../manager/data/room/apps/installed/InstalledApp.kt | 2 +-
.../revanced/manager/domain/installer/RootInstaller.kt | 6 +++++-
.../revanced/manager/patcher/worker/PatcherWorker.kt | 2 +-
.../ui/component/patcher/InstallPickerDialog.kt | 3 ++-
.../manager/ui/screen/InstalledAppInfoScreen.kt | 6 +++---
.../app/revanced/manager/ui/screen/PatcherScreen.kt | 4 +++-
.../manager/ui/screen/VersionSelectorScreen.kt | 4 ++--
.../manager/ui/viewmodel/InstalledAppInfoViewModel.kt | 2 +-
.../manager/ui/viewmodel/InstalledAppsViewModel.kt | 4 ++--
.../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 10 ++++++----
app/src/main/res/values/strings.xml | 2 +-
11 files changed, 27 insertions(+), 18 deletions(-)
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt
index ad7033dd..290a226d 100644
--- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt
@@ -9,7 +9,7 @@ import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
- ROOT(R.string.root_install)
+ MOUNT(R.string.mount_install)
}
@Parcelize
diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
index 9ca6cd9b..885f8ad1 100644
--- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
+++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
@@ -43,7 +43,7 @@ class RootInstaller(
}
}
- return withTimeoutOrNull(Duration.ofSeconds(120L)) {
+ return withTimeoutOrNull(Duration.ofSeconds(20L)) {
remoteFS.await()
} ?: throw RootServiceException()
}
@@ -58,6 +58,10 @@ class RootInstaller(
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
+ fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
+ File(path, "su").canExecute()
+ } ?: false
+
suspend fun isAppInstalled(packageName: String) =
awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
index 0e779df7..a5c551a4 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -135,7 +135,7 @@ class PatcherWorker(
return try {
if (args.input is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let {
- if (it.installType == InstallType.ROOT) {
+ if (it.installType == InstallType.MOUNT) {
rootInstaller.unmount(args.packageName)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
index ec3cf979..e331db2e 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
@@ -7,6 +7,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -27,7 +28,7 @@ fun InstallPickerDialog(
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
- Button(onClick = onDismiss) {
+ TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
},
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
index 2727e290..239aebbf 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
@@ -81,7 +81,7 @@ fun InstalledAppInfoScreen(
AppInfo(viewModel.appInfo) {
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
- if (viewModel.installedApp.installType == InstallType.ROOT) {
+ if (viewModel.installedApp.installType == InstallType.MOUNT) {
Text(
text = if (viewModel.isMounted) {
stringResource(R.string.mounted)
@@ -112,7 +112,7 @@ fun InstalledAppInfoScreen(
onClick = viewModel::uninstall
)
- InstallType.ROOT -> {
+ InstallType.MOUNT -> {
SegmentedButton(
icon = Icons.Outlined.SettingsBackupRestore,
text = stringResource(R.string.unpatch),
@@ -138,7 +138,7 @@ fun InstalledAppInfoScreen(
onPatchClick(viewModel.installedApp.originalPackageName, it)
}
},
- enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess()
+ enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
)
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
index 096bbf03..2c7792d0 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
@@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
+import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.InstallerStatusDialog
@@ -139,7 +140,8 @@ fun PatcherScreen(
},
onClick = {
if (vm.installedPackageName == null)
- showInstallPicker = true
+ if (vm.isDeviceRooted()) showInstallPicker = true
+ else vm.install(InstallType.DEFAULT)
else vm.open()
}
)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
index 3a5fc3d7..2ca8baa6 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
@@ -113,8 +113,8 @@ fun VersionSelectorScreen(
onClick = { viewModel.select(it) },
patchCount = supportedVersions[it.version],
enabled =
- !(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
- alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT
+ !(installedApp?.installType == InstallType.MOUNT && !viewModel.rootInstaller.hasRootAccess()),
+ alreadyPatched = installedApp != null && installedApp.installType != InstallType.MOUNT
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
index 7e610f55..93e2cb74 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
@@ -78,7 +78,7 @@ class InstalledAppInfoViewModel(
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
- InstallType.ROOT -> viewModelScope.launch {
+ InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(installedApp.currentPackageName)
installedAppRepository.delete(installedApp)
onBackClick()
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt
index 27bec4c4..42ad08c7 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt
@@ -30,7 +30,7 @@ class InstalledAppsViewModel(
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
try {
if (
- installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
+ installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
) {
installedAppsRepository.delete(installedApp)
return@withContext null
@@ -39,7 +39,7 @@ class InstalledAppsViewModel(
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
- if (packageInfo == null && installedApp.installType != InstallType.ROOT) {
+ if (packageInfo == null && installedApp.installType != InstallType.MOUNT) {
installedAppsRepository.delete(installedApp)
return@withContext null
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
index ae7f95b9..a63feed7 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
@@ -77,7 +77,7 @@ class PatcherViewModel(
override fun install() {
// Since this is a package installer status dialog,
- // InstallType.ROOT is never used here.
+ // InstallType.MOUNT is never used here.
install(InstallType.DEFAULT)
}
}
@@ -230,7 +230,7 @@ class PatcherViewModel(
app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId)
- if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
+ if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
GlobalScope.launch(Dispatchers.Main) {
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
withTimeout(Duration.ofMinutes(1L)) {
@@ -243,6 +243,8 @@ class PatcherViewModel(
tempDir.deleteRecursively()
}
+ fun isDeviceRooted() = rootInstaller.isDeviceRooted()
+
fun export(uri: Uri?) = viewModelScope.launch {
uri?.let {
withContext(Dispatchers.IO) {
@@ -301,7 +303,7 @@ class PatcherViewModel(
pmInstallStarted = true
}
- InstallType.ROOT -> {
+ InstallType.MOUNT -> {
try {
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
@@ -332,7 +334,7 @@ class PatcherViewModel(
packageName,
packageName,
input.selectedApp.version,
- InstallType.ROOT,
+ InstallType.MOUNT,
input.selectedPatches
)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5b6c6d93..ffcfc9fd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -223,7 +223,7 @@
Applied patches
View applied patches
Default
- Root
+ Mount
Mounted
Not mounted
Mount
From 697386c36c769c07db568aced4b8388e7242924d Mon Sep 17 00:00:00 2001
From: kitadai31 <90122968+kitadai31@users.noreply.github.com>
Date: Tue, 1 Oct 2024 01:51:22 +0900
Subject: [PATCH 2/9] fix: Match "Installation incompatible" dialog message
with Flutter Manager (#2231)
---
app/src/main/res/values/strings.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ffcfc9fd..0b260bb8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -374,7 +374,7 @@
The installation was cancelled manually. Try again?
The installation was blocked. Review your device security settings and try again.
The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?
- The app is incompatible with this device. Use the correct APK for your device and try again.
+ The app is incompatible with this device. Use an APK that is supported by this device and try again.
The app is invalid. Uninstall the app and try again?
The app could not be installed due to insufficient storage. Free up some space and try again.
The installation took too long. Try again?
From b4c37e6ddc396dd15731d861448dba6beff65ced Mon Sep 17 00:00:00 2001
From: Benjamin <73490201+BenjaminHalko@users.noreply.github.com>
Date: Wed, 6 Nov 2024 12:48:40 -0800
Subject: [PATCH 3/9] feat: Add haptic feedback (#1457)
Co-authored-by: Ushie
---
.../manager/ui/component/AutoUpdatesDialog.kt | 6 +--
.../ui/component/AvailableUpdateDialog.kt | 3 +-
.../ui/component/bundle/BaseBundleDialog.kt | 3 +-
.../manager/ui/component/bundle/BundleItem.kt | 4 +-
.../ui/component/bundle/ImportBundleDialog.kt | 29 +++--------
.../ui/component/haptics/HapticCheckbox.kt | 40 ++++++++++++++++
.../HapticExtendedFloatingActionButton.kt | 48 +++++++++++++++++++
.../haptics/HapticFloatingActionButton.kt | 44 +++++++++++++++++
.../ui/component/haptics/HapticRadioButton.kt | 42 ++++++++++++++++
.../ui/component/haptics/HapticSwitch.kt | 46 ++++++++++++++++++
.../manager/ui/component/haptics/HapticTab.kt | 43 +++++++++++++++++
.../component/patcher/InstallPickerDialog.kt | 10 ++--
.../ui/component/patches/OptionFields.kt | 47 +++++-------------
.../ui/component/settings/BooleanItem.kt | 4 +-
.../manager/ui/screen/DashboardScreen.kt | 8 ++--
.../manager/ui/screen/PatcherScreen.kt | 18 ++-----
.../ui/screen/PatchesSelectorScreen.kt | 13 +++--
.../ui/screen/SelectedAppInfoScreen.kt | 17 ++-----
.../ui/screen/VersionSelectorScreen.kt | 8 ++--
.../settings/DownloadsSettingsScreen.kt | 4 +-
.../screen/settings/GeneralSettingsScreen.kt | 3 +-
21 files changed, 326 insertions(+), 114 deletions(-)
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
diff --git a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
index 9da4f27f..1e2234eb 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
@@ -8,7 +8,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Source
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
@@ -24,6 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
+import app.revanced.manager.ui.component.haptics.HapticCheckbox
@Composable
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
@@ -76,6 +76,6 @@ private fun AutoUpdatesItem(
) = ListItem(
leadingContent = { Icon(icon, null) },
headlineContent = { Text(stringResource(headline)) },
- trailingContent = { Checkbox(checked = checked, onCheckedChange = null) },
+ trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) },
modifier = Modifier.clickable { onCheckedChange(!checked) }
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt
index 7059ad0d..4a684c1e 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
+import app.revanced.manager.ui.component.haptics.HapticCheckbox
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -70,7 +71,7 @@ fun AvailableUpdateDialog(
},
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
- Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
+ HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
}
}
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
index 4450ef5c..dfc63735 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.TextInputDialog
+import app.revanced.manager.ui.component.haptics.HapticSwitch
@Composable
fun BaseBundleDialog(
@@ -89,7 +90,7 @@ fun BaseBundleDialog(
headlineText = stringResource(R.string.bundle_auto_update),
supportingText = stringResource(R.string.bundle_auto_update_description),
trailingContent = {
- Switch(
+ HapticSwitch(
checked = autoUpdate,
onCheckedChange = onAutoUpdateChange
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
index 617c384f..2fdd8f5d 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Warning
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
@@ -27,6 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource
+import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map
@@ -71,7 +71,7 @@ fun BundleItem(
),
leadingContent = if (selectable) {
{
- Checkbox(
+ HapticCheckbox(
checked = isBundleSelected,
onCheckedChange = toggleSelection,
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
index 2de48a56..2de10053 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
@@ -10,26 +10,9 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Topic
-import androidx.compose.material3.Checkbox
-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
-import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -37,6 +20,8 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.TextHorizontalPadding
+import app.revanced.manager.ui.component.haptics.HapticCheckbox
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.model.BundleType
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
@@ -170,7 +155,7 @@ fun SelectBundleTypeStep(
overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
leadingContent = {
- RadioButton(
+ HapticRadioButton(
selected = bundleType == BundleType.Remote,
onClick = null
)
@@ -186,7 +171,7 @@ fun SelectBundleTypeStep(
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
overlineContent = { },
leadingContent = {
- RadioButton(
+ HapticRadioButton(
selected = bundleType == BundleType.Local,
onClick = null
)
@@ -263,7 +248,7 @@ fun ImportBundleStep(
headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
- Checkbox(
+ HapticCheckbox(
checked = autoUpdate,
onCheckedChange = {
onAutoUpdateChange(!autoUpdate)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
new file mode 100644
index 00000000..fb98e40f
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
@@ -0,0 +1,40 @@
+package app.revanced.manager.ui.component.haptics
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxColors
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticCheckbox (
+ checked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit)?,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: CheckboxColors = CheckboxDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+ val checkedState = remember { mutableStateOf(checked) }
+
+ // Perform haptic feedback
+ if (checkedState.value != checked) {
+ val view = LocalView.current
+ view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
+ checkedState.value = checked
+ }
+
+ Checkbox(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
new file mode 100644
index 00000000..f9d91caf
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
@@ -0,0 +1,48 @@
+package app.revanced.manager.ui.component.haptics
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.FloatingActionButtonElevation
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticExtendedFloatingActionButton (
+ text: @Composable () -> Unit,
+ icon: @Composable () -> Unit,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ expanded: Boolean = true,
+ shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ val view = LocalView.current
+
+ ExtendedFloatingActionButton(
+ text = text,
+ icon = icon,
+ onClick = {
+ // Perform haptic feedback
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+
+ onClick()
+ },
+ modifier = modifier,
+ expanded = expanded,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ interactionSource = interactionSource
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
new file mode 100644
index 00000000..0268accc
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
@@ -0,0 +1,44 @@
+package app.revanced.manager.ui.component.haptics
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.FloatingActionButtonElevation
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticFloatingActionButton (
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = FloatingActionButtonDefaults.shape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit,
+) {
+ val view = LocalView.current
+
+ FloatingActionButton(
+ onClick = {
+ // Perform haptic feedback
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+
+ onClick()
+ },
+ modifier = modifier,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ interactionSource = interactionSource,
+ content = content
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
new file mode 100644
index 00000000..7ac6a7a8
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
@@ -0,0 +1,42 @@
+package app.revanced.manager.ui.component.haptics
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.RadioButtonColors
+import androidx.compose.material3.RadioButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticRadioButton (
+ selected: Boolean,
+ onClick: (() -> Unit)?,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: RadioButtonColors = RadioButtonDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+ val selectedState = remember { mutableStateOf(selected) }
+
+ // Perform haptic feedback
+ if (selectedState.value != selected) {
+ if (selected) {
+ val view = LocalView.current
+ view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
+ }
+ selectedState.value = selected
+ }
+
+ RadioButton(
+ selected = selected,
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
new file mode 100644
index 00000000..fa3e894b
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
@@ -0,0 +1,46 @@
+package app.revanced.manager.ui.component.haptics
+import android.os.Build
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchColors
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticSwitch(
+ checked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit),
+ modifier: Modifier = Modifier,
+ thumbContent: (@Composable () -> Unit)? = null,
+ enabled: Boolean = true,
+ colors: SwitchColors = SwitchDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ val checkedState = remember { mutableStateOf(checked) }
+
+ // Perform haptic feedback
+ if (checkedState.value != checked) {
+ val view = LocalView.current
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ view.performHapticFeedback(if (checked) HapticFeedbackConstants.TOGGLE_ON else HapticFeedbackConstants.TOGGLE_OFF)
+ } else {
+ view.performHapticFeedback(if (checked) HapticFeedbackConstants.VIRTUAL_KEY else HapticFeedbackConstants.CLOCK_TICK)
+ }
+ checkedState.value = checked
+ }
+
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ thumbContent = thumbContent,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
new file mode 100644
index 00000000..3b5a11e9
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
@@ -0,0 +1,43 @@
+package app.revanced.manager.ui.component.haptics
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.Tab
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun HapticTab (
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ text: @Composable (() -> Unit)? = null,
+ icon: @Composable (() -> Unit)? = null,
+ selectedContentColor: Color = LocalContentColor.current,
+ unselectedContentColor: Color = selectedContentColor,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+ val view = LocalView.current
+
+ Tab(
+ selected = selected,
+ onClick = {
+ // Perform haptic feedback
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+
+ onClick()
+ },
+ modifier = modifier,
+ enabled = enabled,
+ text = text,
+ icon = icon,
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor,
+ interactionSource = interactionSource
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
index e331db2e..bb667bad 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
@@ -2,12 +2,7 @@ package app.revanced.manager.ui.component.patcher
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -17,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
@Composable
fun InstallPickerDialog(
@@ -49,7 +45,7 @@ fun InstallPickerDialog(
ListItem(
modifier = Modifier.clickable { selectedInstallType = it },
leadingContent = {
- RadioButton(
+ HapticRadioButton(
selected = selectedInstallType == it,
onClick = null
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
index 433c711b..993270ea 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
@@ -20,53 +20,31 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle
-import androidx.compose.material.icons.outlined.Add
-import androidx.compose.material.icons.outlined.Delete
-import androidx.compose.material.icons.outlined.Edit
-import androidx.compose.material.icons.outlined.Folder
-import androidx.compose.material.icons.outlined.MoreVert
-import androidx.compose.material.icons.outlined.Restore
-import androidx.compose.material.icons.outlined.SelectAll
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.TextButton
+import androidx.compose.material.icons.outlined.*
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog as ComposeDialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.patcher.patch.Option
-import app.revanced.manager.ui.component.AlertDialogExtended
-import app.revanced.manager.ui.component.AppTopBar
-import app.revanced.manager.ui.component.FloatInputDialog
-import app.revanced.manager.ui.component.IntInputDialog
-import app.revanced.manager.ui.component.LongInputDialog
+import app.revanced.manager.ui.component.*
+import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
+import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateListSaver
@@ -80,6 +58,7 @@ import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState
import java.io.Serializable
import kotlin.random.Random
+import androidx.compose.ui.window.Dialog as ComposeDialog
private class OptionEditorScope(
private val editor: OptionEditor,
@@ -335,7 +314,7 @@ private object BooleanOptionEditor : OptionEditor {
@Composable
override fun ListItemTrailingContent(scope: OptionEditorScope) {
- Switch(checked = scope.current, onCheckedChange = scope.setValue)
+ HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
}
@Composable
@@ -422,7 +401,7 @@ private class PresetOptionEditor(private val innerEditor: OptionEditor<
headlineContent = { Text(title) },
supportingContent = value?.toString()?.let { { Text(it) } },
leadingContent = {
- RadioButton(
+ HapticRadioButton(
selected = selectedPreset == presetKey,
onClick = { selectedPreset = presetKey }
)
@@ -568,7 +547,7 @@ private class ListOptionEditor(private val elementEditor: Opti
floatingActionButton = {
if (deleteMode) return@Scaffold
- ExtendedFloatingActionButton(
+ HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.add)) },
icon = {
Icon(
diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt
index 42e9a83e..0be1be91 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt
@@ -2,13 +2,13 @@ package app.revanced.manager.ui.component.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
-import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.domain.manager.base.Preference
+import app.revanced.manager.ui.component.haptics.HapticSwitch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -45,7 +45,7 @@ fun BooleanItem(
headlineContent = stringResource(headline),
supportingContent = stringResource(description),
trailingContent = {
- Switch(
+ HapticSwitch(
checked = value,
onCheckedChange = onValueChange,
)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
index bf310fc7..157986ec 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
@@ -33,6 +33,8 @@ import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar
+import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
+import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.toast
@@ -168,7 +170,7 @@ fun DashboardScreen(
}
},
floatingActionButton = {
- FloatingActionButton(
+ HapticFloatingActionButton(
onClick = {
vm.cancelSourceSelection()
@@ -181,7 +183,7 @@ fun DashboardScreen(
DashboardPage.BUNDLES.ordinal
)
}
- return@FloatingActionButton
+ return@HapticFloatingActionButton
}
onAppSelectorClick()
@@ -201,7 +203,7 @@ fun DashboardScreen(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {
DashboardPage.entries.forEachIndexed { index, page ->
- Tab(
+ HapticTab(
selected = pagerState.currentPage == index,
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
text = { Text(stringResource(page.titleResId)) },
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
index 2c7792d0..6aedde2f 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
@@ -4,12 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
@@ -17,13 +12,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save
-import androidx.compose.material3.BottomAppBar
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.Text
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -42,6 +31,7 @@ import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.InstallerStatusDialog
+import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
import app.revanced.manager.ui.component.patcher.Steps
import app.revanced.manager.ui.model.State
@@ -121,7 +111,7 @@ fun PatcherScreen(
},
floatingActionButton = {
AnimatedVisibility(visible = canInstall) {
- ExtendedFloatingActionButton(
+ HapticExtendedFloatingActionButton(
text = {
Text(
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
index f8109fab..511ede21 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
@@ -35,6 +35,9 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.SearchView
+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.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
@@ -293,7 +296,7 @@ fun PatchesSelectorScreen(
floatingActionButton = {
if (!showPatchButton) return@Scaffold
- ExtendedFloatingActionButton(
+ HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) },
icon = {
Icon(
@@ -321,7 +324,7 @@ fun PatchesSelectorScreen(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {
bundles.forEachIndexed { index, bundle ->
- Tab(
+ HapticTab(
selected = pagerState.currentPage == index,
onClick = {
composableScope.launch {
@@ -432,13 +435,13 @@ private fun PatchItem(
selected: Boolean,
onToggle: () -> Unit,
supported: Boolean = true
-) = ListItem(
+) = ListItem (
modifier = Modifier
.let { if (!supported) it.alpha(0.5f) else it }
.clickable(onClick = onToggle)
.fillMaxSize(),
leadingContent = {
- Checkbox(
+ HapticCheckbox(
checked = selected,
onCheckedChange = { onToggle() },
enabled = supported
@@ -452,7 +455,7 @@ private fun PatchItem(
Icon(Icons.Outlined.Settings, null)
}
}
- }
+ },
)
@Composable
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
index c4bfc841..9c3f59d4 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
@@ -8,13 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -28,6 +22,7 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
+import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp
@@ -36,11 +31,7 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.toast
-import dev.olshevski.navigation.reimagined.AnimatedNavHost
-import dev.olshevski.navigation.reimagined.NavBackHandler
-import dev.olshevski.navigation.reimagined.navigate
-import dev.olshevski.navigation.reimagined.pop
-import dev.olshevski.navigation.reimagined.rememberNavController
+import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -161,7 +152,7 @@ private fun SelectedAppInfoScreen(
)
},
floatingActionButton = {
- ExtendedFloatingActionButton(
+ HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
index 2ca8baa6..cd18dcc7 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
@@ -11,10 +11,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
-import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -34,6 +32,8 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
+import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
@@ -81,7 +81,7 @@ fun VersionSelectorScreen(
)
},
floatingActionButton = {
- ExtendedFloatingActionButton(
+ HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.select_version)) },
icon = {
Icon(
@@ -170,7 +170,7 @@ fun SelectedAppItem(
alreadyPatched: Boolean = false,
) {
ListItem(
- leadingContent = { RadioButton(selected, null) },
+ leadingContent = { HapticRadioButton(selected, null) },
headlineContent = { Text(selectedApp.version) },
supportingContent = when (selectedApp) {
is SelectedApp.Installed ->
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
index 432c4808..41e80b40 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -20,6 +19,7 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.GroupHeader
+import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
@@ -70,7 +70,7 @@ fun DownloadsSettingsScreen(
modifier = Modifier.clickable { viewModel.toggleItem(app) },
headlineContent = app.packageName,
leadingContent = (@Composable {
- Checkbox(
+ HapticCheckbox(
checked = selected,
onCheckedChange = { viewModel.toggleItem(app) }
)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
index 03eae09f..56242679 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt
@@ -28,6 +28,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.GroupHeader
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.theme.Theme
@@ -113,7 +114,7 @@ private fun ThemePicker(
.clickable { selectedTheme = it },
verticalAlignment = Alignment.CenterVertically
) {
- RadioButton(
+ HapticRadioButton(
selected = selectedTheme == it,
onClick = { selectedTheme = it })
Text(stringResource(it.displayName))
From cf322147d5e16d816508c9b015eaed73a318275d Mon Sep 17 00:00:00 2001
From: Ax333l
Date: Tue, 12 Nov 2024 21:17:02 +0100
Subject: [PATCH 4/9] fix: only perform haptics on events
---
.../ui/component/haptics/HapticCheckbox.kt | 16 ++--------
.../HapticExtendedFloatingActionButton.kt | 11 ++-----
.../haptics/HapticFloatingActionButton.kt | 11 ++-----
.../ui/component/haptics/HapticRadioButton.kt | 22 ++++++--------
.../ui/component/haptics/HapticSwitch.kt | 29 ++++++++-----------
.../manager/ui/component/haptics/HapticTab.kt | 11 ++-----
.../screen/settings/AdvancedSettingsScreen.kt | 8 ++---
.../java/app/revanced/manager/util/Util.kt | 24 ++++++++++++++-
8 files changed, 56 insertions(+), 76 deletions(-)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
index fb98e40f..fb5453f9 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt
@@ -6,13 +6,12 @@ import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxColors
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
+import app.revanced.manager.util.withHapticFeedback
@Composable
-fun HapticCheckbox (
+fun HapticCheckbox(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
@@ -20,18 +19,9 @@ fun HapticCheckbox (
colors: CheckboxColors = CheckboxDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- val checkedState = remember { mutableStateOf(checked) }
-
- // Perform haptic feedback
- if (checkedState.value != checked) {
- val view = LocalView.current
- view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
- checkedState.value = checked
- }
-
Checkbox(
checked = checked,
- onCheckedChange = onCheckedChange,
+ onCheckedChange = onCheckedChange?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK),
modifier = modifier,
enabled = enabled,
colors = colors,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
index f9d91caf..4fc6ad30 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt
@@ -11,7 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.platform.LocalView
+import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticExtendedFloatingActionButton (
@@ -26,17 +26,10 @@ fun HapticExtendedFloatingActionButton (
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
- val view = LocalView.current
-
ExtendedFloatingActionButton(
text = text,
icon = icon,
- onClick = {
- // Perform haptic feedback
- view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
-
- onClick()
- },
+ onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
expanded = expanded,
shape = shape,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
index 0268accc..f4a2e153 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt
@@ -11,7 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.platform.LocalView
+import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticFloatingActionButton (
@@ -24,15 +24,8 @@ fun HapticFloatingActionButton (
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit,
) {
- val view = LocalView.current
-
FloatingActionButton(
- onClick = {
- // Perform haptic feedback
- view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
-
- onClick()
- },
+ onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
shape = shape,
containerColor = containerColor,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
index 7ac6a7a8..63a9e582 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt
@@ -6,13 +6,12 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
@Composable
-fun HapticRadioButton (
+fun HapticRadioButton(
selected: Boolean,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
@@ -20,20 +19,17 @@ fun HapticRadioButton (
colors: RadioButtonColors = RadioButtonDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- val selectedState = remember { mutableStateOf(selected) }
-
- // Perform haptic feedback
- if (selectedState.value != selected) {
- if (selected) {
- val view = LocalView.current
- view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
- }
- selectedState.value = selected
- }
+ val view = LocalView.current
RadioButton(
selected = selected,
- onClick = onClick,
+ onClick = onClick?.let {
+ {
+ // Perform haptic feedback
+ if (!selected) view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
+ it()
+ }
+ },
modifier = modifier,
enabled = enabled,
colors = colors,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
index fa3e894b..c2491397 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt
@@ -1,4 +1,5 @@
package app.revanced.manager.ui.component.haptics
+
import android.os.Build
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -6,37 +7,31 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
@Composable
fun HapticSwitch(
checked: Boolean,
- onCheckedChange: ((Boolean) -> Unit),
+ onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
thumbContent: (@Composable () -> Unit)? = null,
enabled: Boolean = true,
colors: SwitchColors = SwitchDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
- val checkedState = remember { mutableStateOf(checked) }
-
- // Perform haptic feedback
- if (checkedState.value != checked) {
- val view = LocalView.current
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- view.performHapticFeedback(if (checked) HapticFeedbackConstants.TOGGLE_ON else HapticFeedbackConstants.TOGGLE_OFF)
- } else {
- view.performHapticFeedback(if (checked) HapticFeedbackConstants.VIRTUAL_KEY else HapticFeedbackConstants.CLOCK_TICK)
- }
- checkedState.value = checked
- }
-
Switch(
checked = checked,
- onCheckedChange = onCheckedChange,
+ onCheckedChange = { newChecked ->
+ val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ when {
+ newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
+ newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
+ !newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
+ !newChecked -> HapticFeedbackConstants.CLOCK_TICK
+ }
+ onCheckedChange(newChecked)
+ },
modifier = modifier,
thumbContent = thumbContent,
enabled = enabled,
diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
index 3b5a11e9..d0676951 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt
@@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalView
+import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticTab (
@@ -22,16 +22,9 @@ fun HapticTab (
unselectedContentColor: Color = selectedContentColor,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- val view = LocalView.current
-
Tab(
selected = selected,
- onClick = {
- // Perform haptic feedback
- view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
-
- onClick()
- },
+ onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
enabled = enabled,
text = text,
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
index fa8cae28..9082f4bb 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
@@ -4,6 +4,7 @@ import android.app.ActivityManager
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Build
+import android.view.HapticFeedbackConstants
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -17,9 +18,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -35,6 +34,7 @@ import app.revanced.manager.ui.component.settings.IntegerItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import app.revanced.manager.util.toast
+import app.revanced.manager.util.withHapticFeedback
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -52,7 +52,6 @@ fun AdvancedSettingsScreen(
activityManager.largeMemoryClass
)
}
- val haptics = LocalHapticFeedback.current
Scaffold(
topBar = {
@@ -159,13 +158,12 @@ fun AdvancedSettingsScreen(
onClick = { },
onLongClickLabel = stringResource(R.string.copy_to_clipboard),
onLongClick = {
- haptics.performHapticFeedback(HapticFeedbackType.LongPress)
clipboard.setPrimaryClip(
ClipData.newPlainText("Device Information", deviceContent)
)
context.toast(context.getString(R.string.toast_copied_to_clipboard))
- }
+ }.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
),
headlineContent = stringResource(R.string.about_device),
supportingContent = deviceContent
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index f1de38fd..bc48c54a 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -14,6 +14,7 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -21,6 +22,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -218,4 +220,24 @@ fun ScrollState.isScrollingUp(): State {
}
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
-val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
\ No newline at end of file
+val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
+
+@Composable
+@ReadOnlyComposable
+fun (() -> R).withHapticFeedback(constant: Int): () -> R {
+ val view = LocalView.current
+ return {
+ view.performHapticFeedback(constant)
+ this()
+ }
+}
+
+@Composable
+@ReadOnlyComposable
+fun ((T) -> R).withHapticFeedback(constant: Int): (T) -> R {
+ val view = LocalView.current
+ return {
+ view.performHapticFeedback(constant)
+ this(it)
+ }
+}
\ No newline at end of file
From 20c13ee71cb3ed868a0a9375e72d8e9660c10c75 Mon Sep 17 00:00:00 2001
From: Ax333l
Date: Wed, 13 Nov 2024 22:11:36 +0100
Subject: [PATCH 5/9] chore: update dependencies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
🦀 integrations are gone! 🦀
---
app/build.gradle.kts | 21 +++---
.../1.json | 24 +++----
.../data/room/bundles/PatchBundleDao.kt | 8 +--
.../data/room/bundles/PatchBundleEntity.kt | 9 +--
.../manager/data/room/options/Option.kt | 23 ++++---
.../domain/bundles/LocalPatchBundle.kt | 19 ++----
.../domain/bundles/PatchBundleSource.kt | 9 ++-
.../domain/bundles/RemotePatchBundle.kt | 67 +++++--------------
.../manager/domain/installer/RootInstaller.kt | 7 +-
.../manager/domain/manager/KeystoreManager.kt | 16 +++--
.../domain/manager/PreferencesManager.kt | 1 -
.../PatchBundlePersistenceRepository.kt | 9 ++-
.../repository/PatchBundleRepository.kt | 8 +--
.../manager/network/dto/BundleInfo.kt | 9 ---
.../manager/network/dto/PatchBundleInfo.kt | 7 ++
.../app/revanced/manager/patcher/Session.kt | 11 ++-
.../manager/patcher/patch/PatchBundle.kt | 10 +--
.../manager/patcher/patch/PatchInfo.kt | 49 +++++---------
.../patcher/runtime/CoroutineRuntime.kt | 8 +--
.../manager/patcher/runtime/ProcessRuntime.kt | 4 +-
.../manager/patcher/runtime/Runtime.kt | 1 -
.../patcher/runtime/process/Parameters.kt | 2 -
.../patcher/runtime/process/PatcherProcess.kt | 9 +--
.../manager/patcher/worker/PatcherWorker.kt | 2 +-
.../manager/ui/component/AutoUpdatesDialog.kt | 4 +-
.../bundle/BundleInformationDialog.kt | 2 +-
.../manager/ui/component/bundle/BundleItem.kt | 2 +-
.../ui/component/bundle/ImportBundleDialog.kt | 52 ++++----------
.../component/patcher/InstallPickerDialog.kt | 6 +-
.../ui/component/patches/OptionFields.kt | 29 ++++----
.../manager/ui/screen/AppSelectorScreen.kt | 5 +-
.../manager/ui/screen/DashboardScreen.kt | 4 +-
.../ui/screen/PatchesSelectorScreen.kt | 4 +-
.../ui/screen/VersionSelectorScreen.kt | 2 +-
.../screen/settings/AdvancedSettingsScreen.kt | 6 --
.../ui/viewmodel/AppSelectorViewModel.kt | 2 +-
.../ui/viewmodel/DashboardViewModel.kt | 7 +-
.../manager/ui/viewmodel/PatcherViewModel.kt | 6 +-
.../app/revanced/manager/util/Constants.kt | 10 +--
.../main/java/app/revanced/manager/util/PM.kt | 4 +-
.../java/app/revanced/manager/util/Util.kt | 14 +++-
app/src/main/res/values/strings.xml | 6 --
build.gradle.kts | 3 +
gradle/libs.versions.toml | 35 +++++-----
gradle/wrapper/gradle-wrapper.properties | 3 +-
45 files changed, 223 insertions(+), 316 deletions(-)
delete mode 100644 app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt
create mode 100644 app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 70281945..9f04a1b9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -3,21 +3,22 @@ import kotlin.random.Random
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.compose.compiler)
alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries)
- id("kotlin-parcelize")
- kotlin("plugin.serialization") version "1.9.23"
}
android {
namespace = "app.revanced.manager"
- compileSdk = 34
- buildToolsVersion = "34.0.0"
+ compileSdk = 35
+ buildToolsVersion = "35.0.0"
defaultConfig {
applicationId = "app.revanced.manager"
minSdk = 26
- targetSdk = 34
+ targetSdk = 35
versionCode = 1
versionName = "0.0.1"
vectorDrawables.useSupportLibrary = true
@@ -81,9 +82,11 @@ android {
jvmTarget = "17"
}
- buildFeatures.compose = true
- buildFeatures.aidl = true
- buildFeatures.buildConfig=true
+ buildFeatures {
+ compose = true
+ aidl = true
+ buildConfig = true
+ }
android {
androidResources {
@@ -91,7 +94,6 @@ android {
}
}
- composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
@@ -112,7 +114,6 @@ dependencies {
implementation(libs.runtime.compose)
implementation(libs.splash.screen)
implementation(libs.compose.activity)
- implementation(libs.paging.common.ktx)
implementation(libs.work.runtime.ktx)
implementation(libs.preferences.datastore)
diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
index e9c0fd3a..eff10786 100644
--- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
+++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
@@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 1,
- "identityHash": "1dd9d5c0201fdf3cfef3ae669fd65e46",
+ "identityHash": "c385297c07ea54804dc8526c388f706d",
"entities": [
{
"tableName": "patch_bundles",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
+ "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",
@@ -20,6 +20,12 @@
"affinity": "TEXT",
"notNull": true
},
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
{
"fieldPath": "source",
"columnName": "source",
@@ -31,18 +37,6 @@
"columnName": "auto_update",
"affinity": "INTEGER",
"notNull": true
- },
- {
- "fieldPath": "versionInfo.patches",
- "columnName": "version",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "versionInfo.integrations",
- "columnName": "integrations_version",
- "affinity": "TEXT",
- "notNull": false
}
],
"primaryKey": {
@@ -397,7 +391,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dd9d5c0201fdf3cfef3ae669fd65e46')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c385297c07ea54804dc8526c388f706d')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
index 77de9b03..d9955a70 100644
--- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
@@ -8,11 +8,11 @@ interface PatchBundleDao {
@Query("SELECT * FROM patch_bundles")
suspend fun all(): List
- @Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
+ @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow
- @Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
- suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
+ @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
+ suspend fun updateVersion(uid: Int, patches: String?)
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean)
@@ -26,7 +26,7 @@ interface PatchBundleDao {
@Transaction
suspend fun reset() {
purgeCustomBundles()
- updateVersion(0, null, null) // Reset the main source
+ updateVersion(0, null) // Reset the main source
}
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
index d120abf5..8ba5f64a 100644
--- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
@@ -29,21 +29,16 @@ sealed class Source {
}
}
-data class VersionInfo(
- @ColumnInfo(name = "version") val patches: String? = null,
- @ColumnInfo(name = "integrations_version") val integrations: String? = null,
-)
-
@Entity(tableName = "patch_bundles")
data class PatchBundleEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String,
- @Embedded val versionInfo: VersionInfo,
+ @ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)
data class BundleProperties(
- @Embedded val versionInfo: VersionInfo,
+ @ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt
index b59dbd16..44bc3d40 100644
--- a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt
@@ -20,6 +20,8 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
@Entity(
tableName = "options",
@@ -46,8 +48,8 @@ data class Option(
val errorMessage = "Cannot deserialize value as ${option.type}"
try {
- if (option.type.endsWith("Array")) {
- val elementType = option.type.removeSuffix("Array")
+ if (option.type.classifier == List::class) {
+ val elementType = option.type.arguments.first().type!!
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
}
@@ -67,12 +69,17 @@ data class Option(
allowSpecialFloatingPointValues = true
}
- private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) {
- "Boolean" -> value.boolean
- "Int" -> value.int
- "Long" -> value.long
- "Float" -> value.float
- "String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") }
+ private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
+ typeOf() -> value.boolean
+ typeOf() -> value.int
+ typeOf() -> value.long
+ typeOf() -> value.float
+ typeOf() -> value.content.also {
+ if (!value.isString) throw SerializationException(
+ "Expected value to be a string: $value"
+ )
+ }
+
else -> throw SerializationException("Unknown type: $type")
}
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
index 1d8b41f3..bcbc59cf 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
@@ -4,29 +4,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
-import java.nio.file.Files
-import java.nio.file.StandardCopyOption
class LocalPatchBundle(name: String, id: Int, directory: File) :
PatchBundleSource(name, id, directory) {
- suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
+ suspend fun replace(patches: InputStream) {
withContext(Dispatchers.IO) {
- patches?.let { inputStream ->
- patchBundleOutputStream().use { outputStream ->
- inputStream.copyTo(outputStream)
- }
- }
- integrations?.let {
- Files.copy(
- it,
- this@LocalPatchBundle.integrationsFile.toPath(),
- StandardCopyOption.REPLACE_EXISTING
- )
+ patchBundleOutputStream().use { outputStream ->
+ patches.copyTo(outputStream)
}
}
reload()?.also {
- saveVersion(it.readManifestAttribute("Version"), null)
+ saveVersion(it.readManifestAttribute("Version"))
}
}
}
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
index 1ded6d43..308e2a56 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
@@ -28,7 +28,6 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
protected val configRepository: PatchBundlePersistenceRepository by inject()
private val app: Application by inject()
protected val patchesFile = directory.resolve("patches.jar")
- protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(load())
val state = _state.asStateFlow()
@@ -58,7 +57,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
if (!hasInstalled()) return State.Missing
return try {
- State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
+ State.Loaded(PatchBundle(patchesFile))
} catch (t: Throwable) {
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
State.Failed(t)
@@ -85,9 +84,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!!
- suspend fun currentVersion() = getProps().versionInfo
- protected suspend fun saveVersion(patches: String?, integrations: String?) =
- configRepository.updateVersion(uid, patches, integrations)
+ suspend fun currentVersion() = getProps().version
+ protected suspend fun saveVersion(version: String?) =
+ configRepository.updateVersion(uid, version)
suspend fun setName(name: String) {
configRepository.setName(uid, name)
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
index 8bbc230d..e3214db9 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
@@ -1,20 +1,12 @@
package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
-import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.network.api.ReVancedAPI
-import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
-import app.revanced.manager.network.dto.BundleAsset
-import app.revanced.manager.network.dto.BundleInfo
+import app.revanced.manager.network.dto.PatchBundleInfo
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
-import app.revanced.manager.util.APK_MIMETYPE
-import app.revanced.manager.util.JAR_MIMETYPE
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import java.io.File
@@ -24,27 +16,17 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
PatchBundleSource(name, id, directory) {
protected val http: HttpService by inject()
- protected abstract suspend fun getLatestInfo(): BundleInfo
+ protected abstract suspend fun getLatestInfo(): PatchBundleInfo
- private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
- val (patches, integrations) = info
- coroutineScope {
- launch {
- patchBundleOutputStream().use {
- http.streamTo(it) {
- url(patches.url)
- }
- }
- }
-
- launch {
- http.download(integrationsFile) {
- url(integrations.url)
- }
+ private suspend fun download(info: PatchBundleInfo) = withContext(Dispatchers.IO) {
+ val (version, url) = info
+ patchBundleOutputStream().use {
+ http.streamTo(it) {
+ url(url)
}
}
- saveVersion(patches.version, integrations.version)
+ saveVersion(version)
reload()
}
@@ -54,20 +36,15 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo()
- if (hasInstalled() && VersionInfo(
- info.patches.version,
- info.integrations.version
- ) == currentVersion()
- ) {
+ if (hasInstalled() && info.version == currentVersion())
return@withContext false
- }
download(info)
true
}
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
- arrayOf(patchesFile, integrationsFile).forEach(File::delete)
+ patchesFile.delete()
reload()
}
@@ -81,7 +58,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
- http.request {
+ http.request {
url(endpoint)
}.getOrThrow()
}
@@ -91,22 +68,10 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedAPI by inject()
- override suspend fun getLatestInfo() = coroutineScope {
- fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
- api
- .getLatestRelease(repo)
- .getOrThrow()
- .let {
- BundleAsset(it.version, it.findAssetByType(mime).downloadUrl)
- }
+ override suspend fun getLatestInfo() = api
+ .getLatestRelease("revanced-patches")
+ .getOrThrow()
+ .let {
+ PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl)
}
-
- val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
- val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
-
- BundleInfo(
- patches.await(),
- integrations.await()
- )
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
index 885f8ad1..293484ca 100644
--- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
+++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
@@ -109,7 +109,12 @@ class RootInstaller(
stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo ->
- if (packageInfo.versionName <= version)
+ // TODO: get user id programmatically
+ if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
+ pm.getPackageInfo(patchedAPK)
+ ?: error("Failed to get package info for patched app")
+ )
+ )
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
}
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
index c1c4700d..4f9dc5a3 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt
@@ -12,6 +12,8 @@ import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.security.UnrecoverableKeyException
+import java.util.Date
+import kotlin.time.Duration.Companion.days
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object Constants {
@@ -19,6 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
* Default alias and password for the keystore.
*/
const val DEFAULT = "ReVanced"
+ private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
}
private val keystorePath =
@@ -29,23 +32,26 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
prefs.keystorePass.value = pass
}
- private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
+ private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
keyStore = path,
keyStorePassword = null,
alias = prefs.keystoreCommonName.get(),
- signer = prefs.keystoreCommonName.get(),
password = prefs.keystorePass.get()
)
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
- ApkUtils.sign(input, output, signingOptions())
+ ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
}
suspend fun regenerate() = withContext(Dispatchers.Default) {
+ val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
+ prefs.keystoreCommonName.get(),
+ eightYearsFromNow
+ )
val ks = ApkSigner.newKeyStore(
setOf(
ApkSigner.KeyStoreEntry(
- DEFAULT, DEFAULT
+ DEFAULT, DEFAULT, keyCertPair
)
)
)
@@ -64,7 +70,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
try {
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
- ApkSigner.readKeyCertificatePair(ks, cn, pass)
+ ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
} catch (_: UnrecoverableKeyException) {
return false
} catch (_: IllegalArgumentException) {
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
index 548df41a..8cdc1f19 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
@@ -12,7 +12,6 @@ class PreferencesManager(
val api = stringPreference("api_url", "https://api.revanced.app")
- val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
index 4b853ecf..5711d997 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
@@ -4,7 +4,6 @@ import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.bundles.Source
-import app.revanced.manager.data.room.bundles.VersionInfo
import kotlinx.coroutines.flow.distinctUntilChanged
class PatchBundlePersistenceRepository(db: AppDatabase) {
@@ -26,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
PatchBundleEntity(
uid = generateUid(),
name = name,
- versionInfo = VersionInfo(),
+ version = null,
source = source,
autoUpdate = autoUpdate
).also {
@@ -35,8 +34,8 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid)
- suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) =
- dao.updateVersion(uid, patches, integrations)
+ suspend fun updateVersion(uid: Int, version: String?) =
+ dao.updateVersion(uid, version)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
@@ -48,7 +47,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
val defaultSource = PatchBundleEntity(
uid = 0,
name = "",
- versionInfo = VersionInfo(),
+ version = null,
source = Source.API,
autoUpdate = false
)
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
index f40d6c0b..79bb5cea 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
@@ -3,7 +3,7 @@ package app.revanced.manager.domain.repository
import android.app.Application
import android.content.Context
import android.util.Log
-import app.revanced.library.PatchUtils
+import app.revanced.library.mostCommonCompatibleVersions
import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity
@@ -55,7 +55,7 @@ class PatchBundleRepository(
val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
- PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true)
+ allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)
.mapValues { (_, versions) ->
if (versions.keys.size < 2)
return@mapValues versions.keys.firstOrNull()
@@ -137,11 +137,11 @@ class PatchBundleRepository(
private fun addBundle(patchBundle: PatchBundleSource) =
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
- suspend fun createLocal(patches: InputStream, integrations: InputStream?) = withContext(Dispatchers.Default) {
+ suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
val uid = persistenceRepo.create("", SourceInfo.Local).uid
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
- bundle.replace(patches, integrations)
+ bundle.replace(patches)
addBundle(bundle)
}
diff --git a/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt
deleted file mode 100644
index e2b56a87..00000000
--- a/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package app.revanced.manager.network.dto
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset)
-
-@Serializable
-data class BundleAsset(val version: String, val url: String)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
new file mode 100644
index 00000000..02d89919
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
@@ -0,0 +1,7 @@
+package app.revanced.manager.network.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+// TODO: replace this
+data class PatchBundleInfo(val version: String, val url: String)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt
index 4393794d..d1368f24 100644
--- a/app/src/main/java/app/revanced/manager/patcher/Session.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt
@@ -22,7 +22,6 @@ class Session(
cacheDir: String,
frameworkDir: String,
aaptPath: String,
- multithreadingDexFileWriter: Boolean,
private val androidContext: Context,
private val logger: Logger,
private val input: File,
@@ -38,8 +37,7 @@ class Session(
apkFile = input,
temporaryFilesPath = tempDir,
frameworkFileDirectory = frameworkDir,
- aaptBinaryPath = aaptPath,
- multithreadingDexFileWriter = multithreadingDexFileWriter,
+ aaptBinaryPath = aaptPath
)
)
@@ -51,7 +49,7 @@ class Session(
state = State.RUNNING
)
- this.apply(true).collect { (patch, exception) ->
+ this().collect { (patch, exception) ->
if (patch !in selectedPatches) return@collect
if (exception != null) {
@@ -89,7 +87,7 @@ class Session(
)
}
- suspend fun run(output: File, selectedPatches: PatchList, integrations: List) {
+ suspend fun run(output: File, selectedPatches: PatchList) {
updateProgress(state = State.COMPLETED) // Unpacking
java.util.logging.Logger.getLogger("").apply {
@@ -103,8 +101,7 @@ class Session(
with(patcher) {
logger.info("Merging integrations")
- acceptIntegrations(integrations.toSet())
- acceptPatches(selectedPatches.toSet())
+ this += selectedPatches.toSet()
logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
index 8dbcf153..2b93a829 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt
@@ -2,17 +2,17 @@ package app.revanced.manager.patcher.patch
import android.util.Log
import app.revanced.manager.util.tag
-import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch
+import app.revanced.patcher.patch.PatchLoader
import java.io.File
import java.io.IOException
import java.util.jar.JarFile
-class PatchBundle(val patchesJar: File, val integrations: File?) {
+class PatchBundle(val patchesJar: File) {
private val loader = object : Iterable> {
private fun load(): Iterable> {
patchesJar.setReadOnly()
- return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
+ return PatchLoader.Dex(setOf(patchesJar))
}
override fun iterator(): Iterator> = load().iterator()
@@ -41,12 +41,12 @@ class PatchBundle(val patchesJar: File, val integrations: File?) {
/**
* Load all patches compatible with the specified package.
*/
- fun patchClasses(packageName: String) = loader.filter { patch ->
+ fun patches(packageName: String) = loader.filter { patch ->
val compatiblePackages = patch.compatiblePackages
?: // The patch has no compatibility constraints, which means it is universal.
return@filter true
- if (!compatiblePackages.any { it.name == packageName }) {
+ if (!compatiblePackages.any { (name, _) -> name == packageName }) {
// Patch is not compatible with this package.
return@filter false
}
diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
index 31e707ba..cd2a2f83 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
@@ -1,14 +1,14 @@
package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable
-import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch
-import app.revanced.patcher.patch.ResourcePatch
-import app.revanced.patcher.patch.options.PatchOption
+import app.revanced.patcher.patch.Option as PatchOption
+import app.revanced.patcher.patch.resourcePatch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
+import kotlin.reflect.KType
data class PatchInfo(
val name: String,
@@ -21,7 +21,12 @@ data class PatchInfo(
patch.name.orEmpty(),
patch.description,
patch.use,
- patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
+ patch.compatiblePackages?.map { (pkgName, versions) ->
+ CompatiblePackage(
+ pkgName,
+ versions?.toImmutableSet()
+ )
+ }?.toImmutableList(),
patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
)
@@ -45,37 +50,19 @@ data class PatchInfo(
* The resulting patch cannot be executed.
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
*/
- fun toPatcherPatch(): Patch<*> = object : ResourcePatch(
- name = name,
- description = description,
- compatiblePackages = compatiblePackages
- ?.map(app.revanced.manager.patcher.patch.CompatiblePackage::toPatcherCompatiblePackage)
- ?.toSet(),
- use = include,
- ) {
- override fun execute(context: ResourceContext) =
- throw Exception("Metadata patches cannot be executed")
- }
+ fun toPatcherPatch(): Patch<*> =
+ resourcePatch(name = name, description = description, use = include) {
+ compatiblePackages?.let { pkgs ->
+ compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
+ }
+ }
}
@Immutable
data class CompatiblePackage(
val packageName: String,
val versions: ImmutableSet?
-) {
- constructor(pkg: Patch.CompatiblePackage) : this(
- pkg.name,
- pkg.versions?.toImmutableSet()
- )
-
- /**
- * Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher.
- */
- fun toPatcherCompatiblePackage() = Patch.CompatiblePackage(
- name = packageName,
- versions = versions,
- )
-}
+)
@Immutable
data class Option(
@@ -83,7 +70,7 @@ data class Option(
val key: String,
val description: String,
val required: Boolean,
- val type: String,
+ val type: KType,
val default: T?,
val presets: Map?,
val validator: (T?) -> Boolean,
@@ -93,7 +80,7 @@ data class Option(
option.key,
option.description.orEmpty(),
option.required,
- option.valueType,
+ option.type,
option.default,
option.values,
{ option.validator(option, it) },
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
index e2aed2ee..3780e899 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
@@ -27,15 +27,13 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
val selectedBundles = selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
- .mapValues { (_, bundle) -> bundle.patchClasses(packageName) }
+ .mapValues { (_, bundle) -> bundle.patches(packageName) }
val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
}
- val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
-
// Set all patch options.
options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
@@ -53,7 +51,6 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
cacheDir,
frameworkPath,
aaptPath,
- enableMultithreadedDexWriter(),
context,
logger,
File(inputFile),
@@ -62,8 +59,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
).use { session ->
session.run(
File(outputFile),
- patchList,
- integrations
+ patchList
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
index 389d5201..ada1d943 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
@@ -70,7 +70,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
onProgress: ProgressEventHandler,
) = coroutineScope {
// Get the location of our own Apk.
- val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo.sourceDir
+ val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
val propOverride = resolvePropOverride(context)?.absolutePath
@@ -148,13 +148,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
packageName = packageName,
inputFile = inputFile,
outputFile = outputFile,
- enableMultithrededDexWriter = enableMultithreadedDexWriter(),
configurations = selectedPatches.map { (id, patches) ->
val bundle = bundles[id]!!
PatchConfiguration(
bundle.patchesJar.absolutePath,
- bundle.integrations?.absolutePath,
patches,
options[id].orEmpty()
)
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
index fd39c3f3..434c97c6 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
@@ -26,7 +26,6 @@ sealed class Runtime(context: Context) : KoinComponent {
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
protected suspend fun bundles() = patchBundlesRepo.bundles.first()
- protected suspend fun enableMultithreadedDexWriter() = prefs.multithreadingDexFileWriter.get()
abstract suspend fun execute(
inputFile: String,
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt
index c669c875..b00d558a 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt
@@ -12,14 +12,12 @@ data class Parameters(
val packageName: String,
val inputFile: String,
val outputFile: String,
- val enableMultithrededDexWriter: Boolean,
val configurations: List,
) : Parcelable
@Parcelize
data class PatchConfiguration(
val bundlePath: String,
- val integrationsPath: String?,
val patches: Set,
val options: @RawValue Map>
) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
index 4467f3ae..b0f8e248 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
@@ -54,13 +54,11 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
- val integrations =
- parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) }
val patchList = parameters.configurations.flatMap { config ->
- val bundle = PatchBundle(File(config.bundlePath), null)
+ val bundle = PatchBundle(File(config.bundlePath))
val patches =
- bundle.patchClasses(parameters.packageName).filter { it.name in config.patches }
+ bundle.patches(parameters.packageName).filter { it.name in config.patches }
.associateBy { it.name }
config.options.forEach { (patchName, opts) ->
@@ -81,7 +79,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir,
- multithreadingDexFileWriter = parameters.enableMultithrededDexWriter,
androidContext = context,
logger = logger,
input = File(parameters.inputFile),
@@ -90,7 +87,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
events.progress(name, state?.name, message)
}
).use {
- it.run(File(parameters.outputFile), patchList, integrations)
+ it.run(File(parameters.outputFile), patchList)
}
events.finished(null)
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
index a5c551a4..c295bde1 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -154,7 +154,7 @@ class PatcherWorker(
}
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
- is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
+ is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
}
val runtime = if (prefs.useProcessRuntime.get()) {
diff --git a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
index 1e2234eb..29f2f970 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.haptics.HapticCheckbox
+import app.revanced.manager.util.transparentListItemColors
@Composable
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
@@ -77,5 +78,6 @@ private fun AutoUpdatesItem(
leadingContent = { Icon(icon, null) },
headlineContent = { Text(stringResource(headline)) },
trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) },
- modifier = Modifier.clickable { onCheckedChange(!checked) }
+ modifier = Modifier.clickable { onCheckedChange(!checked) },
+ colors = transparentListItemColors
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
index f5919ced..83c60e0f 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
@@ -104,7 +104,7 @@ fun BundleInformationDialog(
name = bundleName,
remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount,
- version = props?.versionInfo?.patches,
+ version = props?.version,
autoUpdate = props?.autoUpdate ?: false,
onAutoUpdateChange = {
composableScope.launch {
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
index 2fdd8f5d..6f3ae914 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
@@ -45,7 +45,7 @@ fun BundleItem(
val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) {
- bundle.propsFlow().map { props -> props?.versionInfo?.patches }
+ bundle.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
val name by bundle.nameState
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
index 2de10053..cbc699ec 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt
@@ -23,19 +23,18 @@ import app.revanced.manager.ui.component.TextHorizontalPadding
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.model.BundleType
-import app.revanced.manager.util.APK_MIMETYPE
-import app.revanced.manager.util.JAR_MIMETYPE
+import app.revanced.manager.util.BIN_MIMETYPE
+import app.revanced.manager.util.transparentListItemColors
@Composable
fun ImportPatchBundleDialog(
onDismiss: () -> Unit,
onRemoteSubmit: (String, Boolean) -> Unit,
- onLocalSubmit: (Uri, Uri?) -> Unit
+ onLocalSubmit: (Uri) -> Unit
) {
var currentStep by rememberSaveable { mutableIntStateOf(0) }
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
var patchBundle by rememberSaveable { mutableStateOf(null) }
- var integrations by rememberSaveable { mutableStateOf(null) }
var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(false) }
@@ -45,16 +44,7 @@ fun ImportPatchBundleDialog(
}
fun launchPatchActivity() {
- patchActivityLauncher.launch(JAR_MIMETYPE)
- }
-
- val integrationsActivityLauncher =
- rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
- uri?.let { integrations = it }
- }
-
- fun launchIntegrationsActivity() {
- integrationsActivityLauncher.launch(APK_MIMETYPE)
+ patchActivityLauncher.launch(BIN_MIMETYPE)
}
val steps = listOf<@Composable () -> Unit>(
@@ -67,11 +57,9 @@ fun ImportPatchBundleDialog(
ImportBundleStep(
bundleType,
patchBundle,
- integrations,
remoteUrl,
autoUpdate,
{ launchPatchActivity() },
- { launchIntegrationsActivity() },
{ remoteUrl = it },
{ autoUpdate = it }
)
@@ -99,13 +87,7 @@ fun ImportPatchBundleDialog(
enabled = inputsAreValid,
onClick = {
when (bundleType) {
- BundleType.Local -> patchBundle?.let {
- onLocalSubmit(
- it,
- integrations
- )
- }
-
+ BundleType.Local -> patchBundle?.let(onLocalSubmit)
BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
}
}
@@ -159,7 +141,8 @@ fun SelectBundleTypeStep(
selected = bundleType == BundleType.Remote,
onClick = null
)
- }
+ },
+ colors = transparentListItemColors
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
ListItem(
@@ -175,7 +158,8 @@ fun SelectBundleTypeStep(
selected = bundleType == BundleType.Local,
onClick = null
)
- }
+ },
+ colors = transparentListItemColors
)
}
}
@@ -186,11 +170,9 @@ fun SelectBundleTypeStep(
fun ImportBundleStep(
bundleType: BundleType,
patchBundle: Uri?,
- integrations: Uri?,
remoteUrl: String,
autoUpdate: Boolean,
launchPatchActivity: () -> Unit,
- launchIntegrationsActivity: () -> Unit,
onRemoteUrlChange: (String) -> Unit,
onAutoUpdateChange: (Boolean) -> Unit
) {
@@ -210,19 +192,8 @@ fun ImportBundleStep(
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
}
},
- modifier = Modifier.clickable { launchPatchActivity() }
- )
- ListItem(
- headlineContent = {
- Text(stringResource(R.string.integrations_field))
- },
- supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) },
- trailingContent = {
- IconButton(onClick = launchIntegrationsActivity) {
- Icon(imageVector = Icons.Default.Topic, contentDescription = null)
- }
- },
- modifier = Modifier.clickable { launchIntegrationsActivity() }
+ modifier = Modifier.clickable { launchPatchActivity() },
+ colors = transparentListItemColors
)
}
}
@@ -256,6 +227,7 @@ fun ImportBundleStep(
)
}
},
+ colors = transparentListItemColors
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
index bb667bad..b86124d9 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.haptics.HapticRadioButton
+import app.revanced.manager.util.transparentListItemColors
@Composable
fun InstallPickerDialog(
@@ -41,7 +42,7 @@ fun InstallPickerDialog(
title = { Text(stringResource(R.string.select_install_type)) },
text = {
Column {
- InstallType.values().forEach {
+ InstallType.entries.forEach {
ListItem(
modifier = Modifier.clickable { selectedInstallType = it },
leadingContent = {
@@ -50,7 +51,8 @@ fun InstallPickerDialog(
onClick = null
)
},
- headlineContent = { Text(stringResource(it.stringResource)) }
+ headlineContent = { Text(stringResource(it.stringResource)) },
+ colors = transparentListItemColors
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
index 993270ea..3c0504bc 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
@@ -50,6 +50,7 @@ import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.saver.snapshotStateSetSaver
import app.revanced.manager.util.toast
+import app.revanced.manager.util.transparentListItemColors
import kotlinx.parcelize.Parcelize
import org.koin.compose.koinInject
import org.koin.core.component.KoinComponent
@@ -58,6 +59,7 @@ import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState
import java.io.Serializable
import kotlin.random.Random
+import kotlin.reflect.typeOf
import androidx.compose.ui.window.Dialog as ComposeDialog
private class OptionEditorScope(
@@ -96,17 +98,17 @@ private interface OptionEditor {
fun Dialog(scope: OptionEditorScope)
}
+private inline fun OptionEditor.toMapEditorElements() = arrayOf(
+ typeOf() to this,
+ typeOf>() to ListOptionEditor(this)
+)
+
private val optionEditors = mapOf(
- "Boolean" to BooleanOptionEditor,
- "String" to StringOptionEditor,
- "Int" to IntOptionEditor,
- "Long" to LongOptionEditor,
- "Float" to FloatOptionEditor,
- "BooleanArray" to ListOptionEditor(BooleanOptionEditor),
- "StringArray" to ListOptionEditor(StringOptionEditor),
- "IntArray" to ListOptionEditor(IntOptionEditor),
- "LongArray" to ListOptionEditor(LongOptionEditor),
- "FloatArray" to ListOptionEditor(FloatOptionEditor),
+ *BooleanOptionEditor.toMapEditorElements(),
+ *StringOptionEditor.toMapEditorElements(),
+ *IntOptionEditor.toMapEditorElements(),
+ *LongOptionEditor.toMapEditorElements(),
+ *FloatOptionEditor.toMapEditorElements()
)
@Composable
@@ -145,7 +147,7 @@ fun OptionItem(option: Option, value: T?, setValue: (T?) -> Unit) {
val baseOptionEditor =
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor
- if (option.type != "Boolean" && option.presets != null) PresetOptionEditor(baseOptionEditor)
+ if (option.type != typeOf() && option.presets != null) PresetOptionEditor(baseOptionEditor)
else baseOptionEditor
}
@@ -405,7 +407,8 @@ private class PresetOptionEditor(private val innerEditor: OptionEditor<
selected = selectedPreset == presetKey,
onClick = { selectedPreset = presetKey }
)
- }
+ },
+ colors = transparentListItemColors
)
}
@@ -430,7 +433,7 @@ private class ListOptionEditor(private val elementEditor: Opti
option.key,
option.description,
option.required,
- option.type.removeSuffix("Array"),
+ option.type.arguments.first().type!!,
null,
null
) { true }
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
index 6d2e1d5f..eb814c3b 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
@@ -33,6 +33,7 @@ import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE
+import app.revanced.manager.util.transparentListItemColors
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -110,9 +111,9 @@ fun AppSelectorScreen(
)
)
}
- }
+ },
+ colors = transparentListItemColors
)
-
}
}
} else {
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
index 157986ec..51caeb25 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
@@ -79,9 +79,9 @@ fun DashboardScreen(
if (showAddBundleDialog) {
ImportPatchBundleDialog(
onDismiss = { showAddBundleDialog = false },
- onLocalSubmit = { patches, integrations ->
+ onLocalSubmit = { patches ->
showAddBundleDialog = false
- vm.createLocalSource(patches, integrations)
+ vm.createLocalSource(patches)
},
onRemoteSubmit = { url, autoUpdate ->
showAddBundleDialog = false
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
index 511ede21..511a1c36 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
@@ -46,6 +46,7 @@ import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp
+import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -480,7 +481,8 @@ private fun ListHeader(
)
}
}
- }
+ },
+ colors = transparentListItemColors
)
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
index cd18dcc7..a8d12d50 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
@@ -104,7 +104,7 @@ fun VersionSelectorScreen(
viewModel.installedApp?.let { (packageInfo, installedApp) ->
SelectedApp.Installed(
packageName = viewModel.packageName,
- version = packageInfo.versionName
+ version = packageInfo.versionName!!
).let {
item {
SelectedAppItem(
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
index 9082f4bb..cb474f09 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt
@@ -102,12 +102,6 @@ fun AdvancedSettingsScreen(
headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description,
)
- BooleanItem(
- preference = vm.prefs.multithreadingDexFileWriter,
- coroutineScope = vm.viewModelScope,
- headline = R.string.multithreaded_dex_file_writer,
- description = R.string.multithreaded_dex_file_writer_description,
- )
GroupHeader(stringResource(R.string.safeguards))
BooleanItem(
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
index 85cee8d1..2acfdcd5 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
@@ -69,7 +69,7 @@ class AppSelectorViewModel(
pm.getPackageInfo(this)?.let { packageInfo ->
SelectedApp.Local(
packageName = packageInfo.packageName,
- version = packageInfo.versionName,
+ version = packageInfo.versionName!!,
file = this,
temporary = true
)
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
index ce68249d..5a019c51 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
@@ -95,13 +95,10 @@ class DashboardViewModel(
selectedSources.clear()
}
- fun createLocalSource(patchBundle: Uri, integrations: Uri?) =
+ fun createLocalSource(patchBundle: Uri) =
viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
- integrations?.let { contentResolver.openInputStream(it) }
- .use { integrationsStream ->
- patchBundleRepository.createLocal(patchesStream, integrationsStream)
- }
+ patchBundleRepository.createLocal(patchesStream)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
index a63feed7..00c1231f 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
@@ -152,8 +152,8 @@ class PatcherViewModel(
)
val patcherSucceeded =
- workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
- when (workInfo.state) {
+ workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo? ->
+ when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.FAILED -> false
else -> null
@@ -308,7 +308,7 @@ class PatcherViewModel(
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk
- if (currentPackageInfo.splitNames != null) {
+ if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package
installerStatusDialogModel.packageInstallerStatus =
PackageInstaller.STATUS_FAILURE_INVALID
diff --git a/app/src/main/java/app/revanced/manager/util/Constants.kt b/app/src/main/java/app/revanced/manager/util/Constants.kt
index 983a7c42..000da463 100644
--- a/app/src/main/java/app/revanced/manager/util/Constants.kt
+++ b/app/src/main/java/app/revanced/manager/util/Constants.kt
@@ -1,14 +1,8 @@
package app.revanced.manager.util
-private const val team = "revanced"
-const val ghOrganization = "https://github.com/$team"
-const val ghCli = "$team/revanced-cli"
-const val ghPatches = "$team/revanced-patches"
-const val ghPatcher = "$team/revanced-patcher"
-const val ghManager = "$team/revanced-manager"
-const val ghIntegrations = "$team/revanced-integrations"
const val tag = "ReVanced Manager"
const val JAR_MIMETYPE = "application/java-archive"
const val APK_MIMETYPE = "application/vnd.android.package-archive"
-const val JSON_MIMETYPE = "application/json"
\ No newline at end of file
+const val JSON_MIMETYPE = "application/json"
+const val BIN_MIMETYPE = "application/octet-stream"
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt
index 0d7a822b..d0e6dbb4 100644
--- a/app/src/main/java/app/revanced/manager/util/PM.kt
+++ b/app/src/main/java/app/revanced/manager/util/PM.kt
@@ -106,7 +106,7 @@ class PM(
val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null
// This is needed in order to load label and icon.
- pkgInfo.applicationInfo.apply {
+ pkgInfo.applicationInfo!!.apply {
sourceDir = path
publicSourceDir = path
}
@@ -114,7 +114,7 @@ class PM(
return pkgInfo
}
- fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
+ fun PackageInfo.label() = this.applicationInfo!!.loadLabel(app.packageManager).toString()
fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo)
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index bc48c54a..8fb13c88 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -13,6 +13,8 @@ import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material3.ListItemColors
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.State
@@ -240,4 +242,14 @@ fun ((T) -> R).withHapticFeedback(constant: Int): (T) -> R {
view.performHapticFeedback(constant)
this(it)
}
-}
\ No newline at end of file
+}
+
+private var transparentListItemColorsCached: ListItemColors? = null
+
+/**
+ * The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
+ */
+val transparentListItemColors
+ @Composable get() = transparentListItemColorsCached
+ ?: ListItemDefaults.colors(containerColor = Color.Transparent)
+ .also { transparentListItemColorsCached = it }
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0b260bb8..20722c02 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,7 +2,6 @@
ReVanced Manager
Patcher
Patches
- Integrations
CLI
Manager
@@ -20,7 +19,6 @@
Import patch bundle
Bundle patches
Patch bundle
- Integrations
Selected
Not selected
@@ -71,8 +69,6 @@
Adapt colors to the wallpaper
Theme
Choose between light or dark theme
- Multi-threaded DEX file writer
- Use multiple cores to write DEX files. This is faster, but uses more memory
Safeguards
Disable version compatibility check
The check restricts patches to supported app versions
@@ -123,7 +119,6 @@
Search apps…
Loading…
Downloading patch bundle…
- Downloading Integrations…
Options
OK
@@ -186,7 +181,6 @@
Download APK file
Failed to download patch bundle: %s
Failed to load updated patch bundle: %s
- Failed to update integrations: %s
No patched apps found
Tap on the patches to get more information about them
%s selected
diff --git a/build.gradle.kts b/build.gradle.kts
index 89d27215..ca1372dd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,5 +2,8 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.devtools) apply false
alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.about.libraries) apply false
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1a3f425a..7c4dafcc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,31 +1,30 @@
[versions]
-ktx = "1.13.1"
-material3 = "1.2.1"
-ui-tooling = "1.6.8"
-viewmodel-lifecycle = "2.8.3"
+ktx = "1.15.0"
+material3 = "1.3.1"
+ui-tooling = "1.7.5"
+viewmodel-lifecycle = "2.8.7"
splash-screen = "1.0.1"
-compose-activity = "1.9.0"
-paging = "3.3.0"
+compose-activity = "1.9.3"
preferences-datastore = "1.1.1"
-work-runtime = "2.9.0"
-compose-bom = "2024.06.00"
+work-runtime = "2.10.0"
+compose-bom = "2024.10.01"
accompanist = "0.34.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
-serialization = "1.6.3"
-collection = "0.3.7"
+serialization = "1.7.3"
+collection = "0.3.8"
room-version = "2.6.1"
-revanced-patcher = "19.3.1"
-revanced-library = "2.2.1"
+revanced-patcher = "21.0.0"
+revanced-library = "3.0.2"
koin-version = "3.5.3"
koin-version-compose = "3.5.3"
reimagined-navigation = "1.5.0"
ktor = "2.3.9"
markdown-renderer = "0.22.0"
fading-edges = "1.0.4"
-android-gradle-plugin = "8.3.2"
-kotlin-gradle-plugin = "1.9.22"
-dev-tools-gradle-plugin = "1.9.22-1.0.17"
+kotlin = "2.0.21"
+android-gradle-plugin = "8.7.2"
+dev-tools-gradle-plugin = "2.0.21-1.0.27"
about-libraries-gradle-plugin = "11.1.1"
coil = "2.6.0"
app-icon-loader-coil = "1.5.0"
@@ -44,7 +43,6 @@ runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", ve
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" }
-paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "paging" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
@@ -135,6 +133,9 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-gradle-plugin" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a80b22ce..dfe2d1c1 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
+#Tue Nov 12 21:36:50 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
From 0685479d5362f898e694524b48a0eae63f048d75 Mon Sep 17 00:00:00 2001
From: somni <82272900+somnisomni@users.noreply.github.com>
Date: Fri, 22 Nov 2024 22:57:23 +0900
Subject: [PATCH 6/9] feat: Make patch bundles list scrollable (#2322)
---
.../manager/ui/screen/BundleListScreen.kt | 54 +++++++++++++++++++
.../manager/ui/screen/DashboardScreen.kt | 39 ++++----------
2 files changed, 65 insertions(+), 28 deletions(-)
create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
new file mode 100644
index 00000000..c2758e71
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
@@ -0,0 +1,54 @@
+package app.revanced.manager.ui.screen
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import app.revanced.manager.domain.bundles.PatchBundleSource
+import app.revanced.manager.ui.component.LazyColumnWithScrollbar
+import app.revanced.manager.ui.component.bundle.BundleItem
+
+@Composable
+fun BundleListScreen(
+ onDelete: (PatchBundleSource) -> Unit,
+ onUpdate: (PatchBundleSource) -> Unit,
+ sources: List,
+ selectedSources: SnapshotStateList,
+ bundlesSelectable: Boolean,
+) {
+ LazyColumnWithScrollbar(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ ) {
+ items(
+ sources,
+ key = { it.uid }
+ ) { source ->
+ BundleItem(
+ bundle = source,
+ onDelete = {
+ onDelete(source)
+ },
+ onUpdate = {
+ onUpdate(source)
+ },
+ selectable = bundlesSelectable,
+ onSelect = {
+ selectedSources.add(source)
+ },
+ isBundleSelected = selectedSources.contains(source),
+ toggleSelection = { bundleIsNotSelected ->
+ if (bundleIsNotSelected) {
+ selectedSources.add(source)
+ } else {
+ selectedSources.remove(source)
+ }
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
index 51caeb25..4dc74766 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
@@ -31,7 +31,6 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard
-import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
@@ -264,33 +263,17 @@ fun DashboardScreen(
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
- Column(
- modifier = Modifier.fillMaxSize(),
- ) {
- sources.forEach {
- BundleItem(
- bundle = it,
- onDelete = {
- vm.delete(it)
- },
- onUpdate = {
- vm.update(it)
- },
- selectable = bundlesSelectable,
- onSelect = {
- vm.selectedSources.add(it)
- },
- isBundleSelected = vm.selectedSources.contains(it),
- toggleSelection = { bundleIsNotSelected ->
- if (bundleIsNotSelected) {
- vm.selectedSources.add(it)
- } else {
- vm.selectedSources.remove(it)
- }
- }
- )
- }
- }
+ BundleListScreen(
+ onDelete = {
+ vm.delete(it)
+ },
+ onUpdate = {
+ vm.update(it)
+ },
+ sources = sources,
+ selectedSources = vm.selectedSources,
+ bundlesSelectable = bundlesSelectable
+ )
}
}
}
From 31fb8b14049a5b7c31849cadd48e3b828cb1eb97 Mon Sep 17 00:00:00 2001
From: Pun Butrach
Date: Sun, 1 Dec 2024 01:13:03 +0700
Subject: [PATCH 7/9] chore: Nitpick on misspelling of comment
---
settings.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 79364a6e..f66506b8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -15,7 +15,7 @@ dependencyResolutionManagement {
maven("https://jitpack.io")
mavenLocal()
maven {
- // A repository must be speficied for some reason. "registry" is a dummy.
+ // A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
From 9dc716b1c80dd9105ca9e8ee9de9dd6e60d52aae Mon Sep 17 00:00:00 2001
From: Ax333l
Date: Thu, 12 Dec 2024 17:52:21 +0100
Subject: [PATCH 8/9] feat: switch to revanced api v4
---
app/build.gradle.kts | 1 +
.../app/revanced/manager/di/ServiceModule.kt | 2 -
.../domain/bundles/RemotePatchBundle.kt | 20 ++---
.../manager/network/api/ReVancedAPI.kt | 44 +++++-----
.../manager/network/dto/GithubChangelog.kt | 16 ----
.../manager/network/dto/PatchBundleInfo.kt | 7 --
.../manager/network/dto/ReVancedAsset.kt | 18 ++++
.../network/dto/ReVancedContributors.kt | 10 +--
.../manager/network/dto/ReVancedInfo.kt | 9 +-
.../manager/network/dto/ReVancedRelease.kt | 41 ---------
.../network/service/ReVancedService.kt | 43 ----------
.../ui/component/settings/Changelog.kt | 5 --
.../settings/update/ChangelogsScreen.kt | 59 +++----------
.../ui/screen/settings/update/UpdateScreen.kt | 24 +++---
.../ui/viewmodel/ChangelogsViewModel.kt | 24 ++----
.../manager/ui/viewmodel/UpdateViewModel.kt | 37 ++-------
.../java/app/revanced/manager/util/Util.kt | 83 ++++++++-----------
gradle/libs.versions.toml | 2 +
18 files changed, 125 insertions(+), 320 deletions(-)
delete mode 100644 app/src/main/java/app/revanced/manager/network/dto/GithubChangelog.kt
delete mode 100644 app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
create mode 100644 app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt
delete mode 100644 app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
delete mode 100644 app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9f04a1b9..79b726bb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -143,6 +143,7 @@ dependencies {
// KotlinX
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collection.immutable)
+ implementation(libs.kotlinx.datetime)
// Room
implementation(libs.room.runtime)
diff --git a/app/src/main/java/app/revanced/manager/di/ServiceModule.kt b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
index c30a711f..cfda5030 100644
--- a/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt
@@ -1,11 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.network.service.HttpService
-import app.revanced.manager.network.service.ReVancedService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
- singleOf(::ReVancedService)
singleOf(::HttpService)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
index e3214db9..9deb7bbe 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
@@ -2,7 +2,7 @@ package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
import app.revanced.manager.network.api.ReVancedAPI
-import app.revanced.manager.network.dto.PatchBundleInfo
+import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import io.ktor.client.request.url
@@ -16,17 +16,16 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
PatchBundleSource(name, id, directory) {
protected val http: HttpService by inject()
- protected abstract suspend fun getLatestInfo(): PatchBundleInfo
+ protected abstract suspend fun getLatestInfo(): ReVancedAsset
- private suspend fun download(info: PatchBundleInfo) = withContext(Dispatchers.IO) {
- val (version, url) = info
+ private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
patchBundleOutputStream().use {
http.streamTo(it) {
- url(url)
+ url(info.downloadUrl)
}
}
- saveVersion(version)
+ saveVersion(info.version)
reload()
}
@@ -58,7 +57,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
- http.request {
+ http.request {
url(endpoint)
}.getOrThrow()
}
@@ -68,10 +67,5 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedAPI by inject()
- override suspend fun getLatestInfo() = api
- .getLatestRelease("revanced-patches")
- .getOrThrow()
- .let {
- PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl)
- }
+ override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt
index f52a8190..bb365580 100644
--- a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt
+++ b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt
@@ -2,37 +2,41 @@ package app.revanced.manager.network.api
import android.os.Build
import app.revanced.manager.domain.manager.PreferencesManager
-import app.revanced.manager.network.dto.ReVancedRelease
-import app.revanced.manager.network.service.ReVancedService
+import app.revanced.manager.network.dto.ReVancedAsset
+import app.revanced.manager.network.dto.ReVancedGitRepository
+import app.revanced.manager.network.dto.ReVancedInfo
+import app.revanced.manager.network.service.HttpService
+import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.network.utils.getOrThrow
-import app.revanced.manager.network.utils.transform
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import io.ktor.client.request.url
class ReVancedAPI(
- private val service: ReVancedService,
+ private val client: HttpService,
private val prefs: PreferencesManager
) {
private suspend fun apiUrl() = prefs.api.get()
- suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
+ private suspend inline fun request(api: String, route: String): APIResponse =
+ withContext(
+ Dispatchers.IO
+ ) {
+ client.request {
+ url("$api/v4/$route")
+ }
+ }
- suspend fun getLatestRelease(name: String) =
- service.getLatestRelease(apiUrl(), name).transform { it.release }
-
- suspend fun getReleases(name: String) =
- service.getReleases(apiUrl(), name).transform { it.releases }
+ private suspend inline fun request(route: String) = request(apiUrl(), route)
suspend fun getAppUpdate() =
- getLatestRelease("revanced-manager")
- .getOrThrow()
- .takeIf { it.version != Build.VERSION.RELEASE }
+ getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
- suspend fun getInfo(api: String? = null) = service.getInfo(api ?: apiUrl()).transform { it.info }
+ suspend fun getLatestAppInfo() = request("manager")
+ suspend fun getPatchesUpdate() = request("patches")
- companion object Extensions {
- fun ReVancedRelease.findAssetByType(mime: String) =
- assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
- }
-}
+ suspend fun getContributors() = request>("contributors")
-class MissingAssetException(type: String) : Exception("No asset with type $type")
\ No newline at end of file
+ suspend fun getInfo(api: String? = null) = request(api ?: apiUrl(), "about")
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/GithubChangelog.kt b/app/src/main/java/app/revanced/manager/network/dto/GithubChangelog.kt
deleted file mode 100644
index 52789017..00000000
--- a/app/src/main/java/app/revanced/manager/network/dto/GithubChangelog.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package app.revanced.manager.network.dto
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class GithubChangelog(
- @SerialName("tag_name") val version: String,
- @SerialName("body") val body: String,
- @SerialName("assets") val assets: List
-)
-
-@Serializable
-data class GithubAsset(
- @SerialName("download_count") val downloadCount: Int,
-)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
deleted file mode 100644
index 02d89919..00000000
--- a/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package app.revanced.manager.network.dto
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-// TODO: replace this
-data class PatchBundleInfo(val version: String, val url: String)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt
new file mode 100644
index 00000000..64c05f31
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt
@@ -0,0 +1,18 @@
+package app.revanced.manager.network.dto
+
+import kotlinx.datetime.LocalDateTime
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ReVancedAsset (
+ @SerialName("download_url")
+ val downloadUrl: String,
+ @SerialName("created_at")
+ val createdAt: LocalDateTime,
+ @SerialName("signature_download_url")
+ val signatureDownloadUrl: String? = null,
+ val description: String,
+ val version: String,
+)
+
diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt
index 82117d96..6583ba7c 100644
--- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt
+++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt
@@ -3,19 +3,15 @@ package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-@Serializable
-data class ReVancedGitRepositories(
- val repositories: List,
-)
-
@Serializable
data class ReVancedGitRepository(
val name: String,
+ val url: String,
val contributors: List,
)
@Serializable
data class ReVancedContributor(
- @SerialName("login") val username: String,
+ @SerialName("name") val username: String,
@SerialName("avatar_url") val avatarUrl: String,
-)
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt
index 8f7e8966..89ed7445 100644
--- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt
+++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt
@@ -1,12 +1,8 @@
package app.revanced.manager.network.dto
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-@Serializable
-data class ReVancedInfoParent(
- val info: ReVancedInfo,
-)
-
@Serializable
data class ReVancedInfo(
val name: String,
@@ -43,7 +39,8 @@ data class ReVancedDonation(
@Serializable
data class ReVancedWallet(
val network: String,
- val currency_code: String,
+ @SerialName("currency_code")
+ val currencyCode: String,
val address: String,
val preferred: Boolean
)
diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
deleted file mode 100644
index 442e4107..00000000
--- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package app.revanced.manager.network.dto
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class ReVancedLatestRelease(
- val release: ReVancedRelease,
-)
-
-@Serializable
-data class ReVancedReleases(
- val releases: List
-)
-
-@Serializable
-data class ReVancedRelease(
- val metadata: ReVancedReleaseMeta,
- val assets: List
-) {
- val version get() = metadata.tag
-}
-
-@Serializable
-data class ReVancedReleaseMeta(
- @SerialName("tag_name") val tag: String,
- val name: String,
- val draft: Boolean,
- val prerelease: Boolean,
- @SerialName("created_at") val createdAt: String,
- @SerialName("published_at") val publishedAt: String,
- val body: String,
-)
-
-@Serializable
-data class Asset(
- val name: String,
- @SerialName("download_count") val downloadCount: Int,
- @SerialName("browser_download_url") val downloadUrl: String,
- @SerialName("content_type") val contentType: String
-)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt b/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt
deleted file mode 100644
index 537a3514..00000000
--- a/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package app.revanced.manager.network.service
-
-import app.revanced.manager.network.dto.ReVancedGitRepositories
-import app.revanced.manager.network.dto.ReVancedInfo
-import app.revanced.manager.network.dto.ReVancedInfoParent
-import app.revanced.manager.network.dto.ReVancedLatestRelease
-import app.revanced.manager.network.dto.ReVancedReleases
-import app.revanced.manager.network.utils.APIResponse
-import io.ktor.client.request.url
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-class ReVancedService(
- private val client: HttpService,
-) {
- suspend fun getLatestRelease(api: String, repo: String): APIResponse =
- withContext(Dispatchers.IO) {
- client.request {
- url("$api/v2/$repo/releases/latest")
- }
- }
-
- suspend fun getReleases(api: String, repo: String): APIResponse =
- withContext(Dispatchers.IO) {
- client.request {
- url("$api/v2/$repo/releases")
- }
- }
-
- suspend fun getContributors(api: String): APIResponse =
- withContext(Dispatchers.IO) {
- client.request {
- url("$api/contributors")
- }
- }
-
- suspend fun getInfo(api: String): APIResponse =
- withContext(Dispatchers.IO) {
- client.request {
- url("$api/v2/info")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt
index 6e707ae2..af26e232 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt
@@ -26,7 +26,6 @@ import app.revanced.manager.ui.component.Markdown
fun Changelog(
markdown: String,
version: String,
- downloadCount: String,
publishDate: String
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -55,10 +54,6 @@ fun Changelog(
modifier = Modifier
.fillMaxWidth()
) {
- Tag(
- Icons.Outlined.FileDownload,
- downloadCount
- )
Tag(
Icons.Outlined.CalendarToday,
publishDate
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt
index 315bc1a1..eadd9990 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt
@@ -5,12 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -19,11 +15,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
-import app.revanced.manager.ui.component.LazyColumnWithScrollbar
+import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
-import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime
import org.koin.androidx.compose.koinViewModel
@@ -33,8 +28,6 @@ fun ChangelogsScreen(
onBackClick: () -> Unit,
vm: ChangelogsViewModel = koinViewModel()
) {
- val changelogs = vm.changelogs
-
Scaffold(
topBar = {
AppTopBar(
@@ -43,54 +36,22 @@ fun ChangelogsScreen(
)
}
) { paddingValues ->
- LazyColumnWithScrollbar(
+ ColumnWithScrollbar(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = if (changelogs.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
+ verticalArrangement = if (vm.releaseInfo == null) Arrangement.Center else Arrangement.Top
) {
- if (changelogs == null) {
- item {
- LoadingIndicator()
- }
- } else if (changelogs.isEmpty()) {
- item {
- Text(
- text = stringResource(id = R.string.no_changelogs_found),
- style = MaterialTheme.typography.titleLarge
+ vm.releaseInfo?.let { info ->
+ Column(modifier = Modifier.padding(16.dp)) {
+ Changelog(
+ markdown = info.description.replace("`", ""),
+ version = info.version,
+ publishDate = info.createdAt.relativeTime(LocalContext.current)
)
}
- } else {
- val lastChangelog = changelogs.last()
- items(
- changelogs,
- key = { it.version }
- ) { changelog ->
- ChangelogItem(changelog, lastChangelog)
- }
- }
- }
- }
-}
-
-@Composable
-fun ChangelogItem(
- changelog: ChangelogsViewModel.Changelog,
- lastChangelog: ChangelogsViewModel.Changelog
-) {
- Column(modifier = Modifier.padding(16.dp)) {
- Changelog(
- markdown = changelog.body.replace("`", ""),
- version = changelog.version,
- downloadCount = changelog.downloadCount.formatNumber(),
- publishDate = changelog.publishDate.relativeTime(LocalContext.current)
- )
- if (changelog != lastChangelog) {
- HorizontalDivider(
- modifier = Modifier.padding(top = 32.dp),
- color = MaterialTheme.colorScheme.outlineVariant
- )
+ } ?: LoadingIndicator()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt
index 4b41dbd9..693adc6a 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt
@@ -33,12 +33,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R
+import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel
-import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
-import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime
import com.gigamole.composefadingedges.content.FadingEdgesContentType
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
@@ -77,10 +76,10 @@ fun UpdateScreen(
) {
Header(
vm.state,
- vm.changelog,
+ vm.releaseInfo,
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
)
- vm.changelog?.let { changelog ->
+ vm.releaseInfo?.let { changelog ->
HorizontalDivider()
Changelog(changelog)
} ?: Spacer(modifier = Modifier.weight(1f))
@@ -118,7 +117,7 @@ private fun MeteredDownloadConfirmationDialog(
}
@Composable
-private fun Header(state: State, changelog: Changelog?, downloadData: DownloadData) {
+private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
text = stringResource(state.title),
@@ -134,11 +133,11 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
- changelog?.let { changelog ->
+ releaseInfo?.version?.let {
Text(
text = stringResource(
- id = R.string.new_version,
- changelog.version.replace("v", "")
+ R.string.new_version,
+ it.replace("v", "")
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -170,7 +169,7 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
}
@Composable
-private fun ColumnScope.Changelog(changelog: Changelog) {
+private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
@@ -194,10 +193,9 @@ private fun ColumnScope.Changelog(changelog: Changelog) {
)
) {
Changelog(
- markdown = changelog.body.replace("`", ""),
- version = changelog.version,
- downloadCount = changelog.downloadCount.formatNumber(),
- publishDate = changelog.publishDate.relativeTime(LocalContext.current)
+ markdown = releaseInfo.description.replace("`", ""),
+ version = releaseInfo.version,
+ publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt
index aa9c878c..bfe1bbfa 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt
@@ -8,9 +8,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.network.api.ReVancedAPI
-import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
-import app.revanced.manager.network.utils.getOrNull
-import app.revanced.manager.util.APK_MIMETYPE
+import app.revanced.manager.network.dto.ReVancedAsset
+import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
@@ -18,27 +17,14 @@ class ChangelogsViewModel(
private val api: ReVancedAPI,
private val app: Application,
) : ViewModel() {
- var changelogs: List? by mutableStateOf(null)
+ var releaseInfo: ReVancedAsset? by mutableStateOf(null)
+ private set
init {
viewModelScope.launch {
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
- changelogs = api.getReleases("revanced-manager").getOrNull().orEmpty().map { release ->
- Changelog(
- release.version,
- release.findAssetByType(APK_MIMETYPE).downloadCount,
- release.metadata.publishedAt,
- release.metadata.body
- )
- }
+ releaseInfo = api.getLatestAppInfo().getOrThrow()
}
}
}
-
- data class Changelog(
- val version: String,
- val downloadCount: Int,
- val publishDate: String,
- val body: String,
- )
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
index 5ea4db74..f4dc457f 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
-import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -19,16 +18,10 @@ import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI
-import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
-import app.revanced.manager.network.dto.ReVancedRelease
+import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService
-import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.service.InstallService
-import app.revanced.manager.service.UninstallService
-import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.PM
-import app.revanced.manager.util.simpleMessage
-import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
@@ -38,7 +31,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
-import java.io.File
class UpdateViewModel(
private val downloadOnScreenEntry: Boolean
@@ -65,23 +57,14 @@ class UpdateViewModel(
var installError by mutableStateOf("")
- var changelog: Changelog? by mutableStateOf(null)
+ var releaseInfo: ReVancedAsset? by mutableStateOf(null)
+ private set
private val location = fs.tempDir.resolve("updater.apk")
- private var release: ReVancedRelease? = null
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
- withContext(Dispatchers.IO) {
- val response = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
+ releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
- release = response
- changelog = Changelog(
- response.version,
- response.findAssetByType(APK_MIMETYPE).downloadCount,
- response.metadata.publishedAt,
- response.metadata.body
- )
- }
if (downloadOnScreenEntry) {
downloadUpdate()
} else {
@@ -92,16 +75,15 @@ class UpdateViewModel(
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
+ val release = releaseInfo!!
withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
showInternetCheckDialog = true
} else {
state = State.DOWNLOADING
- val asset = release?.findAssetByType(APK_MIMETYPE)
- ?: throw Exception("couldn't find asset to download")
http.download(location) {
- url(asset.downloadUrl)
+ url(release.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
@@ -153,13 +135,6 @@ class UpdateViewModel(
location.delete()
}
- data class Changelog(
- val version: String,
- val downloadCount: Int,
- val publishDate: String,
- val body: String,
- )
-
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update, true),
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index 8fb13c88..5cb379e3 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -3,11 +3,6 @@ package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
-import android.icu.number.Notation
-import android.icu.number.NumberFormatter
-import android.icu.number.Precision
-import android.icu.text.CompactDecimalFormat
-import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
@@ -40,11 +35,13 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
-import java.time.Duration
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import java.time.format.DateTimeParseException
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.format.MonthNames
+import kotlinx.datetime.format.char
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
import java.util.Locale
typealias PatchSelection = Map>
@@ -134,53 +131,43 @@ suspend fun Flow>.collectEach(block: suspend (T) -> Unit) {
}
}
-fun Int.formatNumber(): String {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- NumberFormatter.with()
- .notation(Notation.compactShort())
- .decimal(NumberFormatter.DecimalSeparatorDisplay.ALWAYS)
- .precision(Precision.fixedFraction(1))
- .locale(Locale.getDefault())
- .format(this)
- .toString()
- } else {
- val compact = CompactDecimalFormat.getInstance(
- Locale.getDefault(), CompactDecimalFormat.CompactStyle.SHORT
- )
- compact.maximumFractionDigits = 1
- compact.format(this)
- }
-}
-
-fun String.relativeTime(context: Context): String {
+fun LocalDateTime.relativeTime(context: Context): String {
try {
- val currentTime = ZonedDateTime.now(ZoneId.of("UTC"))
- val inputDateTime = ZonedDateTime.parse(this)
- val duration = Duration.between(inputDateTime, currentTime)
+ val now = Clock.System.now()
+ val duration = now - this.toInstant(TimeZone.UTC)
return when {
- duration.toMinutes() < 1 -> context.getString(R.string.just_now)
- duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString())
- duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString())
- duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString())
- else -> {
- val formatter = DateTimeFormatter.ofPattern("MMM d")
- val formattedDate = inputDateTime.format(formatter)
- if (inputDateTime.year != currentTime.year) {
- val yearFormatter = DateTimeFormatter.ofPattern(", yyyy")
- val formattedYear = inputDateTime.format(yearFormatter)
- "$formattedDate$formattedYear"
- } else {
- formattedDate
+ duration.inWholeMinutes < 1 -> context.getString(R.string.just_now)
+ duration.inWholeMinutes < 60 -> context.getString(
+ R.string.minutes_ago,
+ duration.inWholeMinutes.toString()
+ )
+
+ duration.inWholeHours < 24 -> context.getString(
+ R.string.hours_ago,
+ duration.inWholeHours.toString()
+ )
+
+ duration.inWholeHours < 30 -> context.getString(
+ R.string.days_ago,
+ duration.inWholeDays.toString()
+ )
+
+ else -> LocalDateTime.Format {
+ monthName(MonthNames.ENGLISH_ABBREVIATED)
+ char(' ')
+ dayOfMonth()
+ if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
+ chars(", ")
+ year()
}
- }
+ }.format(this)
}
- } catch (e: DateTimeParseException) {
+ } catch (e: IllegalArgumentException) {
return context.getString(R.string.invalid_date)
}
}
-
const val isScrollingUpSensitivity = 10
@Composable
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7c4dafcc..47d9b401 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,6 +13,7 @@ placeholder = "1.1.2"
reorderable = "1.5.2"
serialization = "1.7.3"
collection = "0.3.8"
+datetime = "0.6.0"
room-version = "2.6.1"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
@@ -68,6 +69,7 @@ placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-mate
# Kotlinx
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
kotlinx-collection-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collection" }
+kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }
From 2ec1c0238dd80548afb5e36f1bee0b1779ad2445 Mon Sep 17 00:00:00 2001
From: Ax333l
Date: Thu, 19 Dec 2024 21:41:04 +0100
Subject: [PATCH 9/9] feat: Add downloader plugin system (#2041)
---
app/build.gradle.kts | 8 +-
app/proguard-rules.pro | 4 +
.../1.json | 38 ++-
app/src/main/AndroidManifest.xml | 19 +-
.../java/app/revanced/manager/MainActivity.kt | 56 +--
.../revanced/manager/ManagerApplication.kt | 6 +
.../revanced/manager/data/room/AppDatabase.kt | 8 +-
.../room/apps/downloaded/DownloadedApp.kt | 1 +
.../room/apps/downloaded/DownloadedAppDao.kt | 8 +-
.../room/plugins/TrustedDownloaderPlugin.kt | 11 +
.../plugins/TrustedDownloaderPluginDao.kt | 22 ++
.../revanced/manager/di/RepositoryModule.kt | 1 +
.../revanced/manager/di/ViewModelModule.kt | 1 -
.../domain/manager/PreferencesManager.kt | 4 +-
.../manager/base/BasePreferencesManager.kt | 17 +
.../repository/DownloadedAppRepository.kt | 112 ++++--
.../repository/DownloaderPluginRepository.kt | 168 +++++++++
.../manager/network/downloader/APKMirror.kt | 277 ---------------
.../network/downloader/AppDownloader.kt | 27 --
.../downloader/DownloaderPluginState.kt | 9 +
.../downloader/LoadedDownloaderPlugin.kt | 15 +
.../downloader/ParceledDownloaderData.kt | 45 +++
.../manager/patcher/patch/PatchInfo.kt | 9 +-
.../manager/patcher/worker/PatcherWorker.kt | 80 ++++-
.../ui/component/ExceptionViewerDialog.kt | 79 +++++
.../manager/ui/component/SearchView.kt | 49 +--
.../bundle/BundleInformationDialog.kt | 66 +---
.../manager/ui/component/patcher/Steps.kt | 18 +-
.../ui/component/settings/SettingsListItem.kt | 25 +-
.../manager/ui/destination/Destination.kt | 3 -
.../destination/SelectedAppInfoDestination.kt | 3 -
.../revanced/manager/ui/model/BundleInfo.kt | 4 +-
.../revanced/manager/ui/model/PatcherStep.kt | 2 +-
.../revanced/manager/ui/model/SelectedApp.kt | 29 +-
.../manager/ui/screen/AppSelectorScreen.kt | 31 +-
.../manager/ui/screen/DashboardScreen.kt | 22 +-
.../ui/screen/InstalledAppInfoScreen.kt | 7 +-
.../manager/ui/screen/PatcherScreen.kt | 34 ++
.../ui/screen/PatchesSelectorScreen.kt | 22 +-
.../ui/screen/SelectedAppInfoScreen.kt | 318 ++++++++++++------
.../ui/screen/VersionSelectorScreen.kt | 201 -----------
.../settings/DownloadsSettingsScreen.kt | 204 +++++++++--
.../ui/viewmodel/AppSelectorViewModel.kt | 7 +-
.../ui/viewmodel/DashboardViewModel.kt | 8 +
.../ui/viewmodel/DownloadsViewModel.kt | 42 ++-
.../manager/ui/viewmodel/MainViewModel.kt | 34 ++
.../manager/ui/viewmodel/PatcherViewModel.kt | 109 ++++--
.../ui/viewmodel/SelectedAppInfoViewModel.kt | 168 ++++++++-
.../ui/viewmodel/VersionSelectorViewModel.kt | 173 ----------
.../main/java/app/revanced/manager/util/PM.kt | 39 ++-
.../java/app/revanced/manager/util/Util.kt | 39 ++-
app/src/main/res/values/strings.xml | 42 ++-
app/src/main/res/values/themes.xml | 2 +
build.gradle.kts | 9 +-
downloader-plugin/.gitignore | 1 +
downloader-plugin/api/downloader-plugin.api | 171 ++++++++++
downloader-plugin/build.gradle.kts | 61 ++++
downloader-plugin/consumer-rules.pro | 0
downloader-plugin/proguard-rules.pro | 21 ++
.../src/main/AndroidManifest.xml | 3 +
.../plugin/downloader/webview/IWebView.aidl | 8 +
.../downloader/webview/IWebViewEvents.aidl | 11 +
.../manager/plugin/downloader/Constants.kt | 7 +
.../manager/plugin/downloader/Downloader.kt | 165 +++++++++
.../manager/plugin/downloader/Extensions.kt | 42 +++
.../manager/plugin/downloader/Parcelables.kt | 39 +++
.../manager/plugin/downloader/webview/API.kt | 176 ++++++++++
.../downloader/webview/WebViewActivity.kt | 161 +++++++++
.../src/main/res/layout/activity_webview.xml | 11 +
.../src/main/res/values/strings.xml | 1 +
.../src/main/res/values/themes.xml | 7 +
example-downloader-plugin/.gitignore | 1 +
example-downloader-plugin/build.gradle.kts | 53 +++
example-downloader-plugin/proguard-rules.pro | 21 ++
.../src/main/AndroidManifest.xml | 23 ++
.../downloader/example/ExamplePlugin.kt | 69 ++++
.../downloader/example/InteractionActivity.kt | 65 ++++
.../res/drawable/ic_launcher_background.xml | 170 ++++++++++
.../res/drawable/ic_launcher_foreground.xml | 30 ++
.../main/res/mipmap-anydpi/ic_launcher.xml | 6 +
.../res/mipmap-anydpi/ic_launcher_round.xml | 6 +
.../src/main/res/values/strings.xml | 3 +
gradle/libs.versions.toml | 20 +-
settings.gradle.kts | 2 +
84 files changed, 2984 insertions(+), 1105 deletions(-)
create mode 100644 app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt
create mode 100644 app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt
create mode 100644 app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
delete mode 100644 app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
delete mode 100644 app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
create mode 100644 app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt
create mode 100644 app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
create mode 100644 app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt
create mode 100644 app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt
delete mode 100644 app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
delete mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
create mode 100644 downloader-plugin/.gitignore
create mode 100644 downloader-plugin/api/downloader-plugin.api
create mode 100644 downloader-plugin/build.gradle.kts
create mode 100644 downloader-plugin/consumer-rules.pro
create mode 100644 downloader-plugin/proguard-rules.pro
create mode 100644 downloader-plugin/src/main/AndroidManifest.xml
create mode 100644 downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl
create mode 100644 downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt
create mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt
create mode 100644 downloader-plugin/src/main/res/layout/activity_webview.xml
create mode 100644 downloader-plugin/src/main/res/values/strings.xml
create mode 100644 downloader-plugin/src/main/res/values/themes.xml
create mode 100644 example-downloader-plugin/.gitignore
create mode 100644 example-downloader-plugin/build.gradle.kts
create mode 100644 example-downloader-plugin/proguard-rules.pro
create mode 100644 example-downloader-plugin/src/main/AndroidManifest.xml
create mode 100644 example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt
create mode 100644 example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt
create mode 100644 example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml
create mode 100644 example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml
create mode 100644 example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml
create mode 100644 example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml
create mode 100644 example-downloader-plugin/src/main/res/values/strings.xml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 79b726bb..87f37444 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -113,9 +113,10 @@ dependencies {
implementation(libs.runtime.ktx)
implementation(libs.runtime.compose)
implementation(libs.splash.screen)
- implementation(libs.compose.activity)
+ implementation(libs.activity.compose)
implementation(libs.work.runtime.ktx)
implementation(libs.preferences.datastore)
+ implementation(libs.appcompat)
// Compose
implementation(platform(libs.compose.bom))
@@ -155,6 +156,9 @@ dependencies {
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
+ // Downloader plugins
+ implementation(project(":downloader-plugin"))
+
// Native processes
implementation(libs.kotlin.process)
@@ -196,7 +200,7 @@ dependencies {
// EnumUtil
implementation(libs.enumutil)
ksp(libs.enumutil.ksp)
-
+
// Reorderable lists
implementation(libs.reorderable)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f284b52a..b9b9c1af 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -49,6 +49,10 @@
-keep class com.android.** {
*;
}
+-keep class app.revanced.manager.plugin.** {
+ *;
+}
+
-dontwarn com.google.auto.value.**
-dontwarn java.awt.**
-dontwarn javax.**
diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
index eff10786..fd83a51e 100644
--- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
+++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
- "identityHash": "c385297c07ea54804dc8526c388f706d",
+ "identityHash": "d0119047505da435972c5247181de675",
"entities": [
{
"tableName": "patch_bundles",
@@ -144,7 +144,7 @@
},
{
"tableName": "downloaded_app",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
+ "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",
@@ -163,6 +163,12 @@
"columnName": "directory",
"affinity": "TEXT",
"notNull": true
+ },
+ {
+ "fieldPath": "lastUsed",
+ "columnName": "last_used",
+ "affinity": "INTEGER",
+ "notNull": true
}
],
"primaryKey": {
@@ -386,12 +392,38 @@
]
}
]
+ },
+ {
+ "tableName": "trusted_downloader_plugins",
+ "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"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c385297c07ea54804dc8526c388f706d')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3acb1c04..0404d045 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,9 +2,16 @@
-
+
+
+
@@ -17,12 +24,6 @@
tools:ignore="ScopedStorage" />
-
-
-
-
-
-
+
+
diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt
index 4c8d9ef7..ff01094c 100644
--- a/app/src/main/java/app/revanced/manager/MainActivity.kt
+++ b/app/src/main/java/app/revanced/manager/MainActivity.kt
@@ -3,10 +3,12 @@ package app.revanced.manager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.AppSelectorScreen
@@ -15,11 +17,11 @@ import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
-import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
+import app.revanced.manager.util.EventEffect
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
@@ -35,6 +37,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ enableEdgeToEdge()
installSplashScreen()
val vm: MainViewModel = getAndroidViewModel()
@@ -52,6 +56,10 @@ class MainActivity : ComponentActivity() {
rememberNavController(startDestination = Destination.Dashboard)
NavBackHandler(navController)
+ EventEffect(vm.appSelectFlow) { app ->
+ navController.navigate(Destination.SelectedApplicationInfo(app))
+ }
+
AnimatedNavHost(
controller = navController
) { destination ->
@@ -59,9 +67,12 @@ class MainActivity : ComponentActivity() {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
- onUpdateClick = { navController.navigate(
- Destination.Settings(SettingsDestination.Update())
- ) },
+ onUpdateClick = {
+ navController.navigate(Destination.Settings(SettingsDestination.Update()))
+ },
+ onDownloaderPluginClick = {
+ navController.navigate(Destination.Settings(SettingsDestination.Downloads))
+ },
onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
@@ -72,14 +83,7 @@ class MainActivity : ComponentActivity() {
)
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
- onPatchClick = { packageName, patchSelection ->
- navController.navigate(
- Destination.VersionSelector(
- packageName,
- patchSelection
- )
- )
- },
+ onPatchClick = vm::selectApp,
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
)
@@ -90,35 +94,11 @@ class MainActivity : ComponentActivity() {
)
is Destination.AppSelector -> AppSelectorScreen(
- onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
- onStorageClick = {
- navController.navigate(
- Destination.SelectedApplicationInfo(
- it
- )
- )
- },
+ onSelect = vm::selectApp,
+ onStorageSelect = vm::selectApp,
onBackClick = { navController.pop() }
)
- is Destination.VersionSelector -> VersionSelectorScreen(
- onBackClick = { navController.pop() },
- onAppClick = { selectedApp ->
- navController.navigate(
- Destination.SelectedApplicationInfo(
- selectedApp,
- destination.patchSelection,
- )
- )
- },
- viewModel = getComposeViewModel {
- parametersOf(
- destination.packageName,
- destination.patchSelection
- )
- }
- )
-
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options ->
navController.navigate(
diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
index 66ab2483..2060e602 100644
--- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt
+++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
@@ -3,6 +3,7 @@ package app.revanced.manager
import android.app.Application
import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import kotlinx.coroutines.Dispatchers
import coil.Coil
@@ -23,6 +24,8 @@ class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
+ private val downloaderPluginRepository: DownloaderPluginRepository by inject()
+
override fun onCreate() {
super.onCreate()
@@ -59,6 +62,9 @@ class ManagerApplication : Application() {
scope.launch {
prefs.preload()
}
+ scope.launch(Dispatchers.Default) {
+ downloaderPluginRepository.reload()
+ }
scope.launch(Dispatchers.Default) {
with(patchBundleRepository) {
reload()
diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt
index 0440a7c2..403bd1cf 100644
--- a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt
@@ -16,9 +16,14 @@ import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.options.Option
import app.revanced.manager.data.room.options.OptionDao
import app.revanced.manager.data.room.options.OptionGroup
+import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
+import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
import kotlin.random.Random
-@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class], version = 1)
+@Database(
+ entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
+ version = 1
+)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao
@@ -26,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
abstract fun optionDao(): OptionDao
+ abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
companion object {
fun generateUid() = Random.Default.nextInt()
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
index 60d1561d..f1703314 100644
--- a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
@@ -12,4 +12,5 @@ data class DownloadedApp(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "directory") val directory: File,
+ @ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt
index 4f4d9623..492dbde1 100644
--- a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
+import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
@@ -14,8 +15,11 @@ interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
suspend fun get(packageName: String, version: String): DownloadedApp?
- @Insert
- suspend fun insert(downloadedApp: DownloadedApp)
+ @Upsert
+ suspend fun upsert(downloadedApp: DownloadedApp)
+
+ @Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
+ suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
@Delete
suspend fun delete(downloadedApps: Collection)
diff --git a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt
new file mode 100644
index 00000000..8e1b9c39
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt
@@ -0,0 +1,11 @@
+package app.revanced.manager.data.room.plugins
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "trusted_downloader_plugins")
+class TrustedDownloaderPlugin(
+ @PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
+ @ColumnInfo(name = "signature") val signature: ByteArray
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt
new file mode 100644
index 00000000..ad1845f7
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt
@@ -0,0 +1,22 @@
+package app.revanced.manager.data.room.plugins
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Upsert
+
+@Dao
+interface TrustedDownloaderPluginDao {
+ @Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
+ suspend fun getTrustedSignature(packageName: String): ByteArray?
+
+ @Upsert
+ suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
+
+ @Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
+ suspend fun remove(packageName: String)
+
+ @Transaction
+ @Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
+ suspend fun removeAll(packageNames: Set)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
index df2d7018..159436d4 100644
--- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
@@ -22,6 +22,7 @@ val repositoryModule = module {
// It is best to load patch bundles ASAP
createdAtStart()
}
+ singleOf(::DownloaderPluginRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
index 0c69767c..a59d65a2 100644
--- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
@@ -12,7 +12,6 @@ val viewModelModule = module {
viewModelOf(::SettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
- viewModelOf(::VersionSelectorViewModel)
viewModelOf(::PatcherViewModel)
viewModelOf(::UpdateViewModel)
viewModelOf(::ChangelogsViewModel)
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
index 8cdc1f19..dbf2f100 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
@@ -18,8 +18,6 @@ class PreferencesManager(
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
- val preferSplits = booleanPreference("prefer_splits", false)
-
val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
@@ -28,4 +26,6 @@ class PreferencesManager(
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
+
+ val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
}
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt
index 2b04bd41..06f75465 100644
--- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt
+++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt
@@ -26,6 +26,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String
protected fun stringPreference(key: String, default: String) =
StringPreference(dataStore, key, default)
+ protected fun stringSetPreference(key: String, default: Set) =
+ StringSetPreference(dataStore, key, default)
+
protected fun booleanPreference(key: String, default: Boolean) =
BooleanPreference(dataStore, key, default)
@@ -52,6 +55,10 @@ class EditorContext(private val prefs: MutablePreferences) {
var Preference.value
get() = prefs.run { read() }
set(value) = prefs.run { write(value) }
+
+ operator fun Preference>.plusAssign(value: String) = prefs.run {
+ write(read() + value)
+ }
}
abstract class Preference(
@@ -65,10 +72,12 @@ abstract class Preference(
suspend fun get() = flow.first()
fun getBlocking() = runBlocking { get() }
+
@Composable
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
getBlocking()
})
+
suspend fun update(value: T) = dataStore.editor {
this@Preference.value = value
}
@@ -108,6 +117,14 @@ class StringPreference(
override val key = stringPreferencesKey(key)
}
+class StringSetPreference(
+ dataStore: DataStore,
+ key: String,
+ default: Set
+) : BasePreference>(dataStore, default) {
+ override val key = stringSetPreferencesKey(key)
+}
+
class BooleanPreference(
dataStore: DataStore,
key: String,
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
index fe339a2e..b4598fb9 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
@@ -2,56 +2,126 @@ package app.revanced.manager.domain.repository
import android.app.Application
import android.content.Context
+import android.os.Parcelable
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
-import app.revanced.manager.network.downloader.AppDownloader
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.plugin.downloader.OutputDownloadScope
+import app.revanced.manager.util.PM
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import java.io.File
+import java.io.FilterOutputStream
+import java.nio.file.StandardOpenOption
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.io.path.outputStream
class DownloadedAppRepository(
- app: Application,
- db: AppDatabase
+ private val app: Application,
+ db: AppDatabase,
+ private val pm: PM
) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()
- fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
+ fun getApkFileForApp(app: DownloadedApp): File =
+ getApkFileForDir(dir.resolve(app.directory))
+
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
suspend fun download(
- app: AppDownloader.App,
- preferSplits: Boolean,
- onDownload: suspend (downloadProgress: Pair?) -> Unit = {},
+ plugin: LoadedDownloaderPlugin,
+ data: Parcelable,
+ expectedPackageName: String,
+ expectedVersion: String?,
+ onDownload: suspend (downloadProgress: Pair) -> Unit,
): File {
- this.get(app.packageName, app.version)?.let { downloaded ->
- return getApkFileForApp(downloaded)
- }
-
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
val relativePath = File(generateUid().toString())
- val savePath = dir.resolve(relativePath).also { it.mkdirs() }
+ val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
+ val targetFile = saveDir.resolve("base.apk").toPath()
try {
- app.download(savePath, preferSplits, onDownload)
+ val downloadSize = AtomicLong(0)
+ val downloadedBytes = AtomicLong(0)
- dao.insert(DownloadedApp(
- packageName = app.packageName,
- version = app.version,
- directory = relativePath,
- ))
+ channelFlow {
+ val scope = object : OutputDownloadScope {
+ override val pluginPackageName = plugin.packageName
+ override val hostPackageName = app.packageName
+ override suspend fun reportSize(size: Long) {
+ require(size > 0) { "Size must be greater than zero" }
+ require(
+ downloadSize.compareAndSet(
+ 0,
+ size
+ )
+ ) { "Download size has already been set" }
+ send(downloadedBytes.get() to size)
+ }
+ }
+
+ fun emitProgress(bytes: Long) {
+ val newValue = downloadedBytes.addAndGet(bytes)
+ val totalSize = downloadSize.get()
+ if (totalSize < 1) return
+ trySend(newValue to totalSize).getOrThrow()
+ }
+
+ targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
+ val stream = object : FilterOutputStream(it) {
+ override fun write(b: Int) = out.write(b).also { emitProgress(1) }
+
+ override fun write(b: ByteArray?, off: Int, len: Int) =
+ out.write(b, off, len).also {
+ emitProgress(
+ (len - off).toLong()
+ )
+ }
+ }
+ plugin.download(scope, data, stream)
+ }
+ }
+ .conflate()
+ .flowOn(Dispatchers.IO)
+ .collect { (downloaded, size) -> onDownload(downloaded to size) }
+
+ if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
+ val pkgInfo =
+ pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
+ if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
+ if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
+
+ // Delete the previous copy (if present).
+ dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
+ if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory")
+ }
+ dao.upsert(
+ DownloadedApp(
+ packageName = pkgInfo.packageName,
+ version = pkgInfo.versionName!!,
+ directory = relativePath,
+ )
+ )
} catch (e: Exception) {
- savePath.deleteRecursively()
+ saveDir.deleteRecursively()
throw e
}
// Return the Apk file.
- return getApkFileForDir(savePath)
+ return getApkFileForDir(saveDir)
}
- suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
+ suspend fun get(packageName: String, version: String, markUsed: Boolean = false) =
+ dao.get(packageName, version)?.also {
+ if (markUsed) dao.markUsed(packageName, version)
+ }
suspend fun delete(downloadedApps: Collection) {
downloadedApps.forEach {
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
new file mode 100644
index 00000000..791a09ac
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
@@ -0,0 +1,168 @@
+package app.revanced.manager.domain.repository
+
+import android.app.Application
+import android.content.pm.PackageManager
+import android.os.Parcelable
+import android.util.Log
+import app.revanced.manager.data.room.AppDatabase
+import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
+import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.network.downloader.DownloaderPluginState
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.network.downloader.ParceledDownloaderData
+import app.revanced.manager.plugin.downloader.DownloaderBuilder
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import app.revanced.manager.plugin.downloader.Scope
+import app.revanced.manager.util.PM
+import app.revanced.manager.util.tag
+import dalvik.system.PathClassLoader
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+import java.lang.reflect.Modifier
+
+@OptIn(PluginHostApi::class)
+class DownloaderPluginRepository(
+ private val pm: PM,
+ private val prefs: PreferencesManager,
+ private val app: Application,
+ db: AppDatabase
+) {
+ private val trustDao = db.trustedDownloaderPluginDao()
+ private val _pluginStates = MutableStateFlow(emptyMap())
+ val pluginStates = _pluginStates.asStateFlow()
+ val loadedPluginsFlow = pluginStates.map { states ->
+ states.values.filterIsInstance().map { it.plugin }
+ }
+
+ private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
+ private val installedPluginPackageNames = MutableStateFlow(emptySet())
+ val newPluginPackageNames = combine(
+ installedPluginPackageNames,
+ acknowledgedDownloaderPlugins.flow
+ ) { installed, acknowledged ->
+ installed subtract acknowledged
+ }
+
+ suspend fun reload() {
+ val plugins =
+ withContext(Dispatchers.IO) {
+ pm.getPackagesWithFeature(PLUGIN_FEATURE)
+ .associate { it.packageName to loadPlugin(it.packageName) }
+ }
+
+ _pluginStates.value = plugins
+ installedPluginPackageNames.value = plugins.keys
+
+ val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
+ val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
+ if (uninstalledPlugins.isNotEmpty()) {
+ Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
+ acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
+ trustDao.removeAll(uninstalledPlugins)
+ }
+ }
+
+ fun unwrapParceledData(data: ParceledDownloaderData): Pair {
+ val plugin =
+ (_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
+ ?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
+
+ return plugin to data.unwrapWith(plugin)
+ }
+
+ private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
+ try {
+ if (!verify(packageName)) return DownloaderPluginState.Untrusted
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ Log.e(tag, "Got exception while verifying plugin $packageName", e)
+ return DownloaderPluginState.Failed(e)
+ }
+
+ return try {
+ val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
+ val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
+ ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
+
+ val classLoader =
+ PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
+ val pluginContext = app.createPackageContext(packageName, 0)
+
+ val downloader = classLoader
+ .loadClass(className)
+ .getDownloaderBuilder()
+ .build(
+ scopeImpl = object : Scope {
+ override val hostPackageName = app.packageName
+ override val pluginPackageName = pluginContext.packageName
+ },
+ context = pluginContext
+ )
+
+ DownloaderPluginState.Loaded(
+ LoadedDownloaderPlugin(
+ packageName,
+ with(pm) { packageInfo.label() },
+ packageInfo.versionName!!,
+ downloader.get,
+ downloader.download,
+ classLoader
+ )
+ )
+ } catch (e: CancellationException) {
+ throw e
+ } catch (t: Throwable) {
+ Log.e(tag, "Failed to load plugin $packageName", t)
+ DownloaderPluginState.Failed(t)
+ }
+ }
+
+ suspend fun trustPackage(packageName: String) {
+ trustDao.upsertTrust(
+ TrustedDownloaderPlugin(
+ packageName,
+ pm.getSignature(packageName).toByteArray()
+ )
+ )
+
+ reload()
+ prefs.edit {
+ acknowledgedDownloaderPlugins += packageName
+ }
+ }
+
+ suspend fun revokeTrustForPackage(packageName: String) =
+ trustDao.remove(packageName).also { reload() }
+
+ suspend fun acknowledgeAllNewPlugins() =
+ acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
+
+ private suspend fun verify(packageName: String): Boolean {
+ val expectedSignature =
+ trustDao.getTrustedSignature(packageName) ?: return false
+
+ return pm.hasSignature(packageName, expectedSignature)
+ }
+
+ private companion object {
+ const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
+ const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
+
+ const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
+ val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
+ val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
+
+ @Suppress("UNCHECKED_CAST")
+ fun Class<*>.getDownloaderBuilder() =
+ declaredMethods
+ .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
+ ?.let { it(null) as DownloaderBuilder }
+ ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt b/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
deleted file mode 100644
index 25365551..00000000
--- a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt
+++ /dev/null
@@ -1,277 +0,0 @@
-package app.revanced.manager.network.downloader
-
-import android.os.Build.SUPPORTED_ABIS
-import app.revanced.manager.network.service.HttpService
-import io.ktor.client.plugins.onDownload
-import io.ktor.client.request.parameter
-import io.ktor.client.request.url
-import it.skrape.selects.html5.a
-import it.skrape.selects.html5.div
-import it.skrape.selects.html5.form
-import it.skrape.selects.html5.h5
-import it.skrape.selects.html5.input
-import it.skrape.selects.html5.p
-import it.skrape.selects.html5.span
-import kotlinx.coroutines.flow.flow
-import kotlinx.parcelize.IgnoredOnParcel
-import kotlinx.parcelize.Parcelize
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.get
-import org.koin.core.component.inject
-import java.io.File
-
-class APKMirror : AppDownloader, KoinComponent {
- private val httpClient: HttpService = get()
-
- enum class APKType {
- APK,
- BUNDLE
- }
-
- data class Variant(
- val apkType: APKType,
- val arch: String,
- val link: String
- )
-
- private suspend fun getAppLink(packageName: String): String {
- val searchResults = httpClient.getHtml { url("$APK_MIRROR/?post_type=app_release&searchtype=app&s=$packageName") }
- .div {
- withId = "content"
- findFirst {
- div {
- withClass = "listWidget"
- findAll {
-
- find {
- it.children.first().text.contains(packageName)
- }!!.children.mapNotNull {
- if (it.classNames.isEmpty()) {
- it.h5 {
- withClass = "appRowTitle"
- findFirst {
- a {
- findFirst {
- attribute("href")
- }
- }
- }
- }
- } else null
- }
-
- }
- }
- }
- }
-
- return searchResults.find { url ->
- httpClient.getHtml { url(APK_MIRROR + url) }
- .div {
- withId = "primary"
- findFirst {
- div {
- withClass = "tab-buttons"
- findFirst {
- div {
- withClass = "tab-button-positioning"
- findFirst {
- children.any {
- it.attribute("href") == "https://play.google.com/store/apps/details?id=$packageName"
- }
- }
- }
- }
- }
- }
- }
- } ?: throw Exception("App isn't available for download")
- }
-
- override fun getAvailableVersions(packageName: String, versionFilter: Set) = flow {
-
- // We have to hardcode some apps since there are multiple apps with that package name
- val appCategory = when (packageName) {
- "com.google.android.apps.youtube.music" -> "youtube-music"
- "com.google.android.youtube" -> "youtube"
- else -> getAppLink(packageName).split("/")[3]
- }
-
- var page = 1
-
- val versions = mutableListOf()
-
- while (
- if (versionFilter.isNotEmpty())
- versions.size < versionFilter.size && page <= 7
- else
- page <= 1
- ) {
- httpClient.getHtml {
- url("$APK_MIRROR/uploads/page/$page/")
- parameter("appcategory", appCategory)
- }.div {
- withClass = "widget_appmanager_recentpostswidget"
- findFirst {
- div {
- withClass = "listWidget"
- findFirst {
- children.mapNotNull { element ->
- if (element.className.isEmpty()) {
-
- APKMirrorApp(
- packageName = packageName,
- version = element.div {
- withClass = "infoSlide"
- findFirst {
- p {
- findFirst {
- span {
- withClass = "infoSlide-value"
- findFirst {
- text
- }
- }
- }
- }
- }
- }.also {
- if (it in versionFilter)
- versions.add(it)
- },
- downloadLink = element.findFirst {
- a {
- withClass = "downloadLink"
- findFirst {
- attribute("href")
- }
- }
- }
- )
-
- } else null
- }
- }
- }
- }
- }.onEach { version -> emit(version) }
-
- page++
- }
- }
-
- @Parcelize
- private class APKMirrorApp(
- override val packageName: String,
- override val version: String,
- private val downloadLink: String,
- ) : AppDownloader.App, KoinComponent {
- @IgnoredOnParcel private val httpClient: HttpService by inject()
-
- override suspend fun download(
- saveDirectory: File,
- preferSplit: Boolean,
- onDownload: suspend (downloadProgress: Pair?) -> Unit
- ) {
- val variants = httpClient.getHtml { url(APK_MIRROR + downloadLink) }
- .div {
- withClass = "variants-table"
- findFirst { // list of variants
- children.drop(1).map {
- Variant(
- apkType = it.div {
- findFirst {
- span {
- findFirst {
- enumValueOf(text)
- }
- }
- }
- },
- arch = it.div {
- findSecond {
- text
- }
- },
- link = it.div {
- findFirst {
- a {
- findFirst {
- attribute("href")
- }
- }
- }
- }
- )
- }
- }
- }
-
- val orderedAPKTypes = mutableListOf(APKType.APK, APKType.BUNDLE)
- .also { if (preferSplit) it.reverse() }
-
- val variant = orderedAPKTypes.firstNotNullOfOrNull { apkType ->
- supportedArches.firstNotNullOfOrNull { arch ->
- variants.find { it.arch == arch && it.apkType == apkType }
- }
- } ?: throw Exception("No compatible variant found")
-
- if (variant.apkType == APKType.BUNDLE) throw Exception("Split apks are not supported yet") // TODO
-
- val downloadPage = httpClient.getHtml { url(APK_MIRROR + variant.link) }
- .a {
- withClass = "downloadButton"
- findFirst {
- attribute("href")
- }
- }
-
- val downloadLink = httpClient.getHtml { url(APK_MIRROR + downloadPage) }
- .form {
- withId = "filedownload"
- findFirst {
- val apkLink = attribute("action")
- val id = input {
- withAttribute = "name" to "id"
- findFirst {
- attribute("value")
- }
- }
- val key = input {
- withAttribute = "name" to "key"
- findFirst {
- attribute("value")
- }
- }
- "$apkLink?id=$id&key=$key"
- }
- }
-
- val targetFile = saveDirectory.resolve("base.apk")
-
- try {
- httpClient.download(targetFile) {
- url(APK_MIRROR + downloadLink)
- onDownload { bytesSentTotal, contentLength ->
- onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
- }
- }
-
- if (variant.apkType == APKType.BUNDLE) {
- // TODO: Extract temp.zip
-
- targetFile.delete()
- }
- } finally {
- onDownload(null)
- }
- }
- }
-
- companion object {
- const val APK_MIRROR = "https://www.apkmirror.com"
-
- val supportedArches = listOf("universal", "noarch") + SUPPORTED_ABIS
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt b/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
deleted file mode 100644
index dcefa26e..00000000
--- a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package app.revanced.manager.network.downloader
-
-import android.os.Parcelable
-import kotlinx.coroutines.flow.Flow
-import java.io.File
-
-interface AppDownloader {
-
- /**
- * Returns all downloadable apps.
- *
- * @param packageName The package name of the app.
- * @param versionFilter A set of versions to filter.
- */
- fun getAvailableVersions(packageName: String, versionFilter: Set): Flow
-
- interface App : Parcelable {
- val packageName: String
- val version: String
-
- suspend fun download(
- saveDirectory: File,
- preferSplit: Boolean,
- onDownload: suspend (downloadProgress: Pair?) -> Unit = {}
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt
new file mode 100644
index 00000000..a72d60c7
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt
@@ -0,0 +1,9 @@
+package app.revanced.manager.network.downloader
+
+sealed interface DownloaderPluginState {
+ data object Untrusted : DownloaderPluginState
+
+ data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState
+
+ data class Failed(val throwable: Throwable) : DownloaderPluginState
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
new file mode 100644
index 00000000..50ddd561
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
@@ -0,0 +1,15 @@
+package app.revanced.manager.network.downloader
+
+import android.os.Parcelable
+import app.revanced.manager.plugin.downloader.OutputDownloadScope
+import app.revanced.manager.plugin.downloader.GetScope
+import java.io.OutputStream
+
+class LoadedDownloaderPlugin(
+ val packageName: String,
+ val name: String,
+ val version: String,
+ val get: suspend GetScope.(packageName: String, version: String?) -> Pair?,
+ val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit,
+ val classLoader: ClassLoader
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt
new file mode 100644
index 00000000..a43db930
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt
@@ -0,0 +1,45 @@
+package app.revanced.manager.network.downloader
+
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+/**
+ * A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
+ */
+class ParceledDownloaderData private constructor(
+ val pluginPackageName: String,
+ private val bundle: Bundle
+) : Parcelable {
+ constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
+ plugin.packageName,
+ createBundle(data)
+ )
+
+ fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
+ bundle.classLoader = plugin.classLoader
+
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val className = bundle.getString(CLASS_NAME_KEY)!!
+ val clazz = plugin.classLoader.loadClass(className)
+
+ bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
+ } else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
+ }
+
+ private companion object {
+ const val CLASS_NAME_KEY = "class"
+ const val DATA_KEY = "data"
+
+ fun createBundle(data: Parcelable) = Bundle().apply {
+ putParcelable(DATA_KEY, data)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
+ CLASS_NAME_KEY,
+ data::class.java.canonicalName
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
index cd2a2f83..2babc7f4 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
@@ -33,15 +33,14 @@ data class PatchInfo(
fun compatibleWith(packageName: String) =
compatiblePackages?.any { it.packageName == packageName } ?: true
- fun supportsVersion(packageName: String, versionName: String): Boolean {
+ fun supports(packageName: String, versionName: String?): Boolean {
val packages = compatiblePackages ?: return true // Universal patch
return packages.any { pkg ->
- if (pkg.packageName != packageName) {
- return@any false
- }
+ if (pkg.packageName != packageName) return@any false
+ if (pkg.versions == null) return@any true
- pkg.versions == null || pkg.versions.contains(versionName)
+ versionName != null && versionName in pkg.versions
}
}
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
index c295bde1..d2b5babb 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -1,5 +1,6 @@
package app.revanced.manager.patcher.worker
+import android.app.Activity
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -9,9 +10,11 @@ import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.drawable.Icon
import android.os.Build
+import android.os.Parcelable
import android.os.PowerManager
import android.util.Log
import android.view.WindowManager
+import androidx.activity.result.ActivityResult
import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
@@ -22,26 +25,35 @@ import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime
+import app.revanced.manager.plugin.downloader.GetScope
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
+@OptIn(PluginHostApi::class)
class PatcherWorker(
context: Context,
parameters: WorkerParameters
@@ -49,20 +61,22 @@ class PatcherWorker(
private val workerRepository: WorkerRepository by inject()
private val prefs: PreferencesManager by inject()
private val keystoreManager: KeystoreManager by inject()
+ private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val pm: PM by inject()
private val fs: Filesystem by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
- data class Args(
+ class Args(
val input: SelectedApp,
val output: String,
val selectedPatches: PatchSelection,
val options: Options,
val logger: Logger,
- val downloadProgress: MutableStateFlow?>,
+ val downloadProgress: MutableStateFlow?>,
val patchesProgress: MutableStateFlow>,
+ val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: (File) -> Unit,
val onProgress: ProgressEventHandler
) {
@@ -141,16 +155,57 @@ class PatcherWorker(
}
}
+ suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
+ downloadedAppRepository.download(
+ plugin,
+ data,
+ args.packageName,
+ args.input.version,
+ onDownload = args.downloadProgress::emit
+ ).also {
+ args.setInputFile(it)
+ updateProgress(state = State.COMPLETED) // Download APK
+ }
+
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
- downloadedAppRepository.download(
- selectedApp.app,
- prefs.preferSplits.get(),
- onDownload = { args.downloadProgress.emit(it) }
- ).also {
- args.setInputFile(it)
- updateProgress(state = State.COMPLETED) // Download APK
- }
+ val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
+
+ download(plugin, data)
+ }
+
+ is SelectedApp.Search -> {
+ downloaderPluginRepository.loadedPluginsFlow.first()
+ .firstNotNullOfOrNull { plugin ->
+ try {
+ val getScope = object : GetScope {
+ override val pluginPackageName = plugin.packageName
+ override val hostPackageName = applicationContext.packageName
+ override suspend fun requestStartActivity(intent: Intent): Intent? {
+ val result = args.handleStartActivityRequest(plugin, intent)
+ return when (result.resultCode) {
+ Activity.RESULT_OK -> result.data
+ Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
+ else -> throw UserInteractionException.Activity.NotCompleted(
+ result.resultCode,
+ result.data
+ )
+ }
+ }
+ }
+ withContext(Dispatchers.IO) {
+ plugin.get(
+ getScope,
+ selectedApp.packageName,
+ selectedApp.version
+ )
+ }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
+ } catch (e: UserInteractionException.Activity.NotCompleted) {
+ throw e
+ } catch (_: UserInteractionException) {
+ null
+ }?.let { (data, _) -> download(plugin, data) }
+ } ?: throw Exception("App is not available.")
}
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@@ -184,7 +239,10 @@ class PatcherWorker(
Log.i(tag, "Patching succeeded".logFmt())
Result.success()
} catch (e: ProcessRuntime.RemoteFailureException) {
- Log.e(tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt())
+ Log.e(
+ tag,
+ "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
+ )
updateProgress(state = State.FAILED, message = e.originalStackTrace)
Result.failure()
} catch (e: Exception) {
diff --git a/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt
new file mode 100644
index 00000000..1ceb9cef
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt
@@ -0,0 +1,79 @@
+package app.revanced.manager.ui.component
+
+import android.content.Intent
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Share
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import app.revanced.manager.R
+import app.revanced.manager.ui.component.bundle.BundleTopBar
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
+ val context = LocalContext.current
+
+ Dialog(
+ onDismissRequest = onDismiss,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ dismissOnBackPress = true
+ )
+ ) {
+ Scaffold(
+ topBar = {
+ BundleTopBar(
+ title = stringResource(R.string.bundle_error),
+ onBackClick = onDismiss,
+ backIcon = {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ stringResource(R.string.back)
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ val sendIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(
+ Intent.EXTRA_TEXT,
+ text
+ )
+ type = "text/plain"
+ }
+
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ context.startActivity(shareIntent)
+ }
+ ) {
+ Icon(
+ Icons.Outlined.Share,
+ contentDescription = stringResource(R.string.share)
+ )
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ ColumnWithScrollbar(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ Text(text, modifier = Modifier.horizontalScroll(rememberScrollState()))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt b/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt
index 303cff05..04b5b589 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt
@@ -1,13 +1,15 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarColors
+import androidx.compose.material3.SearchBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -27,29 +29,38 @@ fun SearchView(
placeholder: (@Composable () -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
+ val colors = SearchBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ dividerColor = MaterialTheme.colorScheme.outline
+ )
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
SearchBar(
- query = query,
- onQueryChange = onQueryChange,
- onSearch = {
- keyboardController?.hide()
- },
- active = true,
- onActiveChange = onActiveChange,
- modifier = Modifier
- .fillMaxSize()
- .focusRequester(focusRequester),
- placeholder = placeholder,
- leadingIcon = {
- IconButton({ onActiveChange(false) }) {
- Icon(
- Icons.AutoMirrored.Filled.ArrowBack,
- stringResource(R.string.back)
- )
- }
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = query,
+ onQueryChange = onQueryChange,
+ onSearch = {
+ keyboardController?.hide()
+ },
+ expanded = true,
+ onExpandedChange = onActiveChange,
+ placeholder = placeholder,
+ leadingIcon = {
+ IconButton(onClick = { onActiveChange(false) }) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ stringResource(R.string.back)
+ )
+ }
+ }
+ )
},
+ expanded = true,
+ onExpandedChange = onActiveChange,
+ modifier = Modifier.focusRequester(focusRequester),
+ colors = colors,
content = content
)
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
index 83c60e0f..eaebd834 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt
@@ -1,21 +1,16 @@
package app.revanced.manager.ui.component.bundle
-import android.content.Intent
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.DeleteOutline
-import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -26,7 +21,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
-import app.revanced.manager.ui.component.ColumnWithScrollbar
+import app.revanced.manager.ui.component.ExceptionViewerDialog
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -119,7 +114,7 @@ fun BundleInformationDialog(
var showDialog by rememberSaveable {
mutableStateOf(false)
}
- if (showDialog) BundleErrorViewerDialog(
+ if (showDialog) ExceptionViewerDialog(
onDismiss = { showDialog = false },
text = remember(it) { it.stackTraceToString() }
)
@@ -148,61 +143,4 @@ fun BundleInformationDialog(
)
}
}
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun BundleErrorViewerDialog(onDismiss: () -> Unit, text: String) {
- val context = LocalContext.current
-
- Dialog(
- onDismissRequest = onDismiss,
- properties = DialogProperties(
- usePlatformDefaultWidth = false,
- dismissOnBackPress = true
- )
- ) {
- Scaffold(
- topBar = {
- BundleTopBar(
- title = stringResource(R.string.bundle_error),
- onBackClick = onDismiss,
- backIcon = {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = stringResource(R.string.back)
- )
- },
- actions = {
- IconButton(
- onClick = {
- val sendIntent: Intent = Intent().apply {
- action = Intent.ACTION_SEND
- putExtra(
- Intent.EXTRA_TEXT,
- text
- )
- type = "text/plain"
- }
-
- val shareIntent = Intent.createChooser(sendIntent, null)
- context.startActivity(shareIntent)
- }
- ) {
- Icon(
- Icons.Outlined.Share,
- contentDescription = stringResource(R.string.share)
- )
- }
- }
- )
- }
- ) { paddingValues ->
- ColumnWithScrollbar(
- modifier = Modifier.padding(paddingValues)
- ) {
- Text(text, modifier = Modifier.horizontalScroll(rememberScrollState()))
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
index 6840837b..280635ce 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
@@ -43,6 +43,7 @@ import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
+import java.util.Locale
import kotlin.math.floor
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
@@ -134,7 +135,7 @@ fun SubStep(
name: String,
state: State,
message: String? = null,
- downloadProgress: Pair? = null
+ downloadProgress: Pair? = null
) {
var messageExpanded by rememberSaveable { mutableStateOf(true) }
@@ -180,7 +181,7 @@ fun SubStep(
} else {
downloadProgress?.let { (current, total) ->
Text(
- "$current/$total MB",
+ if (total != null) "${current.megaBytes}/${total.megaBytes} MB" else "${current.megaBytes} MB",
style = MaterialTheme.typography.labelSmall
)
}
@@ -199,7 +200,7 @@ fun SubStep(
}
@Composable
-fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
+fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
val strokeWidth = Dp(floor(size.value / 10) + 1)
when (state) {
@@ -233,8 +234,15 @@ fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
contentDescription = description
}
},
- progress = { progress?.let { (current, total) -> current / total } },
+ progress = {
+ progress?.let { (current, total) ->
+ if (total == null) return@let null
+ current / total
+ }?.toFloat()
+ },
strokeWidth = strokeWidth
)
}
-}
\ No newline at end of file
+}
+
+private val Long.megaBytes get() = "%.1f".format(locale = Locale.ROOT, toDouble() / 1_000_000)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt
index 2d40dda7..7c680477 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt
@@ -22,13 +22,36 @@ fun SettingsListItem(
colors: ListItemColors = ListItemDefaults.colors(),
tonalElevation: Dp = ListItemDefaults.Elevation,
shadowElevation: Dp = ListItemDefaults.Elevation,
-) = ListItem(
+) = SettingsListItem(
headlineContent = {
Text(
text = headlineContent,
style = MaterialTheme.typography.titleLarge
)
},
+ modifier = modifier,
+ overlineContent = overlineContent,
+ supportingContent = supportingContent,
+ leadingContent = leadingContent,
+ trailingContent = trailingContent,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+)
+
+@Composable
+fun SettingsListItem(
+ headlineContent: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ overlineContent: @Composable (() -> Unit)? = null,
+ supportingContent: String? = null,
+ leadingContent: @Composable (() -> Unit)? = null,
+ trailingContent: @Composable (() -> Unit)? = null,
+ colors: ListItemColors = ListItemDefaults.colors(),
+ tonalElevation: Dp = ListItemDefaults.Elevation,
+ shadowElevation: Dp = ListItemDefaults.Elevation,
+) = ListItem(
+ headlineContent = headlineContent,
modifier = modifier.then(Modifier.padding(horizontal = 8.dp)),
overlineContent = overlineContent,
supportingContent = {
diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
index e15bdfb6..93c59411 100644
--- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
+++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
@@ -22,9 +22,6 @@ sealed interface Destination : Parcelable {
@Parcelize
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
- @Parcelize
- data class VersionSelector(val packageName: String, val patchSelection: PatchSelection? = null) : Destination
-
@Parcelize
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination
diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt
index a1fafa32..9a1f3e29 100644
--- a/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt
+++ b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt
@@ -13,7 +13,4 @@ sealed interface SelectedAppInfoDestination : Parcelable {
@Parcelize
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
-
- @Parcelize
- data object VersionSelector: SelectedAppInfoDestination
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
index e8dc938d..e2bd8b1e 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
@@ -49,7 +49,7 @@ data class BundleInfo(
bundle.uid to patches
}
- fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) =
+ fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
sources.flatMapLatestAndCombine(
combiner = { it.filterNotNull() }
) { source ->
@@ -64,7 +64,7 @@ data class BundleInfo(
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
val targetList = when {
it.compatiblePackages == null -> universal
- it.supportsVersion(
+ it.supports(
packageName,
version
) -> supported
diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
index 4c7fc417..c08c823e 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
@@ -19,5 +19,5 @@ data class Step(
val category: StepCategory,
val state: State = State.WAITING,
val message: String? = null,
- val downloadProgress: StateFlow?>? = null
+ val downloadProgress: StateFlow?>? = null
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
index 4e3e8807..5d05c4ea 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt
@@ -1,20 +1,35 @@
package app.revanced.manager.ui.model
import android.os.Parcelable
-import app.revanced.manager.network.downloader.AppDownloader
+import app.revanced.manager.network.downloader.ParceledDownloaderData
import kotlinx.parcelize.Parcelize
import java.io.File
-sealed class SelectedApp : Parcelable {
- abstract val packageName: String
- abstract val version: String
+sealed interface SelectedApp : Parcelable {
+ val packageName: String
+ val version: String?
@Parcelize
- data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp()
+ data class Download(
+ override val packageName: String,
+ override val version: String?,
+ val data: ParceledDownloaderData
+ ) : SelectedApp
@Parcelize
- data class Local(override val packageName: String, override val version: String, val file: File, val temporary: Boolean) : SelectedApp()
+ data class Search(override val packageName: String, override val version: String?) : SelectedApp
@Parcelize
- data class Installed(override val packageName: String, override val version: String) : SelectedApp()
+ data class Local(
+ override val packageName: String,
+ override val version: String,
+ val file: File,
+ val temporary: Boolean
+ ) : SelectedApp
+
+ @Parcelize
+ data class Installed(
+ override val packageName: String,
+ override val version: String
+ ) : SelectedApp
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
index eb814c3b..ce6a13ce 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -33,19 +32,20 @@ import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE
+import app.revanced.manager.util.EventEffect
import app.revanced.manager.util.transparentListItemColors
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppSelectorScreen(
- onAppClick: (packageName: String) -> Unit,
- onStorageClick: (SelectedApp.Local) -> Unit,
+ onSelect: (String) -> Unit,
+ onStorageSelect: (SelectedApp.Local) -> Unit,
onBackClick: () -> Unit,
vm: AppSelectorViewModel = koinViewModel()
) {
- SideEffect {
- vm.onStorageClick = onStorageClick
+ EventEffect(flow = vm.storageSelectionFlow) {
+ onStorageSelect(it)
}
val pickApkLauncher =
@@ -75,7 +75,7 @@ fun AppSelectorScreen(
)
}
- if (search) {
+ if (search)
SearchView(
query = filterText,
onQueryChange = { filterText = it },
@@ -83,15 +83,15 @@ fun AppSelectorScreen(
placeholder = { Text(stringResource(R.string.search_apps)) }
) {
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
- LazyColumnWithScrollbar(
- modifier = Modifier.fillMaxSize()
- ) {
+ LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
items(
items = filteredAppList,
key = { it.packageName }
) { app ->
ListItem(
- modifier = Modifier.clickable { onAppClick(app.packageName) },
+ modifier = Modifier.clickable {
+ onSelect(app.packageName)
+ },
leadingContent = {
AppIcon(
app.packageInfo,
@@ -125,17 +125,18 @@ fun AppSelectorScreen(
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = stringResource(R.string.search),
- modifier = Modifier.size(64.dp)
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.type_anything),
- style = MaterialTheme.typography.bodyLarge
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
- }
Scaffold(
topBar = {
@@ -184,7 +185,9 @@ fun AppSelectorScreen(
key = { it.packageName }
) { app ->
ListItem(
- modifier = Modifier.clickable { onAppClick(app.packageName) },
+ modifier = Modifier.clickable {
+ onSelect(app.packageName)
+ },
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = {
AppLabel(
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
index 4dc74766..6d316899 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt
@@ -5,7 +5,7 @@ import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -49,17 +49,21 @@ enum class DashboardPage(
}
@SuppressLint("BatteryLife")
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
vm: DashboardViewModel = koinViewModel(),
onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit,
+ onDownloaderPluginClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
+ val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
+ false
+ )
val androidContext = LocalContext.current
val composableScope = rememberCoroutineScope()
val pagerState = rememberPagerState(
@@ -237,6 +241,20 @@ fun DashboardScreen(
}
)
}
+ } else null,
+ if (showNewDownloaderPluginsNotification) {
+ {
+ NotificationCard(
+ text = stringResource(R.string.new_downloader_plugins_notification),
+ icon = Icons.Outlined.Download,
+ modifier = Modifier.clickable(onClick = onDownloaderPluginClick),
+ actions = {
+ TextButton(onClick = vm::ignoreNewDownloaderPlugins) {
+ Text(stringResource(R.string.dismiss))
+ }
+ }
+ )
+ }
} else null
)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
index 239aebbf..9ddf2ef8 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt
@@ -41,13 +41,12 @@ 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.PatchSelection
import app.revanced.manager.util.toast
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InstalledAppInfoScreen(
- onPatchClick: (packageName: String, patchSelection: PatchSelection) -> Unit,
+ onPatchClick: (packageName: String) -> Unit,
onBackClick: () -> Unit,
viewModel: InstalledAppInfoViewModel
) {
@@ -134,9 +133,7 @@ fun InstalledAppInfoScreen(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
- viewModel.appliedPatches?.let {
- onPatchClick(viewModel.installedApp.originalPackageName, it)
- }
+ onPatchClick(viewModel.installedApp.originalPackageName)
},
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
index 6aedde2f..5df81ed3 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
@@ -2,6 +2,7 @@ package app.revanced.manager.ui.screen
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
@@ -38,6 +39,7 @@ import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE
+import app.revanced.manager.util.EventEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -86,6 +88,38 @@ fun PatcherScreen(
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
InstallerStatusDialog(vm.installerStatusDialogModel)
+ val activityLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult(),
+ onResult = vm::handleActivityResult
+ )
+ EventEffect(flow = vm.launchActivityFlow) { intent ->
+ activityLauncher.launch(intent)
+ }
+
+ vm.activityPromptDialog?.let { title ->
+ AlertDialog(
+ onDismissRequest = vm::rejectInteraction,
+ confirmButton = {
+ TextButton(
+ onClick = vm::allowInteraction
+ ) {
+ Text(stringResource(R.string.continue_))
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = vm::rejectInteraction
+ ) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ title = { Text(title) },
+ text = {
+ Text(stringResource(R.string.plugin_activity_dialog_body))
+ }
+ )
+ }
+
AppScaffold(
topBar = {
AppTopBar(
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
index 511a1c36..e38f320b 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
@@ -1,6 +1,5 @@
package app.revanced.manager.ui.screen
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
@@ -49,7 +48,7 @@ import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit,
@@ -137,7 +136,7 @@ fun PatchesSelectorScreen(
if (vm.compatibleVersions.isNotEmpty())
UnsupportedPatchDialog(
- appVersion = vm.appVersion,
+ appVersion = vm.appVersion ?: stringResource(R.string.any_version),
supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs
)
@@ -146,7 +145,7 @@ fun PatchesSelectorScreen(
}
if (showUnsupportedPatchesDialog)
UnsupportedPatchesDialog(
- appVersion = vm.appVersion,
+ appVersion = vm.appVersion ?: stringResource(R.string.any_version),
onDismissRequest = { showUnsupportedPatchesDialog = false }
)
@@ -204,15 +203,15 @@ fun PatchesSelectorScreen(
when {
// Open unsupported dialog if the patch is not supported
!supported -> vm.openUnsupportedDialog(patch)
-
+
// Show selection warning if enabled
vm.selectionWarningEnabled -> showSelectionWarning = true
-
+
// Set pending universal patch action if the universal patch warning is enabled and there are no compatible packages
vm.universalPatchWarningEnabled && patch.compatiblePackages == null -> {
vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) }
}
-
+
// Toggle the patch otherwise
else -> vm.togglePatch(uid, patch)
}
@@ -275,7 +274,11 @@ fun PatchesSelectorScreen(
Scaffold(
topBar = {
AppTopBar(
- title = stringResource(R.string.patches_selected, selectedPatchCount, availablePatchCount),
+ title = stringResource(
+ R.string.patches_selected,
+ selectedPatchCount,
+ availablePatchCount
+ ),
onBackClick = onBackClick,
actions = {
IconButton(onClick = vm::reset) {
@@ -436,7 +439,7 @@ private fun PatchItem(
selected: Boolean,
onToggle: () -> Unit,
supported: Boolean = true
-) = ListItem (
+) = ListItem(
modifier = Modifier
.let { if (!supported) it.alpha(0.5f) else it }
.clickable(onClick = onToggle)
@@ -457,6 +460,7 @@ private fun PatchItem(
}
}
},
+ colors = transparentListItemColors
)
@Composable
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
index 9c3f59d4..9fb48372 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
@@ -1,10 +1,14 @@
package app.revanced.manager.ui.screen
-import android.content.pm.PackageInfo
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh
@@ -19,22 +23,31 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
+import app.revanced.manager.data.room.apps.installed.InstallType
+import app.revanced.manager.data.room.apps.installed.InstalledApp
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
+import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
+import app.revanced.manager.util.EventEffect
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
+import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast
+import app.revanced.manager.util.transparentListItemColors
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectedAppInfoScreen(
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
@@ -61,56 +74,137 @@ fun SelectedAppInfoScreen(
}
}
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult(),
+ onResult = vm::handlePluginActivityResult
+ )
+ EventEffect(flow = vm.launchActivityFlow) { intent ->
+ launcher.launch(intent)
+ }
+
val navController =
rememberNavController(startDestination = SelectedAppInfoDestination.Main)
NavBackHandler(controller = navController)
AnimatedNavHost(controller = navController) { destination ->
+ val error by vm.errorFlow.collectAsStateWithLifecycle(null)
when (destination) {
- is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen(
- onPatchClick = patchClick@{
- if (selectedPatchCount == 0) {
- context.toast(context.getString(R.string.no_patches_selected))
-
- return@patchClick
- }
- onPatchClick(
- vm.selectedApp,
- patches,
- vm.getOptionsFiltered(bundles)
+ is SelectedAppInfoDestination.Main -> Scaffold(
+ topBar = {
+ AppTopBar(
+ title = stringResource(R.string.app_info),
+ onBackClick = onBackClick
)
},
- onPatchSelectorClick = {
- navController.navigate(
- SelectedAppInfoDestination.PatchesSelector(
- vm.selectedApp,
- vm.getCustomPatches(
- bundles,
- allowIncompatiblePatches
- ),
- vm.options
+ floatingActionButton = {
+ if (error != null) return@Scaffold
+
+ HapticExtendedFloatingActionButton(
+ text = { Text(stringResource(R.string.patch)) },
+ icon = {
+ Icon(
+ Icons.Default.AutoFixHigh,
+ stringResource(R.string.patch)
+ )
+ },
+ onClick = patchClick@{
+ if (selectedPatchCount == 0) {
+ context.toast(context.getString(R.string.no_patches_selected))
+
+ return@patchClick
+ }
+ onPatchClick(
+ vm.selectedApp,
+ patches,
+ vm.getOptionsFiltered(bundles)
+ )
+ }
+ )
+ }
+ ) { paddingValues ->
+ val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
+
+ if (vm.showSourceSelector) {
+ val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
+
+ AppSourceSelectorDialog(
+ plugins = plugins,
+ installedApp = vm.installedAppData,
+ searchApp = SelectedApp.Search(
+ vm.packageName,
+ vm.desiredVersion
+ ),
+ activeSearchJob = vm.activePluginAction,
+ hasRoot = vm.hasRoot,
+ onDismissRequest = vm::dismissSourceSelector,
+ onSelectPlugin = vm::searchUsingPlugin,
+ requiredVersion = requiredVersion,
+ onSelect = {
+ vm.selectedApp = it
+ vm.dismissSourceSelector()
+ }
+ )
+ }
+
+ ColumnWithScrollbar(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
+ Text(
+ version ?: stringResource(R.string.selected_app_meta_any_version),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium,
)
- )
- },
- onVersionSelectorClick = {
- navController.navigate(SelectedAppInfoDestination.VersionSelector)
- },
- onBackClick = onBackClick,
- selectedPatchCount = selectedPatchCount,
- packageName = packageName,
- version = version,
- packageInfo = vm.selectedAppInfo,
- )
+ }
- is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
- onBackClick = navController::pop,
- onAppClick = {
- vm.selectedApp = it
- navController.pop()
- },
- viewModel = koinViewModel { parametersOf(packageName) }
- )
+ PageItem(
+ R.string.patch_selector_item,
+ stringResource(
+ R.string.patch_selector_item_description,
+ selectedPatchCount
+ ),
+ onClick = {
+ navController.navigate(
+ SelectedAppInfoDestination.PatchesSelector(
+ vm.selectedApp,
+ vm.getCustomPatches(
+ bundles,
+ allowIncompatiblePatches
+ ),
+ vm.options
+ )
+ )
+ }
+ )
+ PageItem(
+ R.string.apk_source_selector_item,
+ when (val app = vm.selectedApp) {
+ is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
+ is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
+ is SelectedApp.Download -> stringResource(
+ R.string.apk_source_downloader,
+ plugins.find { it.packageName == app.data.pluginPackageName }?.name
+ ?: app.data.pluginPackageName
+ )
+
+ is SelectedApp.Local -> stringResource(R.string.apk_source_local)
+ },
+ onClick = {
+ vm.showSourceSelector()
+ }
+ )
+ error?.let {
+ Text(
+ stringResource(it.resourceId),
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(horizontal = 24.dp)
+ )
+ }
+ }
+ }
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
@@ -132,65 +226,6 @@ fun SelectedAppInfoScreen(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun SelectedAppInfoScreen(
- onPatchClick: () -> Unit,
- onPatchSelectorClick: () -> Unit,
- onVersionSelectorClick: () -> Unit,
- onBackClick: () -> Unit,
- selectedPatchCount: Int,
- packageName: String,
- version: String,
- packageInfo: PackageInfo?,
-) {
- Scaffold(
- topBar = {
- AppTopBar(
- title = stringResource(R.string.app_info),
- onBackClick = onBackClick
- )
- },
- floatingActionButton = {
- HapticExtendedFloatingActionButton(
- text = { Text(stringResource(R.string.patch)) },
- icon = {
- Icon(
- Icons.Default.AutoFixHigh,
- stringResource(R.string.patch)
- )
- },
- onClick = onPatchClick
- )
- }
- ) { paddingValues ->
- ColumnWithScrollbar(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- ) {
- AppInfo(packageInfo, placeholderLabel = packageName) {
- Text(
- stringResource(R.string.selected_app_meta, version),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.bodyMedium,
- )
- }
-
- PageItem(
- R.string.patch_selector_item,
- stringResource(R.string.patch_selector_item_description, selectedPatchCount),
- onPatchSelectorClick
- )
- PageItem(
- R.string.version_selector_item,
- stringResource(R.string.version_selector_item_description, version),
- onVersionSelectorClick
- )
- }
- }
-}
-
@Composable
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
ListItem(
@@ -215,4 +250,89 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
}
)
+}
+
+@Composable
+private fun AppSourceSelectorDialog(
+ plugins: List,
+ installedApp: Pair?,
+ searchApp: SelectedApp.Search,
+ activeSearchJob: String?,
+ hasRoot: Boolean,
+ requiredVersion: String?,
+ onDismissRequest: () -> Unit,
+ onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
+ onSelect: (SelectedApp) -> Unit,
+) {
+ val canSelect = activeSearchJob == null
+
+ AlertDialogExtended(
+ onDismissRequest = onDismissRequest,
+ confirmButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ title = { Text(stringResource(R.string.app_source_dialog_title)) },
+ textHorizontalPadding = PaddingValues(horizontal = 0.dp),
+ text = {
+ LazyColumn {
+ item(key = "auto") {
+ val hasPlugins = plugins.isNotEmpty()
+ ListItem(
+ modifier = Modifier
+ .clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
+ .enabled(hasPlugins),
+ headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
+ supportingContent = {
+ Text(
+ if (hasPlugins)
+ stringResource(R.string.app_source_dialog_option_auto_description)
+ else
+ stringResource(R.string.app_source_dialog_option_auto_unavailable)
+ )
+ },
+ colors = transparentListItemColors
+ )
+ }
+
+ installedApp?.let { (app, meta) ->
+ item(key = "installed") {
+ val (usable, text) = when {
+ // Mounted apps must be unpatched before patching, which cannot be done without root access.
+ meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource(
+ R.string.app_source_dialog_option_installed_no_root
+ )
+ // Patching already patched apps is not allowed because patches expect unpatched apps.
+ meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
+ // Version does not match suggested version.
+ requiredVersion != null && app.version != requiredVersion -> false to stringResource(
+ R.string.app_source_dialog_option_installed_version_not_suggested,
+ app.version
+ )
+
+ else -> true to app.version
+ }
+ ListItem(
+ modifier = Modifier
+ .clickable(enabled = canSelect && usable) { onSelect(app) }
+ .enabled(usable),
+ headlineContent = { Text(stringResource(R.string.installed)) },
+ supportingContent = { Text(text) },
+ colors = transparentListItemColors
+ )
+ }
+ }
+
+ items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
+ ListItem(
+ modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
+ headlineContent = { Text(plugin.name) },
+ trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
+ colors = transparentListItemColors
+ )
+ }
+ }
+ }
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
deleted file mode 100644
index a8d12d50..00000000
--- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package app.revanced.manager.ui.screen
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.res.pluralStringResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import app.revanced.manager.R
-import app.revanced.manager.data.room.apps.installed.InstallType
-import app.revanced.manager.ui.component.AppTopBar
-import app.revanced.manager.ui.component.GroupHeader
-import app.revanced.manager.ui.component.LazyColumnWithScrollbar
-import app.revanced.manager.ui.component.LoadingIndicator
-import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
-import app.revanced.manager.ui.component.haptics.HapticRadioButton
-import app.revanced.manager.ui.component.NonSuggestedVersionDialog
-import app.revanced.manager.ui.model.SelectedApp
-import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
-import app.revanced.manager.util.isScrollingUp
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun VersionSelectorScreen(
- onBackClick: () -> Unit,
- onAppClick: (SelectedApp) -> Unit,
- viewModel: VersionSelectorViewModel
-) {
- val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap())
- val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
-
- val list by remember {
- derivedStateOf {
- val apps = (downloadedVersions + viewModel.downloadableVersions)
- .distinctBy { it.version }
- .sortedWith(
- compareByDescending {
- it is SelectedApp.Local
- }.thenByDescending { supportedVersions[it.version] }
- .thenByDescending { it.version }
- )
-
- viewModel.requiredVersion?.let { requiredVersion ->
- apps.filter { it.version == requiredVersion }
- } ?: apps
- }
- }
-
- if (viewModel.showNonSuggestedVersionDialog)
- NonSuggestedVersionDialog(
- suggestedVersion = viewModel.requiredVersion.orEmpty(),
- onDismiss = viewModel::dismissNonSuggestedVersionDialog
- )
-
- val lazyListState = rememberLazyListState()
- Scaffold(
- topBar = {
- AppTopBar(
- title = stringResource(R.string.select_version),
- onBackClick = onBackClick,
- )
- },
- floatingActionButton = {
- HapticExtendedFloatingActionButton(
- text = { Text(stringResource(R.string.select_version)) },
- icon = {
- Icon(
- Icons.Default.Check,
- stringResource(R.string.select_version)
- )
- },
- expanded = lazyListState.isScrollingUp,
- onClick = { viewModel.selectedVersion?.let(onAppClick) }
- )
- }
- ) { paddingValues ->
- LazyColumnWithScrollbar(
- modifier = Modifier
- .padding(paddingValues)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- state = lazyListState
- ) {
- viewModel.installedApp?.let { (packageInfo, installedApp) ->
- SelectedApp.Installed(
- packageName = viewModel.packageName,
- version = packageInfo.versionName!!
- ).let {
- item {
- SelectedAppItem(
- selectedApp = it,
- selected = viewModel.selectedVersion == it,
- onClick = { viewModel.select(it) },
- patchCount = supportedVersions[it.version],
- enabled =
- !(installedApp?.installType == InstallType.MOUNT && !viewModel.rootInstaller.hasRootAccess()),
- alreadyPatched = installedApp != null && installedApp.installType != InstallType.MOUNT
- )
- }
- }
- }
-
- item {
- Row(Modifier.fillMaxWidth()) {
- GroupHeader(stringResource(R.string.downloadable_versions))
- }
- }
-
- items(
- items = list,
- key = { it.version }
- ) {
- SelectedAppItem(
- selectedApp = it,
- selected = viewModel.selectedVersion == it,
- onClick = { viewModel.select(it) },
- patchCount = supportedVersions[it.version]
- )
- }
-
- if (viewModel.errorMessage != null) {
- item {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(stringResource(R.string.error_occurred))
- Text(
- text = viewModel.errorMessage!!,
- modifier = Modifier.padding(horizontal = 15.dp)
- )
- }
- }
- } else if (viewModel.isLoading) {
- item {
- LoadingIndicator()
- }
- }
- }
- }
-}
-
-@Composable
-fun SelectedAppItem(
- selectedApp: SelectedApp,
- selected: Boolean,
- onClick: () -> Unit,
- patchCount: Int?,
- enabled: Boolean = true,
- alreadyPatched: Boolean = false,
-) {
- ListItem(
- leadingContent = { HapticRadioButton(selected, null) },
- headlineContent = { Text(selectedApp.version) },
- supportingContent = when (selectedApp) {
- is SelectedApp.Installed ->
- if (alreadyPatched) {
- { Text(stringResource(R.string.already_patched)) }
- } else {
- { Text(stringResource(R.string.installed)) }
- }
-
- is SelectedApp.Local -> {
- { Text(stringResource(R.string.already_downloaded)) }
- }
-
- else -> null
- },
- trailingContent = patchCount?.let {
- {
- Text(pluralStringResource(R.plurals.patch_count, it, it))
- }
- },
- modifier = Modifier
- .clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
- .run {
- if (!enabled || alreadyPatched) alpha(0.5f)
- else this
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
index 41e80b40..68e02942 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt
@@ -1,39 +1,60 @@
package app.revanced.manager.ui.screen.settings
+import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
+import androidx.compose.material3.pulltorefresh.pullToRefresh
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
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
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
+import app.revanced.manager.network.downloader.DownloaderPluginState
+import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
-import app.revanced.manager.ui.component.ColumnWithScrollbar
+import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.GroupHeader
+import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.haptics.HapticCheckbox
-import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import org.koin.androidx.compose.koinViewModel
+import java.security.MessageDigest
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class)
@Composable
fun DownloadsSettingsScreen(
onBackClick: () -> Unit,
viewModel: DownloadsViewModel = koinViewModel()
) {
- val prefs = viewModel.prefs
-
- val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(initialValue = emptyList())
+ val pullRefreshState = rememberPullToRefreshState()
+ val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
+ val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
Scaffold(
topBar = {
@@ -41,8 +62,8 @@ fun DownloadsSettingsScreen(
title = stringResource(R.string.downloads),
onBackClick = onBackClick,
actions = {
- if (viewModel.selection.isNotEmpty()) {
- IconButton(onClick = { viewModel.delete() }) {
+ if (viewModel.appSelection.isNotEmpty()) {
+ IconButton(onClick = { viewModel.deleteApps() }) {
Icon(Icons.Default.Delete, stringResource(R.string.delete))
}
}
@@ -50,35 +71,178 @@ fun DownloadsSettingsScreen(
)
}
) { paddingValues ->
- ColumnWithScrollbar(
+ Box(
+ contentAlignment = Alignment.TopCenter,
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxWidth()
+ .zIndex(1f)
+ ) {
+ PullToRefreshDefaults.Indicator(
+ state = pullRefreshState,
+ isRefreshing = viewModel.isRefreshingPlugins
+ )
+ }
+
+ LazyColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
+ .pullToRefresh(
+ isRefreshing = viewModel.isRefreshingPlugins,
+ state = pullRefreshState,
+ onRefresh = viewModel::refreshPlugins
+ )
) {
- BooleanItem(
- preference = prefs.preferSplits,
- headline = R.string.prefer_splits,
- description = R.string.prefer_splits_description,
- )
+ item {
+ GroupHeader(stringResource(R.string.downloader_plugins))
+ }
+ pluginStates.forEach { (packageName, state) ->
+ item(key = packageName) {
+ var showDialog by rememberSaveable {
+ mutableStateOf(false)
+ }
- GroupHeader(stringResource(R.string.downloaded_apps))
+ fun dismiss() {
+ showDialog = false
+ }
- downloadedApps.forEach { app ->
- val selected = app in viewModel.selection
+ val packageInfo =
+ remember(packageName) {
+ viewModel.pm.getPackageInfo(
+ packageName
+ )
+ } ?: return@item
+
+ if (showDialog) {
+ val signature =
+ remember(packageName) {
+ val androidSignature =
+ viewModel.pm.getSignature(packageName)
+ val hash = MessageDigest.getInstance("SHA-256")
+ .digest(androidSignature.toByteArray())
+ hash.toHexString(format = HexFormat.UpperCase)
+ }
+
+ when (state) {
+ is DownloaderPluginState.Loaded -> TrustDialog(
+ title = R.string.downloader_plugin_revoke_trust_dialog_title,
+ body = stringResource(
+ R.string.downloader_plugin_trust_dialog_body,
+ packageName,
+ signature
+ ),
+ onDismiss = ::dismiss,
+ onConfirm = {
+ viewModel.revokePluginTrust(packageName)
+ dismiss()
+ }
+ )
+
+ is DownloaderPluginState.Failed -> ExceptionViewerDialog(
+ text = remember(state.throwable) {
+ state.throwable.stackTraceToString()
+ },
+ onDismiss = ::dismiss
+ )
+
+ is DownloaderPluginState.Untrusted -> TrustDialog(
+ title = R.string.downloader_plugin_trust_dialog_title,
+ body = stringResource(
+ R.string.downloader_plugin_trust_dialog_body,
+ packageName,
+ signature
+ ),
+ onDismiss = ::dismiss,
+ onConfirm = {
+ viewModel.trustPlugin(packageName)
+ dismiss()
+ }
+ )
+ }
+ }
+
+ SettingsListItem(
+ modifier = Modifier.clickable { showDialog = true },
+ headlineContent = {
+ AppLabel(
+ packageInfo = packageInfo,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ supportingContent = stringResource(
+ when (state) {
+ is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
+ is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
+ is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
+ }
+ ),
+ trailingContent = { Text(packageInfo.versionName!!) }
+ )
+ }
+ }
+ if (pluginStates.isEmpty()) {
+ item {
+ Text(
+ stringResource(R.string.downloader_no_plugins_installed),
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+
+ item {
+ GroupHeader(stringResource(R.string.downloaded_apps))
+ }
+ items(downloadedApps, key = { it.packageName to it.version }) { app ->
+ val selected = app in viewModel.appSelection
SettingsListItem(
- modifier = Modifier.clickable { viewModel.toggleItem(app) },
+ modifier = Modifier.clickable { viewModel.toggleApp(app) },
headlineContent = app.packageName,
leadingContent = (@Composable {
HapticCheckbox(
checked = selected,
- onCheckedChange = { viewModel.toggleItem(app) }
+ onCheckedChange = { viewModel.toggleApp(app) }
)
- }).takeIf { viewModel.selection.isNotEmpty() },
+ }).takeIf { viewModel.appSelection.isNotEmpty() },
supportingContent = app.version,
tonalElevation = if (selected) 8.dp else 0.dp
)
}
+ if (downloadedApps.isEmpty()) {
+ item {
+ Text(
+ stringResource(R.string.downloader_settings_no_apps),
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
}
}
+}
+
+@Composable
+private fun TrustDialog(
+ @StringRes title: Int,
+ body: String,
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(stringResource(R.string.continue_))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.dismiss))
+ }
+ },
+ title = { Text(stringResource(title)) },
+ text = { Text(body) }
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
index 2acfdcd5..eaa66f47 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
@@ -14,7 +14,9 @@ import app.revanced.manager.ui.model.SelectedApp
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.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@@ -30,7 +32,8 @@ class AppSelectorViewModel(
}
val appList = pm.appList
- var onStorageClick: (SelectedApp.Local) -> Unit = {}
+ private val storageSelectionChannel = Channel()
+ val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
@@ -54,7 +57,7 @@ class AppSelectorViewModel(
}
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
- onStorageClick(selectedApp)
+ storageSelectionChannel.send(selectedApp)
} else {
nonSuggestedVersionDialogSubject = selectedApp
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
index 5a019c51..99be81ec 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
@@ -17,6 +17,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.toast
@@ -28,6 +29,7 @@ import kotlinx.coroutines.launch
class DashboardViewModel(
private val app: Application,
private val patchBundleRepository: PatchBundleRepository,
+ private val downloaderPluginRepository: DownloaderPluginRepository,
private val reVancedAPI: ReVancedAPI,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager
@@ -39,6 +41,8 @@ class DashboardViewModel(
val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf()
+ val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
+
var updatedManagerVersion: String? by mutableStateOf(null)
private set
var showBatteryOptimizationsWarning by mutableStateOf(false)
@@ -52,6 +56,10 @@ class DashboardViewModel(
}
}
+ fun ignoreNewDownloaderPlugins() = viewModelScope.launch {
+ downloaderPluginRepository.acknowledgeAllNewPlugins()
+ }
+
fun dismissUpdateDialog() {
updatedManagerVersion = null
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt
index 4688cf16..e2c750df 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt
@@ -1,10 +1,15 @@
package app.revanced.manager.ui.viewmodel
+import android.content.pm.PackageInfo
+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.downloaded.DownloadedApp
-import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
+import app.revanced.manager.util.PM
import app.revanced.manager.util.mutableStateSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
@@ -14,8 +19,10 @@ import kotlinx.coroutines.withContext
class DownloadsViewModel(
private val downloadedAppRepository: DownloadedAppRepository,
- val prefs: PreferencesManager
+ private val downloaderPluginRepository: DownloaderPluginRepository,
+ val pm: PM
) : ViewModel() {
+ val downloaderPluginStates = downloaderPluginRepository.pluginStates
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
downloadedApps.sortedWith(
compareBy {
@@ -23,24 +30,39 @@ class DownloadsViewModel(
}.thenBy { it.version }
)
}
+ val appSelection = mutableStateSetOf()
- val selection = mutableStateSetOf()
+ var isRefreshingPlugins by mutableStateOf(false)
+ private set
- fun toggleItem(downloadedApp: DownloadedApp) {
- if (selection.contains(downloadedApp))
- selection.remove(downloadedApp)
+ fun toggleApp(downloadedApp: DownloadedApp) {
+ if (appSelection.contains(downloadedApp))
+ appSelection.remove(downloadedApp)
else
- selection.add(downloadedApp)
+ appSelection.add(downloadedApp)
}
- fun delete() {
+ fun deleteApps() {
viewModelScope.launch(NonCancellable) {
- downloadedAppRepository.delete(selection)
+ downloadedAppRepository.delete(appSelection)
withContext(Dispatchers.Main) {
- selection.clear()
+ appSelection.clear()
}
}
}
+ fun refreshPlugins() = viewModelScope.launch {
+ isRefreshingPlugins = true
+ downloaderPluginRepository.reload()
+ isRefreshingPlugins = false
+ }
+
+ fun trustPlugin(packageName: String) = viewModelScope.launch {
+ downloaderPluginRepository.trustPackage(packageName)
+ }
+
+ fun revokePluginTrust(packageName: String) = viewModelScope.launch {
+ downloaderPluginRepository.revokeTrustForPackage(packageName)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
index 1b9aab30..d665efb0 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt
@@ -15,13 +15,17 @@ import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SerializedSelection
+import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -29,10 +33,40 @@ import kotlinx.serialization.json.Json
class MainViewModel(
private val patchBundleRepository: PatchBundleRepository,
private val patchSelectionRepository: PatchSelectionRepository,
+ private val downloadedAppRepository: DownloadedAppRepository,
private val keystoreManager: KeystoreManager,
private val app: Application,
val prefs: PreferencesManager
) : ViewModel() {
+ private val appSelectChannel = Channel()
+ val appSelectFlow = appSelectChannel.receiveAsFlow()
+
+ private suspend fun suggestedVersion(packageName: String) =
+ patchBundleRepository.suggestedVersions.first()[packageName]
+
+ private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? {
+ if (app !is SelectedApp.Search) return null
+
+ val suggestedVersion = suggestedVersion(app.packageName) ?: return null
+
+ val downloadedApp =
+ downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true) ?: return null
+ return SelectedApp.Local(
+ downloadedApp.packageName,
+ downloadedApp.version,
+ downloadedAppRepository.getApkFileForApp(downloadedApp),
+ false
+ )
+ }
+
+ fun selectApp(app: SelectedApp) = viewModelScope.launch {
+ appSelectChannel.send(findDownloadedApp(app) ?: app)
+ }
+
+ fun selectApp(packageName: String) = viewModelScope.launch {
+ selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
+ }
+
fun importLegacySettings(componentActivity: ComponentActivity) {
if (!prefs.firstLaunch.getBlocking()) return
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
index 00c1231f..5ab42a89 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
@@ -8,7 +8,9 @@ import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.net.Uri
import android.util.Log
+import androidx.activity.result.ActivityResult
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -29,6 +31,8 @@ import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.component.InstallerStatusDialogModel
@@ -42,11 +46,14 @@ import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext
@@ -58,6 +65,7 @@ import java.time.Duration
import java.util.UUID
@Stable
+@OptIn(PluginHostApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
) : ViewModel(), KoinComponent {
@@ -68,19 +76,20 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
- val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
- override var packageInstallerStatus: Int? by mutableStateOf(null)
+ val installerStatusDialogModel: InstallerStatusDialogModel =
+ object : InstallerStatusDialogModel {
+ override var packageInstallerStatus: Int? by mutableStateOf(null)
- override fun reinstall() {
- this@PatcherViewModel.reinstall()
- }
+ override fun reinstall() {
+ this@PatcherViewModel.reinstall()
+ }
- override fun install() {
- // Since this is a package installer status dialog,
- // InstallType.MOUNT is never used here.
- install(InstallType.DEFAULT)
+ override fun install() {
+ // Since this is a package installer status dialog,
+ // InstallType.MOUNT is never used here.
+ install(InstallType.DEFAULT)
+ }
}
- }
private var installedApp: InstalledApp? = null
val packageName: String = input.selectedApp.packageName
@@ -89,6 +98,13 @@ class PatcherViewModel(
var isInstalling by mutableStateOf(false)
private set
+ private var currentActivityRequest: Pair, String>? by mutableStateOf(null)
+ val activityPromptDialog by derivedStateOf { currentActivityRequest?.second }
+
+ private var launchedActivity: CompletableDeferred? = null
+ private val launchActivityChannel = Channel()
+ val launchActivityFlow = launchActivityChannel.receiveAsFlow()
+
private val tempDir = fs.tempDir.resolve("installer").also {
it.deleteRecursively()
it.mkdirs()
@@ -109,7 +125,7 @@ class PatcherViewModel(
}
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
- private val downloadProgress = MutableStateFlow?>(null)
+ private val downloadProgress = MutableStateFlow?>(null)
val steps = generateSteps(
app,
input.selectedApp,
@@ -130,6 +146,33 @@ class PatcherViewModel(
downloadProgress,
patchesProgress,
setInputFile = { inputFile = it },
+ handleStartActivityRequest = { plugin, intent ->
+ withContext(Dispatchers.Main) {
+ if (currentActivityRequest != null) throw Exception("Another request is already pending.")
+ try {
+ // Wait for the dialog interaction.
+ val accepted = with(CompletableDeferred()) {
+ currentActivityRequest = this to plugin.name
+
+ await()
+ }
+ if (!accepted) throw UserInteractionException.RequestDenied()
+
+ // Launch the activity and wait for the result.
+ try {
+ with(CompletableDeferred()) {
+ launchedActivity = this
+ launchActivityChannel.send(intent)
+ await()
+ }
+ } finally {
+ launchedActivity = null
+ }
+ } finally {
+ currentActivityRequest = null
+ }
+ }
+ },
onProgress = { name, state, message ->
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
@@ -173,13 +216,15 @@ class PatcherViewModel(
?.let(logger::trace)
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
+ app.toast(app.getString(R.string.install_app_success))
installedPackageName =
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
viewModelScope.launch {
installedAppRepository.addOrUpdate(
installedPackageName!!,
packageName,
- input.selectedApp.version,
+ input.selectedApp.version
+ ?: pm.getPackageInfo(outputFile)?.versionName!!,
InstallType.DEFAULT,
input.selectedPatches
)
@@ -245,6 +290,18 @@ class PatcherViewModel(
fun isDeviceRooted() = rootInstaller.isDeviceRooted()
+ fun rejectInteraction() {
+ currentActivityRequest?.first?.complete(false)
+ }
+
+ fun allowInteraction() {
+ currentActivityRequest?.first?.complete(true)
+ }
+
+ fun handleActivityResult(result: ActivityResult) {
+ launchedActivity?.complete(result)
+ }
+
fun export(uri: Uri?) = viewModelScope.launch {
uri?.let {
withContext(Dispatchers.IO) {
@@ -285,7 +342,8 @@ class PatcherViewModel(
// Check if the app version is less than the installed version
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
// Exit if the selected app version is less than the installed version
- installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
+ installerStatusDialogModel.packageInstallerStatus =
+ PackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
@@ -305,6 +363,11 @@ class PatcherViewModel(
InstallType.MOUNT -> {
try {
+ val packageInfo = pm.getPackageInfo(outputFile)
+ ?: throw Exception("Failed to load application info")
+ val label = with(pm) {
+ packageInfo.label()
+ }
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk
@@ -316,24 +379,23 @@ class PatcherViewModel(
}
}
- // Get label
- val label = with(pm) {
- currentPackageInfo.label()
- }
+ val inputVersion = input.selectedApp.version
+ ?: inputFile?.let(pm::getPackageInfo)?.versionName
+ ?: throw Exception("Failed to determine input APK version")
// Install as root
rootInstaller.install(
outputFile,
inputFile,
packageName,
- input.selectedApp.version,
+ inputVersion,
label
)
installedAppRepository.addOrUpdate(
+ packageInfo.packageName,
packageName,
- packageName,
- input.selectedApp.version,
+ inputVersion,
InstallType.MOUNT,
input.selectedPatches
)
@@ -353,7 +415,7 @@ class PatcherViewModel(
}
}
}
- } catch(e: Exception) {
+ } catch (e: Exception) {
Log.e(tag, "Failed to install", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally {
@@ -385,9 +447,10 @@ class PatcherViewModel(
fun generateSteps(
context: Context,
selectedApp: SelectedApp,
- downloadProgress: StateFlow?>? = null
+ downloadProgress: StateFlow?>? = null
): List {
- val needsDownload = selectedApp is SelectedApp.Download
+ val needsDownload =
+ selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
return listOfNotNull(
Step(
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
index 4dba4136..3e16d69e 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
@@ -1,50 +1,91 @@
package app.revanced.manager.ui.viewmodel
+import android.app.Activity
+import android.app.Application
+import android.content.Intent
import android.content.pm.PackageInfo
import android.os.Parcelable
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
+import app.revanced.manager.R
+import app.revanced.manager.data.room.apps.installed.InstalledApp
+import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
+import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.network.downloader.ParceledDownloaderData
+import app.revanced.manager.plugin.downloader.GetScope
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
+import app.revanced.manager.util.simpleMessage
+import app.revanced.manager.util.tag
+import app.revanced.manager.util.toast
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
-@OptIn(SavedStateHandleSaveableApi::class)
+@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
+ private val app: Application = get()
val bundlesRepo: PatchBundleRepository = get()
private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get()
+ private val pluginsRepository: DownloaderPluginRepository = get()
+ private val installedAppRepository: InstalledAppRepository = get()
+ private val rootInstaller: RootInstaller = get()
private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get()
+ val plugins = pluginsRepository.loadedPluginsFlow
+ val desiredVersion = input.app.version
+ val packageName = input.app.packageName
private val persistConfiguration = input.patches == null
+ val hasRoot = rootInstaller.hasRootAccess()
+ var installedAppData: Pair? by mutableStateOf(null)
+ private set
+
private var _selectedApp by savedStateHandle.saveable {
mutableStateOf(input.app)
}
+ var selectedAppInfo: PackageInfo? by mutableStateOf(null)
+ private set
+
var selectedApp
get() = _selectedApp
set(value) {
@@ -52,10 +93,27 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
invalidateSelectedAppInfo()
}
- var selectedAppInfo: PackageInfo? by mutableStateOf(null)
-
init {
invalidateSelectedAppInfo()
+ viewModelScope.launch(Dispatchers.Main) {
+ val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
+ val installedAppDeferred =
+ async(Dispatchers.IO) { installedAppRepository.get(packageName) }
+
+ installedAppData =
+ packageInfo.await()?.let {
+ SelectedApp.Installed(
+ packageName,
+ it.versionName!!
+ ) to installedAppDeferred.await()
+ }
+ }
+ }
+
+ val requiredVersion = combine(prefs.suggestedVersionSafeguard.flow, bundleRepository.suggestedVersions) { suggestedVersionSafeguard, suggestedVersions ->
+ if (!suggestedVersionSafeguard) return@combine null
+
+ suggestedVersions[input.app.packageName]
}
var options: Options by savedStateHandle.saveable {
@@ -64,9 +122,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
- val packageName =
- selectedApp.packageName // Accessing this from another thread may cause crashes.
-
state.value = withContext(Dispatchers.Default) {
val bundlePatches = bundleRepository.bundles.first()
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
@@ -89,7 +144,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
viewModelScope.launch {
if (!prefs.disableSelectionWarning.get()) return@launch
- val previous = selectionRepository.getSelection(selectedApp.packageName)
+ val previous = selectionRepository.getSelection(packageName)
if (previous.values.sumOf { it.size } == 0) return@launch
selection.value = SelectionState.Customized(previous)
}
@@ -97,11 +152,102 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
selection
}
+ var showSourceSelector by mutableStateOf(false)
+ private set
+ private var pluginAction: Pair? by mutableStateOf(null)
+ val activePluginAction get() = pluginAction?.first?.packageName
+ private var launchedActivity by mutableStateOf?>(null)
+ private val launchActivityChannel = Channel()
+ val launchActivityFlow = launchActivityChannel.receiveAsFlow()
+
+ val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
+ when {
+ app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
+ else -> null
+ }
+ }
+
+ fun showSourceSelector() {
+ dismissSourceSelector()
+ showSourceSelector = true
+ }
+
+ private fun cancelPluginAction() {
+ pluginAction?.second?.cancel()
+ pluginAction = null
+ }
+
+ fun dismissSourceSelector() {
+ cancelPluginAction()
+ showSourceSelector = false
+ }
+
+ fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
+ cancelPluginAction()
+ pluginAction = plugin to viewModelScope.launch {
+ try {
+ val scope = object : GetScope {
+ override val hostPackageName = app.packageName
+ override val pluginPackageName = plugin.packageName
+ override suspend fun requestStartActivity(intent: Intent) =
+ withContext(Dispatchers.Main) {
+ if (launchedActivity != null) error("Previous activity has not finished")
+ try {
+ val result = with(CompletableDeferred()) {
+ launchedActivity = this
+ launchActivityChannel.send(intent)
+ await()
+ }
+ when (result.resultCode) {
+ Activity.RESULT_OK -> result.data
+ Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
+ else -> throw UserInteractionException.Activity.NotCompleted(
+ result.resultCode,
+ result.data
+ )
+ }
+ } finally {
+ launchedActivity = null
+ }
+ }
+ }
+
+ withContext(Dispatchers.IO) {
+ plugin.get(scope, packageName, desiredVersion)
+ }?.let { (data, version) ->
+ if (desiredVersion != null && version != desiredVersion) {
+ app.toast(app.getString(R.string.downloader_invalid_version))
+ return@launch
+ }
+ selectedApp = SelectedApp.Download(
+ packageName,
+ version,
+ ParceledDownloaderData(plugin, data)
+ )
+ } ?: app.toast(app.getString(R.string.downloader_app_not_found))
+ } catch (e: UserInteractionException.Activity) {
+ app.toast(e.message!!)
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
+ Log.e(tag, "Downloader.get threw an exception", e)
+ } finally {
+ pluginAction = null
+ dismissSourceSelector()
+ }
+ }
+ }
+
+ fun handlePluginActivityResult(result: ActivityResult) {
+ launchedActivity?.complete(result)
+ }
+
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) {
- is SelectedApp.Download -> null
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
+ else -> null
}
selectedAppInfo = info
@@ -129,8 +275,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
this.options = filteredOptions
if (!persistConfiguration) return
-
- val packageName = selectedApp.packageName
viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName)
@@ -144,6 +288,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
val patches: PatchSelection?,
)
+ enum class Error(@StringRes val resourceId: Int) {
+ NoPlugins(R.string.downloader_no_plugins_available)
+ }
+
private companion object {
/**
* Returns a copy with all nonexistent options removed.
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
deleted file mode 100644
index d9f73264..00000000
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package app.revanced.manager.ui.viewmodel
-
-import android.content.pm.PackageInfo
-import android.util.Log
-import androidx.compose.runtime.derivedStateOf
-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.InstalledApp
-import app.revanced.manager.domain.installer.RootInstaller
-import app.revanced.manager.domain.manager.PreferencesManager
-import app.revanced.manager.domain.repository.DownloadedAppRepository
-import app.revanced.manager.domain.repository.InstalledAppRepository
-import app.revanced.manager.domain.repository.PatchBundleRepository
-import app.revanced.manager.network.downloader.APKMirror
-import app.revanced.manager.network.downloader.AppDownloader
-import app.revanced.manager.ui.model.SelectedApp
-import app.revanced.manager.util.PM
-import app.revanced.manager.util.mutableStateSetOf
-import app.revanced.manager.util.simpleMessage
-import app.revanced.manager.util.tag
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
-
-class VersionSelectorViewModel(
- val packageName: String
-) : ViewModel(), KoinComponent {
- private val downloadedAppRepository: DownloadedAppRepository by inject()
- private val installedAppRepository: InstalledAppRepository by inject()
- private val patchBundleRepository: PatchBundleRepository by inject()
- private val pm: PM by inject()
- private val prefs: PreferencesManager by inject()
- private val appDownloader: AppDownloader = APKMirror()
- val rootInstaller: RootInstaller by inject()
-
- var installedApp: Pair? by mutableStateOf(null)
- private set
- var isLoading by mutableStateOf(true)
- private set
- var errorMessage: String? by mutableStateOf(null)
- private set
-
- var requiredVersion: String? by mutableStateOf(null)
- private set
-
- var selectedVersion: SelectedApp? by mutableStateOf(null)
- private set
-
- private var nonSuggestedVersionDialogSubject by mutableStateOf(null)
- val showNonSuggestedVersionDialog by derivedStateOf { nonSuggestedVersionDialogSubject != null }
-
- private val requiredVersionAsync = viewModelScope.async(Dispatchers.Default) {
- if (!prefs.suggestedVersionSafeguard.get()) return@async null
-
- patchBundleRepository.suggestedVersions.first()[packageName]
- }
-
- val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles ->
- requiredVersionAsync.await()?.let { version ->
- // It is mandatory to use the suggested version if the safeguard is enabled.
- return@supportedVersions mapOf(
- version to bundles
- .asSequence()
- .flatMap { (_, bundle) -> bundle.patches }
- .flatMap { it.compatiblePackages.orEmpty() }
- .filter { it.packageName == packageName }
- .count { it.versions.isNullOrEmpty() || version in it.versions }
- )
- }
-
- var patchesWithoutVersions = 0
-
- bundles.flatMap { (_, bundle) ->
- bundle.patches.flatMap { patch ->
- patch.compatiblePackages.orEmpty()
- .filter { it.packageName == packageName }
- .onEach { if (it.versions == null) patchesWithoutVersions++ }
- .flatMap { it.versions.orEmpty() }
- }
- }.groupingBy { it }
- .eachCount()
- .toMutableMap()
- .apply {
- replaceAll { _, count ->
- count + patchesWithoutVersions
- }
- }
- }.flowOn(Dispatchers.Default)
-
- init {
- viewModelScope.launch {
- requiredVersion = requiredVersionAsync.await()
- }
- }
-
- val downloadableVersions = mutableStateSetOf()
-
- val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
- downloadedApps.filter { it.packageName == packageName }.map {
- SelectedApp.Local(
- it.packageName,
- it.version,
- downloadedAppRepository.getApkFileForApp(it),
- false
- )
- }
- }
-
- init {
- viewModelScope.launch(Dispatchers.Main) {
- val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
- val installedAppDeferred =
- async(Dispatchers.IO) { installedAppRepository.get(packageName) }
-
- installedApp =
- packageInfo.await()?.let {
- it to installedAppDeferred.await()
- }
- }
-
- viewModelScope.launch(Dispatchers.IO) {
- try {
- val compatibleVersions = supportedVersions.first()
-
- appDownloader.getAvailableVersions(
- packageName,
- compatibleVersions.keys
- ).collect {
- if (it.version in compatibleVersions || compatibleVersions.isEmpty()) {
- downloadableVersions.add(
- SelectedApp.Download(
- packageName,
- it.version,
- it
- )
- )
- }
- }
-
- withContext(Dispatchers.Main) {
- isLoading = false
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- Log.e(tag, "Failed to load apps", e)
- errorMessage = e.simpleMessage()
- }
- }
- }
- }
-
- fun dismissNonSuggestedVersionDialog() {
- nonSuggestedVersionDialogSubject = null
- }
-
- fun select(app: SelectedApp) {
- if (requiredVersion != null && app.version != requiredVersion) {
- nonSuggestedVersionDialogSubject = app
- return
- }
-
- selectedVersion = app
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt
index d0e6dbb4..f137e699 100644
--- a/app/src/main/java/app/revanced/manager/util/PM.kt
+++ b/app/src/main/java/app/revanced/manager/util/PM.kt
@@ -8,9 +8,10 @@ import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
-import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
+import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.PackageManager.NameNotFoundException
import androidx.core.content.pm.PackageInfoCompat
+import android.content.pm.Signature
import android.os.Build
import android.os.Parcelable
import androidx.compose.runtime.Immutable
@@ -37,7 +38,6 @@ data class AppInfo(
) : Parcelable
@SuppressLint("QueryPermissionsNeeded")
-@Suppress("DEPRECATION")
class PM(
private val app: Application,
patchBundleRepository: PatchBundleRepository
@@ -68,7 +68,7 @@ class PM(
}
val installedApps = scope.async {
- app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo ->
+ getInstalledPackages().map { packageInfo ->
AppInfo(
packageInfo.packageName,
0,
@@ -81,7 +81,7 @@ class PM(
(compatibleApps.await() + installedApps.await())
.distinctBy { it.packageName }
.sortedWith(
- compareByDescending{
+ compareByDescending {
it.packageInfo != null && (it.patches ?: 0) > 0
}.thenByDescending {
it.patches
@@ -94,9 +94,24 @@ class PM(
}
}.flowOn(Dispatchers.IO)
- fun getPackageInfo(packageName: String): PackageInfo? =
+ private fun getInstalledPackages(flags: Int = 0): List =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ app.packageManager.getInstalledPackages(PackageInfoFlags.of(flags.toLong()))
+ else
+ app.packageManager.getInstalledPackages(flags)
+
+ fun getPackagesWithFeature(feature: String) =
+ getInstalledPackages(PackageManager.GET_CONFIGURATIONS)
+ .filter { pkg ->
+ pkg.reqFeatures?.any { it.name == feature } ?: false
+ }
+
+ fun getPackageInfo(packageName: String, flags: Int = 0): PackageInfo? =
try {
- app.packageManager.getPackageInfo(packageName, 0)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong()))
+ else
+ app.packageManager.getPackageInfo(packageName, flags)
} catch (e: NameNotFoundException) {
null
}
@@ -118,6 +133,18 @@ class PM(
fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo)
+ fun getSignature(packageName: String): Signature =
+ // Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used.
+ PackageInfoCompat.getSignatures(app.packageManager, packageName).last()
+
+ @SuppressLint("InlinedApi")
+ fun hasSignature(packageName: String, signature: ByteArray) = PackageInfoCompat.hasSignatures(
+ app.packageManager,
+ packageName,
+ mapOf(signature to PackageManager.CERT_INPUT_RAW_X509),
+ false
+ )
+
suspend fun installApp(apks: List) = withContext(Dispatchers.IO) {
val packageInstaller = app.packageManager.packageInstaller
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index 5cb379e3..ac0046df 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -12,17 +12,22 @@ import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R
@@ -168,6 +173,30 @@ fun LocalDateTime.relativeTime(context: Context): String {
}
}
+private var transparentListItemColorsCached: ListItemColors? = null
+
+/**
+ * The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
+ */
+val transparentListItemColors
+ @Composable get() = transparentListItemColorsCached
+ ?: ListItemDefaults.colors(containerColor = Color.Transparent)
+ .also { transparentListItemColorsCached = it }
+
+@Composable
+fun EventEffect(flow: Flow, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val currentBlock by rememberUpdatedState(block)
+
+ LaunchedEffect(flow, state, *keys) {
+ lifecycleOwner.repeatOnLifecycle(state) {
+ flow.collect {
+ currentBlock(it)
+ }
+ }
+ }
+}
+
const val isScrollingUpSensitivity = 10
@Composable
@@ -231,12 +260,4 @@ fun ((T) -> R).withHapticFeedback(constant: Int): (T) -> R {
}
}
-private var transparentListItemColorsCached: ListItemColors? = null
-
-/**
- * The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
- */
-val transparentListItemColors
- @Composable get() = transparentListItemColorsCached
- ?: ListItemDefaults.colors(containerColor = Color.Transparent)
- .also { transparentListItemColorsCached = it }
\ No newline at end of file
+fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f)
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 20722c02..b9ba4305 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,6 +5,9 @@
CLI
Manager
+ ReVanced Manager plugin host
+ Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.
+
Copied!
Copy to clipboard
@@ -13,6 +16,7 @@
Select an app
%1$d/%2$d selected
+ New downloader plugins available. Click here to configure them.
Patching on this device architecture is unsupported and will most likely fail.
Import
@@ -31,15 +35,24 @@
Default
Unnamed
- %1$s
+ Any available version
+ Select source
+ Auto
+ Use all installed downloaders to find a suitable APK file
+ No plugins available
+ Mounted apps cannot be patched again without root access
+ Version %s does not match the suggested version
Start patching the application
Patch selection and options
%d patches selected
No patches selected
- Change version
- %s selected
+ Change source
+ Current: All downloaders
+ Current: %s
+ Current: Installed
+ Current: File
Could not import legacy settings
@@ -111,10 +124,14 @@
Resets patch options for all patches in a bundle
Reset patch options
Resets all patch options
- Prefer split APK\'s
- Prefer split APK\'s instead of full APK\'s
- Prefer universal APK\'s
- Prefer universal instead of arch-specific APK\'s
+ Plugins
+ Trusted
+ Failed to load. Click for more details
+ Untrusted
+ Trust plugin?
+ Revoke trust?
+ Package name: %1$s\nSignature (SHA-256): %2$s
+ No downloaded apps found
Search apps…
Loading…
@@ -227,10 +244,11 @@
Unpatch app?
Are you sure you want to unpatch this app?
- An error occurred
- Already downloaded
- Select version
- Downloadable versions
+ Downloader did not fetch the correct version
+ Downloader did not find the app
+ Downloader error: %s
+ No plugins installed.
+ No trusted plugins available for use. Check your settings.
Already patched
Filter
@@ -258,6 +276,7 @@
APK Saved
Failed to sign APK: %s
Save logs
+ User interaction is required in order to proceed with this plugin.
Select installation type
Preparing
@@ -383,6 +402,7 @@
Auto update
These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.
Unsupported patch
+ Any
Never show again
Show update message on launch
Shows a popup notification whenever there is a new update available on launch.
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 25bde821..59eb8c34 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -4,5 +4,7 @@
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index ca1372dd..8ed32e02 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,9 +1,16 @@
plugins {
alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
alias(libs.plugins.devtools) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.parcelize) apply false
- alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.about.libraries) apply false
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.binary.compatibility.validator)
}
+
+apiValidation {
+ ignoredProjects.addAll(listOf("app", "example-downloader-plugin"))
+ nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
+}
\ No newline at end of file
diff --git a/downloader-plugin/.gitignore b/downloader-plugin/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/downloader-plugin/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api
new file mode 100644
index 00000000..d3a22653
--- /dev/null
+++ b/downloader-plugin/api/downloader-plugin.api
@@ -0,0 +1,171 @@
+public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
+}
+
+public final class app/revanced/manager/plugin/downloader/ConstantsKt {
+ public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
+ public static final field CREATOR Landroid/os/Parcelable$Creator;
+ public fun (Ljava/lang/String;Ljava/util/Map;)V
+ public synthetic fun (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
+ public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
+ public final fun describeContents ()I
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getHeaders ()Ljava/util/Map;
+ public final fun getUrl ()Ljava/lang/String;
+ public fun hashCode ()I
+ public final fun toDownloadResult ()Lkotlin/Pair;
+ public fun toString ()Ljava/lang/String;
+ public final fun writeToParcel (Landroid/os/Parcel;I)V
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
+ public fun ()V
+ public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
+ public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+ public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
+ public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
+public final class app/revanced/manager/plugin/downloader/Downloader {
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloaderKt {
+ public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
+ public final fun download (Lkotlin/jvm/functions/Function3;)V
+ public final fun get (Lkotlin/jvm/functions/Function4;)V
+ public fun getHostPackageName ()Ljava/lang/String;
+ public fun getPluginPackageName ()Ljava/lang/String;
+ public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
+ public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
+ public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
+ public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
+ public static final field CREATOR Landroid/os/Parcelable$Creator;
+ public fun (Ljava/lang/String;Ljava/lang/String;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
+ public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
+ public final fun describeContents ()I
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getName ()Ljava/lang/String;
+ public final fun getVersion ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun writeToParcel (Landroid/os/Parcel;I)V
+}
+
+public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
+ public fun ()V
+ public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
+ public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+ public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
+ public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
+public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/Scope {
+ public abstract fun getHostPackageName ()Ljava/lang/String;
+ public abstract fun getPluginPackageName ()Ljava/lang/String;
+}
+
+public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
+ public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+}
+
+public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
+ public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+}
+
+public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
+}
+
+public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
+ public final fun getIntent ()Landroid/content/Intent;
+ public final fun getResultCode ()I
+}
+
+public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
+}
+
+public final class app/revanced/manager/plugin/downloader/webview/APIKt {
+ public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
+ public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
+ public fun ()V
+ public fun asBinder ()Landroid/os/IBinder;
+ public fun finish ()V
+ public fun load (Ljava/lang/String;)V
+}
+
+public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
+ public fun ()V
+ public fun asBinder ()Landroid/os/IBinder;
+ public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
+ public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
+}
+
+public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
+ public fun ()V
+ public fun asBinder ()Landroid/os/IBinder;
+ public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ public fun pageLoad (Ljava/lang/String;)V
+ public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
+}
+
+public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
+ public fun ()V
+ public fun asBinder ()Landroid/os/IBinder;
+ public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
+ public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
+}
+
+public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
+ public fun ()V
+ public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
+ public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+ public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
+ public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
+ public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
+ public final fun download (Lkotlin/jvm/functions/Function5;)V
+ public fun getHostPackageName ()Ljava/lang/String;
+ public fun getPluginPackageName ()Ljava/lang/String;
+ public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
+}
+
diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts
new file mode 100644
index 00000000..9d66a6e0
--- /dev/null
+++ b/downloader-plugin/build.gradle.kts
@@ -0,0 +1,61 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+ `maven-publish`
+}
+
+android {
+ namespace = "app.revanced.manager.plugin.downloader"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 26
+
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ buildFeatures {
+ aidl = true
+ }
+}
+dependencies {
+ implementation(libs.androidx.ktx)
+ implementation(libs.activity.ktx)
+ implementation(libs.runtime.ktx)
+ implementation(libs.appcompat)
+}
+
+publishing {
+ repositories {
+ mavenLocal()
+ }
+
+ publications {
+ create("release") {
+ groupId = "app.revanced"
+ artifactId = "manager-downloader-plugin"
+ version = "1.0"
+
+ afterEvaluate {
+ from(components["release"])
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/downloader-plugin/consumer-rules.pro b/downloader-plugin/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/downloader-plugin/proguard-rules.pro b/downloader-plugin/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/downloader-plugin/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/downloader-plugin/src/main/AndroidManifest.xml b/downloader-plugin/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..74b7379f
--- /dev/null
+++ b/downloader-plugin/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl
new file mode 100644
index 00000000..d657fcc3
--- /dev/null
+++ b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl
@@ -0,0 +1,8 @@
+// IWebView.aidl
+package app.revanced.manager.plugin.downloader.webview;
+
+@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
+oneway interface IWebView {
+ void load(String url);
+ void finish();
+}
\ No newline at end of file
diff --git a/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl
new file mode 100644
index 00000000..b0237de2
--- /dev/null
+++ b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl
@@ -0,0 +1,11 @@
+// IWebViewEvents.aidl
+package app.revanced.manager.plugin.downloader.webview;
+
+import app.revanced.manager.plugin.downloader.webview.IWebView;
+
+@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
+oneway interface IWebViewEvents {
+ void ready(IWebView iface);
+ void pageLoad(String url);
+ void download(String url, String mimetype, String userAgent);
+}
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt
new file mode 100644
index 00000000..469daaae
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt
@@ -0,0 +1,7 @@
+package app.revanced.manager.plugin.downloader
+
+/**
+ * The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
+ * Plugin UI activities and internal services can be protected using this permission.
+ */
+const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
new file mode 100644
index 00000000..bf0a219b
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
@@ -0,0 +1,165 @@
+package app.revanced.manager.plugin.downloader
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.app.Activity
+import android.os.Parcelable
+import kotlinx.coroutines.withTimeout
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+@RequiresOptIn(
+ level = RequiresOptIn.Level.ERROR,
+ message = "This API is only intended for plugin hosts, don't use it in a plugin.",
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class PluginHostApi
+
+/**
+ * The base interface for all DSL scopes.
+ */
+interface Scope {
+ /**
+ * The package name of ReVanced Manager.
+ */
+ val hostPackageName: String
+
+ /**
+ * The package name of the plugin.
+ */
+ val pluginPackageName: String
+}
+
+/**
+ * The scope of [DownloaderScope.get].
+ */
+interface GetScope : Scope {
+ /**
+ * Ask the user to perform some required interaction in the activity specified by the provided [Intent].
+ * This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
+ *
+ * @throws UserInteractionException.RequestDenied User decided to skip this plugin.
+ * @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
+ * @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
+ */
+ suspend fun requestStartActivity(intent: Intent): Intent?
+}
+
+interface BaseDownloadScope : Scope
+
+/**
+ * The scope for [DownloaderScope.download].
+ */
+interface InputDownloadScope : BaseDownloadScope
+
+typealias Size = Long
+typealias DownloadResult = Pair
+
+typealias Version = String
+typealias GetResult = Pair
+
+class DownloaderScope internal constructor(
+ private val scopeImpl: Scope,
+ internal val context: Context
+) : Scope by scopeImpl {
+ // Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
+ // It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
+ internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
+ internal var get: (suspend GetScope.(String, String?) -> GetResult?)? = null
+ private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
+
+ /**
+ * Define the download block of the plugin.
+ */
+ fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
+ download = { app, outputStream ->
+ val (inputStream, size) = inputDownloadScopeImpl.block(app)
+
+ inputStream.use {
+ if (size != null) reportSize(size)
+ it.copyTo(outputStream)
+ }
+ }
+ }
+
+ /**
+ * Define the get block of the plugin.
+ * The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
+ */
+ fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) {
+ get = block
+ }
+
+ /**
+ * Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
+ */
+ suspend fun useService(intent: Intent, block: suspend (IBinder) -> R): R {
+ var onBind: ((IBinder) -> Unit)? = null
+ val serviceConn = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
+ onBind!!(service!!)
+
+ override fun onServiceDisconnected(name: ComponentName?) {}
+ }
+
+ return try {
+ val binder = withTimeout(10000L) {
+ suspendCoroutine { continuation ->
+ onBind = continuation::resume
+ context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
+ }
+ }
+ block(binder)
+ } finally {
+ onBind = null
+ context.unbindService(serviceConn)
+ }
+ }
+}
+
+class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) {
+ @PluginHostApi
+ fun build(scopeImpl: Scope, context: Context) =
+ with(DownloaderScope(scopeImpl, context)) {
+ block()
+
+ Downloader(
+ download = download!!,
+ get = get!!
+ )
+ }
+}
+
+class Downloader internal constructor(
+ @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult?,
+ @property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
+)
+
+/**
+ * Define a downloader plugin.
+ */
+fun Downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block)
+
+/**
+ * @see GetScope.requestStartActivity
+ */
+sealed class UserInteractionException(message: String) : Exception(message) {
+ class RequestDenied @PluginHostApi constructor() :
+ UserInteractionException("Request denied by user")
+
+ sealed class Activity(message: String) : UserInteractionException(message) {
+ class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
+
+ /**
+ * @param resultCode The result code of the activity.
+ * @param intent The [Intent] of the activity.
+ */
+ class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
+ Activity("Unexpected activity result code: $resultCode")
+ }
+}
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt
new file mode 100644
index 00000000..a1e6bf79
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt
@@ -0,0 +1,42 @@
+package app.revanced.manager.plugin.downloader
+
+import android.app.Activity
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.os.Parcelable
+import java.io.OutputStream
+
+/**
+ * The scope of the [OutputStream] version of [DownloaderScope.download].
+ */
+interface OutputDownloadScope : BaseDownloadScope {
+ suspend fun reportSize(size: Long)
+}
+
+/**
+ * A replacement for [DownloaderScope.download] that uses [OutputStream].
+ * The provided [OutputStream] does not need to be closed manually.
+ */
+fun DownloaderScope.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
+ download = block
+}
+
+/**
+ * Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
+ * @see [GetScope.requestStartActivity]
+ */
+suspend inline fun GetScope.requestStartActivity() =
+ requestStartActivity(
+ Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
+ )
+
+/**
+ * Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
+ * @see [DownloaderScope.useService]
+ */
+suspend inline fun DownloaderScope<*>.useService(
+ noinline block: suspend (IBinder) -> R
+) = useService(
+ Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
+)
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt
new file mode 100644
index 00000000..414ad889
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt
@@ -0,0 +1,39 @@
+package app.revanced.manager.plugin.downloader
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import java.net.HttpURLConnection
+import java.net.URI
+
+/**
+ * A simple parcelable data class for storing a package name and version.
+ * This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
+ *
+ * @param name The package name.
+ * @param version The version.
+ */
+@Parcelize
+data class Package(val name: String, val version: String) : Parcelable
+
+/**
+ * A data class for storing a download URL.
+ *
+ * @param url The download URL.
+ * @param headers The headers to use for the request.
+ */
+@Parcelize
+data class DownloadUrl(val url: String, val headers: Map = emptyMap()) : Parcelable {
+ /**
+ * Converts this into a [DownloadResult].
+ */
+ fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
+ useCaches = false
+ allowUserInteraction = false
+ headers.forEach(::setRequestProperty)
+
+ connectTimeout = 10_000
+ connect()
+
+ inputStream to getHeaderField("Content-Length").toLong()
+ }
+}
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt
new file mode 100644
index 00000000..2e5034e1
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt
@@ -0,0 +1,176 @@
+package app.revanced.manager.plugin.downloader.webview
+
+import android.content.Intent
+import app.revanced.manager.plugin.downloader.DownloadUrl
+import app.revanced.manager.plugin.downloader.DownloaderScope
+import app.revanced.manager.plugin.downloader.GetScope
+import app.revanced.manager.plugin.downloader.Scope
+import app.revanced.manager.plugin.downloader.Downloader
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.withContext
+import kotlin.properties.Delegates
+
+typealias InitialUrl = String
+typealias PageLoadCallback = suspend WebViewCallbackScope.(url: String) -> Unit
+typealias DownloadCallback = suspend WebViewCallbackScope.(url: String, mimeType: String, userAgent: String) -> Unit
+
+interface WebViewCallbackScope : Scope {
+ /**
+ * Finishes the activity and returns the [result].
+ */
+ suspend fun finish(result: T)
+
+ /**
+ * Tells the WebView to load the specified [url].
+ */
+ suspend fun load(url: String)
+}
+
+@OptIn(PluginHostApi::class)
+class WebViewScope internal constructor(
+ coroutineScope: CoroutineScope,
+ private val scopeImpl: Scope,
+ setResult: (T) -> Unit
+) : Scope by scopeImpl {
+ private var onPageLoadCallback: PageLoadCallback = {}
+ private var onDownloadCallback: DownloadCallback = { _, _, _ -> }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private val dispatcher = Dispatchers.Default.limitedParallelism(1)
+ private lateinit var webView: IWebView
+ internal lateinit var initialUrl: String
+
+ internal val binder = object : IWebViewEvents.Stub() {
+ override fun ready(iface: IWebView?) {
+ coroutineScope.launch(dispatcher) {
+ webView = iface!!.also {
+ it.load(initialUrl)
+ }
+ }
+ }
+
+ override fun pageLoad(url: String?) {
+ coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
+ }
+
+ override fun download(url: String?, mimetype: String?, userAgent: String?) {
+ coroutineScope.launch(dispatcher) {
+ onDownloadCallback(
+ callbackScope,
+ url!!,
+ mimetype!!,
+ userAgent!!
+ )
+ }
+ }
+ }
+
+ private val callbackScope = object : WebViewCallbackScope, Scope by scopeImpl {
+ override suspend fun finish(result: T) {
+ setResult(result)
+ // Tell the WebViewActivity to finish
+ webView.let { withContext(Dispatchers.IO) { it.finish() } }
+ }
+
+ override suspend fun load(url: String) {
+ webView.let { withContext(Dispatchers.IO) { it.load(url) } }
+ }
+
+ }
+
+ /**
+ * Called when the WebView attempts to download a file to disk.
+ */
+ fun download(block: DownloadCallback) {
+ onDownloadCallback = block
+ }
+
+ /**
+ * Called when the WebView finishes loading a page.
+ */
+ fun pageLoad(block: PageLoadCallback) {
+ onPageLoadCallback = block
+ }
+}
+
+@JvmInline
+private value class Container(val value: U)
+
+/**
+ * Run a [android.webkit.WebView] Activity controlled by the provided code block.
+ * The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
+ * The [block] defines the event handlers and returns the initial URL.
+ *
+ * @param title The string displayed in the action bar.
+ * @param block The control block.
+ */
+@OptIn(PluginHostApi::class)
+suspend fun GetScope.runWebView(
+ title: String,
+ block: suspend WebViewScope.() -> InitialUrl
+) = supervisorScope {
+ var result by Delegates.notNull>()
+
+ val scope = WebViewScope(this@supervisorScope, this@runWebView) { result = Container(it) }
+ scope.initialUrl = scope.block()
+
+ // Start the webview activity and wait until it finishes.
+ requestStartActivity(Intent().apply {
+ putExtra(
+ WebViewActivity.KEY,
+ WebViewActivity.Parameters(title, scope.binder)
+ )
+ setClassName(
+ hostPackageName,
+ WebViewActivity::class.qualifiedName!!
+ )
+ })
+
+ // Return the result and cancel any leftover coroutines.
+ coroutineContext.cancelChildren()
+ result.value
+}
+
+/**
+ * Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
+ * Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
+ *
+ * @see runWebView
+ */
+fun WebViewDownloader(block: suspend WebViewScope.(packageName: String, version: String?) -> InitialUrl?) =
+ Downloader {
+ val label = context.applicationInfo.loadLabel(
+ context.packageManager
+ ).toString()
+
+ get { packageName, version ->
+ class ReturnNull : Exception()
+
+ try {
+ runWebView(label) {
+ download { url, _, userAgent ->
+ finish(
+ DownloadUrl(
+ url,
+ mapOf("User-Agent" to userAgent)
+ )
+ )
+ }
+
+ block(this@runWebView, packageName, version) ?: throw ReturnNull()
+ } to version
+ } catch (_: ReturnNull) {
+ null
+ }
+ }
+
+ download {
+ it.toDownloadResult()
+ }
+ }
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt
new file mode 100644
index 00000000..aff01337
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt
@@ -0,0 +1,161 @@
+package app.revanced.manager.plugin.downloader.webview
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.os.IBinder
+import android.os.Parcelable
+import android.view.MenuItem
+import android.webkit.CookieManager
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.ComponentActivity
+import androidx.activity.addCallback
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.lifecycle.viewModelScope
+import app.revanced.manager.plugin.downloader.PluginHostApi
+import app.revanced.manager.plugin.downloader.R
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+
+@OptIn(PluginHostApi::class)
+@PluginHostApi
+class WebViewActivity : ComponentActivity() {
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val vm by viewModels()
+ enableEdgeToEdge()
+ setContentView(R.layout.activity_webview)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ insets
+ }
+ val webView = findViewById(R.id.webview)
+ onBackPressedDispatcher.addCallback {
+ if (webView.canGoBack()) webView.goBack()
+ else cancelActivity()
+ }
+
+ val params = intent.getParcelableExtra(KEY)!!
+ actionBar?.apply {
+ title = params.title
+ setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
+ setDisplayHomeAsUpEnabled(true)
+ }
+
+ val events = IWebViewEvents.Stub.asInterface(params.events)!!
+ vm.setup(events)
+
+ webView.apply {
+ settings.apply {
+ cacheMode = WebSettings.LOAD_NO_CACHE
+ allowContentAccess = false
+ domStorageEnabled = true
+ javaScriptEnabled = true
+ }
+
+ webViewClient = vm.webViewClient
+ setDownloadListener { url, userAgent, _, mimetype, _ ->
+ vm.onDownload(url, mimetype, userAgent)
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ vm.commands.collect {
+ when (it) {
+ is WebViewModel.Command.Finish -> {
+ setResult(RESULT_OK)
+ finish()
+ }
+
+ is WebViewModel.Command.Load -> webView.loadUrl(it.url)
+ }
+ }
+ }
+ }
+ }
+
+ private fun cancelActivity() {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
+ cancelActivity()
+
+ true
+ } else super.onOptionsItemSelected(item)
+
+ @Parcelize
+ internal class Parameters(
+ val title: String, val events: IBinder
+ ) : Parcelable
+
+ internal companion object {
+ const val KEY = "params"
+ }
+}
+
+@OptIn(PluginHostApi::class)
+internal class WebViewModel : ViewModel() {
+ init {
+ CookieManager.getInstance().apply {
+ removeAllCookies(null)
+ setAcceptCookie(true)
+ }
+ }
+
+ private val commandChannel = Channel()
+ val commands = commandChannel.receiveAsFlow()
+
+ private var eventBinder: IWebViewEvents? = null
+ private val ctrlBinder = object : IWebView.Stub() {
+ override fun load(url: String?) {
+ viewModelScope.launch {
+ commandChannel.send(Command.Load(url!!))
+ }
+ }
+
+ override fun finish() {
+ viewModelScope.launch {
+ commandChannel.send(Command.Finish)
+ }
+ }
+ }
+
+ val webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ eventBinder!!.pageLoad(url)
+ }
+ }
+
+ fun onDownload(url: String, mimeType: String, userAgent: String) {
+ eventBinder!!.download(url, mimeType, userAgent)
+ }
+
+ fun setup(binder: IWebViewEvents) {
+ if (eventBinder != null) return
+ eventBinder = binder
+ binder.ready(ctrlBinder)
+ }
+
+ sealed interface Command {
+ data class Load(val url: String) : Command
+ data object Finish : Command
+ }
+}
\ No newline at end of file
diff --git a/downloader-plugin/src/main/res/layout/activity_webview.xml b/downloader-plugin/src/main/res/layout/activity_webview.xml
new file mode 100644
index 00000000..51f761d9
--- /dev/null
+++ b/downloader-plugin/src/main/res/layout/activity_webview.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/downloader-plugin/src/main/res/values/strings.xml b/downloader-plugin/src/main/res/values/strings.xml
new file mode 100644
index 00000000..73862c41
--- /dev/null
+++ b/downloader-plugin/src/main/res/values/strings.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/downloader-plugin/src/main/res/values/themes.xml b/downloader-plugin/src/main/res/values/themes.xml
new file mode 100644
index 00000000..495cde8e
--- /dev/null
+++ b/downloader-plugin/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/example-downloader-plugin/.gitignore b/example-downloader-plugin/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/example-downloader-plugin/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/example-downloader-plugin/build.gradle.kts b/example-downloader-plugin/build.gradle.kts
new file mode 100644
index 00000000..b480add9
--- /dev/null
+++ b/example-downloader-plugin/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ val packageName = "app.revanced.manager.plugin.downloader.example"
+
+ namespace = packageName
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = packageName
+ minSdk = 26
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+
+ if (project.hasProperty("signAsDebug")) {
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ buildFeatures.compose = true
+}
+
+dependencies {
+ implementation(libs.activity.compose)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.ui)
+ implementation(libs.compose.ui.tooling)
+ implementation(libs.compose.material3)
+
+ compileOnly(project(":downloader-plugin"))
+}
\ No newline at end of file
diff --git a/example-downloader-plugin/proguard-rules.pro b/example-downloader-plugin/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/example-downloader-plugin/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/AndroidManifest.xml b/example-downloader-plugin/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e10b2d28
--- /dev/null
+++ b/example-downloader-plugin/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt
new file mode 100644
index 00000000..dd2b26c5
--- /dev/null
+++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt
@@ -0,0 +1,69 @@
+@file:Suppress("Unused")
+
+package app.revanced.manager.plugin.downloader.example
+
+import android.app.Application
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Parcelable
+import app.revanced.manager.plugin.downloader.Downloader
+import app.revanced.manager.plugin.downloader.requestStartActivity
+import app.revanced.manager.plugin.downloader.webview.WebViewDownloader
+import kotlinx.parcelize.Parcelize
+import kotlin.io.path.*
+
+val apkMirrorDownloader = WebViewDownloader { packageName, version ->
+ with(Uri.Builder()) {
+ scheme("https")
+ authority("www.apkmirror.com")
+ mapOf(
+ "post_type" to "app_release",
+ "searchtype" to "apk",
+ "s" to (version?.let { "$packageName $it" } ?: packageName),
+ "bundles%5B%5D" to "apk_files" // bundles[]
+ ).forEach { (key, value) ->
+ appendQueryParameter(key, value)
+ }
+
+ build().toString()
+ }
+}
+
+@Parcelize
+class InstalledApp(val path: String) : Parcelable
+
+private val application by lazy {
+ // Don't do this in a real plugin.
+ val clazz = Class.forName("android.app.ActivityThread")
+ val activityThread = clazz.getMethod("currentActivityThread")(null)
+ clazz.getMethod("getApplication")(activityThread) as Application
+}
+
+val installedAppDownloader = Downloader {
+ val pm = application.packageManager
+
+ get { packageName, version ->
+ val packageInfo = try {
+ pm.getPackageInfo(packageName, 0)
+ } catch (_: PackageManager.NameNotFoundException) {
+ return@get null
+ }
+ if (version != null && packageInfo.versionName != version) return@get null
+
+ requestStartActivity()
+
+ InstalledApp(packageInfo.applicationInfo!!.sourceDir) to packageInfo.versionName
+ }
+
+
+ download { app ->
+ with(Path(app.path)) { inputStream() to fileSize() }
+ }
+
+ /*
+ download { app, outputStream ->
+ val path = Path(app.path)
+ reportSize(path.fileSize())
+ Files.copy(path, outputStream)
+ }*/
+}
diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt
new file mode 100644
index 00000000..0390f3bd
--- /dev/null
+++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt
@@ -0,0 +1,65 @@
+package app.revanced.manager.plugin.downloader.example
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.Modifier
+
+class InteractionActivity : ComponentActivity() {
+ @OptIn(ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val isDarkTheme = isSystemInDarkTheme()
+
+ val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()
+
+ MaterialTheme(colorScheme) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("User interaction example") }
+ )
+ }
+ ) { paddingValues ->
+ Column(modifier = Modifier.padding(paddingValues)) {
+ Text("This is an example interaction.")
+ Row {
+ TextButton(
+ onClick = {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ ) {
+ Text("Cancel")
+ }
+
+ TextButton(
+ onClick = {
+ setResult(RESULT_OK)
+ finish()
+ }
+ ) {
+ Text("Continue")
+ }
+ }
+ }
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml b/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml b/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/res/values/strings.xml b/example-downloader-plugin/src/main/res/values/strings.xml
new file mode 100644
index 00000000..4006549c
--- /dev/null
+++ b/example-downloader-plugin/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Example Downloader Plugin
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 47d9b401..325e1127 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,13 +1,14 @@
[versions]
ktx = "1.15.0"
material3 = "1.3.1"
-ui-tooling = "1.7.5"
+ui-tooling = "1.7.6"
viewmodel-lifecycle = "2.8.7"
splash-screen = "1.0.1"
-compose-activity = "1.9.3"
+activity = "1.9.3"
+appcompat = "1.7.0"
preferences-datastore = "1.1.1"
work-runtime = "2.10.0"
-compose-bom = "2024.10.01"
+compose-bom = "2024.12.01"
accompanist = "0.34.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
@@ -23,10 +24,11 @@ reimagined-navigation = "1.5.0"
ktor = "2.3.9"
markdown-renderer = "0.22.0"
fading-edges = "1.0.4"
-kotlin = "2.0.21"
-android-gradle-plugin = "8.7.2"
-dev-tools-gradle-plugin = "2.0.21-1.0.27"
+kotlin = "2.1.0"
+android-gradle-plugin = "8.7.3"
+dev-tools-gradle-plugin = "2.1.0-1.0.29"
about-libraries-gradle-plugin = "11.1.1"
+binary-compatibility-validator = "0.17.0"
coil = "2.6.0"
app-icon-loader-coil = "1.5.0"
skrapeit = "1.2.2"
@@ -43,9 +45,11 @@ androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx"
runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "viewmodel-lifecycle" }
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
-compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" }
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
+activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
@@ -135,9 +139,11 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
+android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
+binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f66506b8..18b2f456 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -26,3 +26,5 @@ dependencyResolutionManagement {
}
rootProject.name = "ReVanced Manager"
include(":app")
+include(":downloader-plugin")
+include(":example-downloader-plugin")