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 48aa6323..47ff71df 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": "f7e0fef1b937143a8b128e3dbab7c041",
+ "identityHash": "7142188e25ce489eb233aed8fb76e4cc",
"entities": [
{
- "tableName": "sources",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `location` TEXT NOT NULL, `version` TEXT NOT NULL, `integrations_version` TEXT NOT NULL, PRIMARY KEY(`uid`))",
+ "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`))",
"fields": [
{
"fieldPath": "uid",
@@ -21,22 +21,28 @@
"notNull": true
},
{
- "fieldPath": "location",
- "columnName": "location",
+ "fieldPath": "source",
+ "columnName": "source",
"affinity": "TEXT",
"notNull": true
},
+ {
+ "fieldPath": "autoUpdate",
+ "columnName": "auto_update",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
{
"fieldPath": "versionInfo.patches",
"columnName": "version",
"affinity": "TEXT",
- "notNull": true
+ "notNull": false
},
{
"fieldPath": "versionInfo.integrations",
"columnName": "integrations_version",
"affinity": "TEXT",
- "notNull": true
+ "notNull": false
}
],
"primaryKey": {
@@ -47,20 +53,20 @@
},
"indices": [
{
- "name": "index_sources_name",
+ "name": "index_patch_bundles_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_sources_name` ON `${TABLE_NAME}` (`name`)"
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "patch_selections",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `source` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`source`) REFERENCES `sources`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
@@ -69,8 +75,8 @@
"notNull": true
},
{
- "fieldPath": "source",
- "columnName": "source",
+ "fieldPath": "patchBundle",
+ "columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
@@ -89,23 +95,23 @@
},
"indices": [
{
- "name": "index_patch_selections_source_package_name",
+ "name": "index_patch_selections_patch_bundle_package_name",
"unique": true,
"columnNames": [
- "source",
+ "patch_bundle",
"package_name"
],
"orders": [],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_source_package_name` ON `${TABLE_NAME}` (`source`, `package_name`)"
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
- "table": "sources",
+ "table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
- "source"
+ "patch_bundle"
],
"referencedColumns": [
"uid"
@@ -189,7 +195,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, 'f7e0fef1b937143a8b128e3dbab7c041')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7142188e25ce489eb233aed8fb76e4cc')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 687c71d3..edb11886 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt
index 439be037..41275864 100644
--- a/app/src/main/java/app/revanced/manager/MainActivity.kt
+++ b/app/src/main/java/app/revanced/manager/MainActivity.kt
@@ -7,7 +7,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.screen.AppSelectorScreen
@@ -28,22 +28,19 @@ import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
-import org.koin.android.ext.android.get
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
- private val prefs: PreferencesManager = get()
-
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- installSplashScreen()
+ val vm: MainViewModel = getActivityViewModel()
- getActivityViewModel()
+ installSplashScreen()
val scale = this.resources.displayMetrics.density
val pixels = (36 * scale).roundToInt()
@@ -57,8 +54,8 @@ class MainActivity : ComponentActivity() {
)
setContent {
- val theme by prefs.theme.getAsState()
- val dynamicColor by prefs.dynamicColor.getAsState()
+ val theme by vm.prefs.theme.getAsState()
+ val dynamicColor by vm.prefs.dynamicColor.getAsState()
ReVancedManagerTheme(
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
@@ -69,6 +66,11 @@ class MainActivity : ComponentActivity() {
NavBackHandler(navController)
+ val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
+ if (showAutoUpdatesDialog) {
+ AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
+ }
+
AnimatedNavHost(
controller = navController
) { destination ->
diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
index 5484918a..07856a59 100644
--- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt
+++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
@@ -3,6 +3,8 @@ 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.PatchBundleRepository
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@@ -14,6 +16,7 @@ import org.koin.core.context.startKoin
class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
+ private val patchBundleRepository: PatchBundleRepository by inject()
override fun onCreate() {
super.onCreate()
@@ -36,5 +39,11 @@ class ManagerApplication : Application() {
scope.launch {
prefs.preload()
}
+ scope.launch(Dispatchers.Default) {
+ with(patchBundleRepository) {
+ reload()
+ updateCheck()
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt
new file mode 100644
index 00000000..f5d3dd89
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt
@@ -0,0 +1,19 @@
+package app.revanced.manager.data.platform
+
+import android.app.Application
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import androidx.core.content.getSystemService
+
+class NetworkInfo(app: Application) {
+ private val connectivityManager = app.getSystemService()!!
+
+ private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
+ fun isConnected() = connectivityManager.activeNetwork != null
+ fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true
+
+ /**
+ * Returns true if it is safe to download large files.
+ */
+ fun isSafe() = isConnected() && isUnmetered()
+}
\ No newline at end of file
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 432f40e9..00b770bf 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
@@ -8,14 +8,14 @@ import app.revanced.manager.data.room.apps.DownloadedApp
import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
-import app.revanced.manager.data.room.sources.SourceDao
-import app.revanced.manager.data.room.sources.SourceEntity
+import app.revanced.manager.data.room.bundles.PatchBundleDao
+import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlin.random.Random
-@Database(entities = [SourceEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1)
+@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
- abstract fun sourceDao(): SourceDao
+ abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao
abstract fun appDao(): AppDao
diff --git a/app/src/main/java/app/revanced/manager/data/room/Converters.kt b/app/src/main/java/app/revanced/manager/data/room/Converters.kt
index ceba65df..f8aa073d 100644
--- a/app/src/main/java/app/revanced/manager/data/room/Converters.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/Converters.kt
@@ -1,19 +1,16 @@
package app.revanced.manager.data.room
import androidx.room.TypeConverter
-import app.revanced.manager.data.room.sources.SourceLocation
+import app.revanced.manager.data.room.bundles.Source
import io.ktor.http.*
import java.io.File
class Converters {
@TypeConverter
- fun locationFromString(value: String) = when(value) {
- SourceLocation.Local.SENTINEL -> SourceLocation.Local
- else -> SourceLocation.Remote(Url(value))
- }
+ fun sourceFromString(value: String) = Source.from(value)
@TypeConverter
- fun locationToString(location: SourceLocation) = location.toString()
+ fun sourceToString(value: Source) = value.toString()
@TypeConverter
fun fileFromString(value: String) = File(value)
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
new file mode 100644
index 00000000..28f54e5c
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
@@ -0,0 +1,34 @@
+package app.revanced.manager.data.room.bundles
+
+import androidx.room.*
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface PatchBundleDao {
+ @Query("SELECT * FROM patch_bundles")
+ suspend fun all(): List
+
+ @Query("SELECT version, integrations_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 auto_update = :value WHERE uid = :uid")
+ suspend fun setAutoUpdate(uid: Int, value: Boolean)
+
+ @Query("DELETE FROM patch_bundles WHERE uid != 0")
+ suspend fun purgeCustomBundles()
+
+ @Transaction
+ suspend fun reset() {
+ purgeCustomBundles()
+ updateVersion(0, null, null) // Reset the main source
+ }
+
+ @Query("DELETE FROM patch_bundles WHERE uid = :uid")
+ suspend fun remove(uid: Int)
+
+ @Insert
+ suspend fun add(source: PatchBundleEntity)
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..e9869de9
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
@@ -0,0 +1,49 @@
+package app.revanced.manager.data.room.bundles
+
+import androidx.room.*
+import io.ktor.http.*
+
+sealed class Source {
+ object Local : Source() {
+ const val SENTINEL = "local"
+
+ override fun toString() = SENTINEL
+ }
+
+ object API : Source() {
+ const val SENTINEL = "api"
+
+ override fun toString() = SENTINEL
+ }
+
+ data class Remote(val url: Url) : Source() {
+ override fun toString() = url.toString()
+ }
+
+ companion object {
+ fun from(value: String) = when(value) {
+ Local.SENTINEL -> Local
+ API.SENTINEL -> API
+ else -> Remote(Url(value))
+ }
+ }
+}
+
+data class VersionInfo(
+ @ColumnInfo(name = "version") val patches: String? = null,
+ @ColumnInfo(name = "integrations_version") val integrations: String? = null,
+)
+
+@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)])
+data class PatchBundleEntity(
+ @PrimaryKey val uid: Int,
+ @ColumnInfo(name = "name") val name: String,
+ @Embedded val versionInfo: VersionInfo,
+ @ColumnInfo(name = "source") val source: Source,
+ @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
+)
+
+data class BundleProperties(
+ @Embedded val versionInfo: VersionInfo,
+ @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/selection/PatchSelection.kt b/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt
index 16ed490e..02f5ab94 100644
--- a/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt
@@ -5,20 +5,20 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
-import app.revanced.manager.data.room.sources.SourceEntity
+import app.revanced.manager.data.room.bundles.PatchBundleEntity
@Entity(
tableName = "patch_selections",
foreignKeys = [ForeignKey(
- SourceEntity::class,
+ PatchBundleEntity::class,
parentColumns = ["uid"],
- childColumns = ["source"],
+ childColumns = ["patch_bundle"],
onDelete = ForeignKey.CASCADE
)],
- indices = [Index(value = ["source", "package_name"], unique = true)]
+ indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
)
data class PatchSelection(
@PrimaryKey val uid: Int,
- @ColumnInfo(name = "source") val source: Int,
+ @ColumnInfo(name = "patch_bundle") val patchBundle: Int,
@ColumnInfo(name = "package_name") val packageName: String
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
index c85d1e81..2e288d97 100644
--- a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt
@@ -9,9 +9,9 @@ import androidx.room.Transaction
@Dao
abstract class SelectionDao {
@Transaction
- @MapInfo(keyColumn = "source", valueColumn = "patch_name")
+ @MapInfo(keyColumn = "patch_bundle", valueColumn = "patch_name")
@Query(
- "SELECT source, patch_name FROM patch_selections" +
+ "SELECT patch_bundle, patch_name FROM patch_selections" +
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
" WHERE package_name = :packageName"
)
@@ -22,18 +22,18 @@ abstract class SelectionDao {
@Query(
"SELECT package_name, patch_name FROM patch_selections" +
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
- " WHERE source = :sourceUid"
+ " WHERE patch_bundle = :bundleUid"
)
- abstract suspend fun exportSelection(sourceUid: Int): Map>
+ abstract suspend fun exportSelection(bundleUid: Int): Map>
- @Query("SELECT uid FROM patch_selections WHERE source = :sourceUid AND package_name = :packageName")
- abstract suspend fun getSelectionId(sourceUid: Int, packageName: String): Int?
+ @Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName")
+ abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int?
@Insert
abstract suspend fun createSelection(selection: PatchSelection)
- @Query("DELETE FROM patch_selections WHERE source = :uid")
- abstract suspend fun clearForSource(uid: Int)
+ @Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
+ abstract suspend fun clearForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections")
abstract suspend fun reset()
diff --git a/app/src/main/java/app/revanced/manager/data/room/sources/SourceDao.kt b/app/src/main/java/app/revanced/manager/data/room/sources/SourceDao.kt
deleted file mode 100644
index b318a9dd..00000000
--- a/app/src/main/java/app/revanced/manager/data/room/sources/SourceDao.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package app.revanced.manager.data.room.sources
-
-import androidx.room.*
-
-@Dao
-interface SourceDao {
- @Query("SELECT * FROM $sourcesTableName")
- suspend fun all(): List
-
- @Query("SELECT version, integrations_version FROM $sourcesTableName WHERE uid = :uid")
- suspend fun getVersionById(uid: Int): VersionInfo
-
- @Query("UPDATE $sourcesTableName SET version=:patches, integrations_version=:integrations WHERE uid=:uid")
- suspend fun updateVersion(uid: Int, patches: String, integrations: String)
-
- @Query("DELETE FROM $sourcesTableName")
- suspend fun purge()
-
- @Query("DELETE FROM $sourcesTableName WHERE uid=:uid")
- suspend fun remove(uid: Int)
-
- @Insert
- suspend fun add(source: SourceEntity)
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/sources/SourceEntity.kt b/app/src/main/java/app/revanced/manager/data/room/sources/SourceEntity.kt
deleted file mode 100644
index 7000d447..00000000
--- a/app/src/main/java/app/revanced/manager/data/room/sources/SourceEntity.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package app.revanced.manager.data.room.sources
-
-import androidx.room.*
-import io.ktor.http.*
-
-const val sourcesTableName = "sources"
-
-sealed class SourceLocation {
- object Local : SourceLocation() {
- const val SENTINEL = "local"
-
- override fun toString() = SENTINEL
- }
-
- data class Remote(val url: Url) : SourceLocation() {
- override fun toString() = url.toString()
- }
-}
-
-data class VersionInfo(
- @ColumnInfo(name = "version") val patches: String,
- @ColumnInfo(name = "integrations_version") val integrations: String,
-)
-
-@Entity(tableName = sourcesTableName, indices = [Index(value = ["name"], unique = true)])
-data class SourceEntity(
- @PrimaryKey val uid: Int,
- @ColumnInfo(name = "name") val name: String,
- @Embedded val versionInfo: VersionInfo,
- @ColumnInfo(name = "location") val location: SourceLocation,
-)
\ 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 5646751d..6f2ab435 100644
--- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
@@ -1,6 +1,7 @@
package app.revanced.manager.di
import app.revanced.manager.data.platform.FileSystem
+import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.*
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ManagerAPI
@@ -12,9 +13,10 @@ val repositoryModule = module {
singleOf(::GithubRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem)
- singleOf(::SourcePersistenceRepository)
+ singleOf(::NetworkInfo)
+ singleOf(::PatchBundlePersistenceRepository)
singleOf(::PatchSelectionRepository)
- singleOf(::SourceRepository)
+ singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
}
\ No newline at end of file
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 d33c3541..b0545375 100644
--- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
@@ -6,11 +6,13 @@ import org.koin.dsl.module
val viewModelModule = module {
viewModelOf(::MainViewModel)
+ viewModelOf(::DashboardViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel)
+ viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::VersionSelectorViewModel)
- viewModelOf(::SourcesViewModel)
+ viewModelOf(::BundlesViewModel)
viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateProgressViewModel)
viewModelOf(::ManagerUpdateChangelogViewModel)
diff --git a/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
similarity index 53%
rename from app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt
rename to app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
index cb410a48..43f86e72 100644
--- a/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
@@ -1,4 +1,4 @@
-package app.revanced.manager.domain.sources
+package app.revanced.manager.domain.bundles
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -7,17 +7,17 @@ import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
-class LocalSource(name: String, id: Int, directory: File) : Source(name, id, directory) {
+class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) {
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
withContext(Dispatchers.IO) {
patches?.let {
- Files.copy(it, patchesJar.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
integrations?.let {
- Files.copy(it, this@LocalSource.integrations.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
- _bundle.emit(loadBundle { throw it })
+ reload()
}
}
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
new file mode 100644
index 00000000..f8b8e74c
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
@@ -0,0 +1,55 @@
+package app.revanced.manager.domain.bundles
+
+import androidx.compose.runtime.Stable
+import app.revanced.manager.patcher.patch.PatchBundle
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
+import java.io.File
+
+/**
+ * A [PatchBundle] source.
+ */
+@Stable
+sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) {
+ protected val patchesFile = directory.resolve("patches.jar")
+ protected val integrationsFile = directory.resolve("integrations.apk")
+
+ private val _state = MutableStateFlow(load())
+ val state = _state.asStateFlow()
+
+ /**
+ * Returns true if the bundle has been downloaded to local storage.
+ */
+ fun hasInstalled() = patchesFile.exists()
+
+ private fun load(): State {
+ if (!hasInstalled()) return State.Missing
+
+ return try {
+ State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
+ } catch (t: Throwable) {
+ State.Failed(t)
+ }
+ }
+
+ fun reload() {
+ _state.value = load()
+ }
+
+ sealed interface State {
+ fun patchBundleOrNull(): PatchBundle? = null
+
+ object Missing : State
+ data class Failed(val throwable: Throwable) : State
+ data class Loaded(val bundle: PatchBundle) : State {
+ override fun patchBundleOrNull() = bundle
+ }
+ }
+
+ companion object {
+ val PatchBundleSource.isDefault get() = uid == 0
+ val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle
+ fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null)
+ }
+}
\ 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
new file mode 100644
index 00000000..7ef5e99a
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
@@ -0,0 +1,111 @@
+package app.revanced.manager.domain.bundles
+
+import androidx.compose.runtime.Stable
+import app.revanced.manager.data.room.bundles.VersionInfo
+import app.revanced.manager.domain.bundles.APIPatchBundle.Companion.toBundleAsset
+import app.revanced.manager.domain.repository.Assets
+import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
+import app.revanced.manager.domain.repository.ReVancedRepository
+import app.revanced.manager.network.dto.Asset
+import app.revanced.manager.network.dto.BundleAsset
+import app.revanced.manager.network.dto.BundleInfo
+import app.revanced.manager.network.service.HttpService
+import app.revanced.manager.network.utils.getOrThrow
+import app.revanced.manager.util.ghIntegrations
+import app.revanced.manager.util.ghPatches
+import io.ktor.client.request.url
+import io.ktor.http.Url
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.io.File
+
+@Stable
+sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
+ PatchBundleSource(name, id, directory), KoinComponent {
+ private val configRepository: PatchBundlePersistenceRepository by inject()
+ protected val http: HttpService by inject()
+
+ protected abstract suspend fun getLatestInfo(): BundleInfo
+
+ private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
+ val (patches, integrations) = info
+ coroutineScope {
+ mapOf(
+ patches.url to patchesFile,
+ integrations.url to integrationsFile
+ ).forEach { (asset, file) ->
+ launch {
+ http.download(file) {
+ url(asset)
+ }
+ }
+ }
+ }
+
+ saveVersion(patches.version, integrations.version)
+ reload()
+ }
+
+ suspend fun downloadLatest() {
+ download(getLatestInfo())
+ }
+
+ suspend fun update(): Boolean = withContext(Dispatchers.IO) {
+ val info = getLatestInfo()
+ if (hasInstalled() && VersionInfo(info.patches.version, info.integrations.version) == currentVersion()) {
+ return@withContext false
+ }
+
+ download(info)
+ true
+ }
+
+ private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
+ private suspend fun saveVersion(patches: String, integrations: String) =
+ configRepository.updateVersion(uid, patches, integrations)
+
+ suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
+ arrayOf(patchesFile, integrationsFile).forEach(File::delete)
+ reload()
+ }
+
+ fun propsFlow() = configRepository.getProps(uid)
+
+ suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
+
+ companion object {
+ const val updateFailMsg = "Failed to update patch bundle(s)"
+ }
+}
+
+class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
+ RemotePatchBundle(name, id, directory, endpoint) {
+ override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
+ http.request {
+ url(endpoint)
+ }.getOrThrow()
+ }
+}
+
+class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
+ RemotePatchBundle(name, id, directory, endpoint) {
+ private val api: ReVancedRepository by inject()
+
+ override suspend fun getLatestInfo() = api.getAssets().toBundleInfo()
+
+ private companion object {
+ fun Assets.toBundleInfo(): BundleInfo {
+ val patches = find(ghPatches, ".jar")
+ val integrations = find(ghIntegrations, ".apk")
+
+ return BundleInfo(patches.toBundleAsset(), integrations.toBundleAsset())
+ }
+
+ fun Asset.toBundleAsset() = BundleAsset(version, downloadUrl)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt
index 61e3274f..697a72eb 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
@@ -10,10 +10,15 @@ class PreferencesManager(
val dynamicColor = booleanPreference("dynamic_color", true)
val theme = enumPreference("theme", Theme.SYSTEM)
+ val api = stringPreference("api_url", "https://releases.revanced.app")
+
val allowExperimental = booleanPreference("allow_experimental", false)
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
val preferSplits = booleanPreference("prefer_splits", false)
+
+ val showAutoUpdatesDialog = booleanPreference("show_auto_updates_dialog", true)
+ val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
}
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
new file mode 100644
index 00000000..8ea5733d
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
@@ -0,0 +1,56 @@
+package app.revanced.manager.domain.repository
+
+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 io.ktor.http.*
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+class PatchBundlePersistenceRepository(db: AppDatabase) {
+ private val dao = db.patchBundleDao()
+
+ suspend fun loadConfiguration(): List {
+ val all = dao.all()
+ if (all.isEmpty()) {
+ dao.add(defaultSource)
+ return listOf(defaultSource)
+ }
+
+ return all
+ }
+
+ suspend fun reset() = dao.reset()
+
+
+ suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
+ PatchBundleEntity(
+ uid = generateUid(),
+ name = name,
+ versionInfo = VersionInfo(),
+ source = source,
+ autoUpdate = autoUpdate
+ ).also {
+ dao.add(it)
+ }
+
+ suspend fun delete(uid: Int) = dao.remove(uid)
+
+ suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
+ dao.updateVersion(uid, patches, integrations)
+
+ suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
+
+ fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
+
+ private companion object {
+ val defaultSource = PatchBundleEntity(
+ uid = 0,
+ name = "Main",
+ versionInfo = VersionInfo(),
+ source = Source.API,
+ autoUpdate = false
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..ffdf7d13
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
@@ -0,0 +1,143 @@
+package app.revanced.manager.domain.repository
+
+import android.app.Application
+import android.content.Context
+import android.util.Log
+import app.revanced.manager.data.platform.NetworkInfo
+import app.revanced.manager.data.room.bundles.PatchBundleEntity
+import app.revanced.manager.domain.bundles.APIPatchBundle
+import app.revanced.manager.domain.bundles.JsonPatchBundle
+import app.revanced.manager.data.room.bundles.Source as SourceInfo
+import app.revanced.manager.domain.bundles.LocalPatchBundle
+import app.revanced.manager.domain.bundles.RemotePatchBundle
+import app.revanced.manager.domain.bundles.PatchBundleSource
+import app.revanced.manager.util.flatMapLatestAndCombine
+import app.revanced.manager.util.tag
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.withContext
+import java.io.InputStream
+
+class PatchBundleRepository(
+ app: Application,
+ private val persistenceRepo: PatchBundlePersistenceRepository,
+ private val networkInfo: NetworkInfo,
+) {
+ private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
+
+ private val _sources: MutableStateFlow