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> = + MutableStateFlow(emptyMap()) + val sources = _sources.map { it.values.toList() } + + val bundles = sources.flatMapLatestAndCombine( + combiner = { + it.mapNotNull { (uid, state) -> + val bundle = state.patchBundleOrNull() ?: return@mapNotNull null + uid to bundle + }.toMap() + } + ) { + it.state.map { state -> it.uid to state } + } + + /** + * Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed. + */ + private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() } + + private fun PatchBundleEntity.load(): PatchBundleSource { + val dir = directoryOf(uid) + + return when (source) { + is SourceInfo.Local -> LocalPatchBundle(name, uid, dir) + is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL) + is SourceInfo.Remote -> JsonPatchBundle( + name, + uid, + dir, + source.url.toString() + ) + } + } + + suspend fun reload() = withContext(Dispatchers.Default) { + val entities = persistenceRepo.loadConfiguration().onEach { + Log.d(tag, "Bundle: $it") + } + + _sources.value = entities.associate { + it.uid to it.load() + } + } + + suspend fun reset() = withContext(Dispatchers.Default) { + persistenceRepo.reset() + _sources.value = emptyMap() + bundlesDir.apply { + deleteRecursively() + mkdirs() + } + + reload() + } + + suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) { + persistenceRepo.delete(bundle.uid) + directoryOf(bundle.uid).deleteRecursively() + + _sources.update { + it.filterKeys { key -> + key != bundle.uid + } + } + } + + private fun addBundle(patchBundle: PatchBundleSource) = + _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } + + suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) { + val id = persistenceRepo.create(name, SourceInfo.Local).uid + val bundle = LocalPatchBundle(name, id, directoryOf(id)) + + bundle.replace(patches, integrations) + addBundle(bundle) + } + + suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) { + val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate) + addBundle(entity.load()) + } + + private suspend inline fun getBundlesByType() = + sources.first().filterIsInstance() + + suspend fun reloadApiBundles() { + getBundlesByType().forEach { + it.deleteLocalFiles() + } + + reload() + } + + suspend fun redownloadRemoteBundles() = getBundlesByType().forEach { it.downloadLatest() } + + suspend fun updateCheck() = supervisorScope { + if (!networkInfo.isSafe()) { + Log.d(tag, "Skipping update check because the network is down or metered.") + return@supervisorScope + } + + getBundlesByType().forEach { + launch { + if (!it.propsFlow().first().autoUpdate) return@launch + Log.d(tag, "Updating patch bundle: ${it.name}") + it.update() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt index 0aca8458..cade4291 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt @@ -3,15 +3,14 @@ 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.selection.PatchSelection -import app.revanced.manager.domain.sources.Source class PatchSelectionRepository(db: AppDatabase) { private val dao = db.selectionDao() - private suspend fun getOrCreateSelection(sourceUid: Int, packageName: String) = - dao.getSelectionId(sourceUid, packageName) ?: PatchSelection( + private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) = + dao.getSelectionId(bundleUid, packageName) ?: PatchSelection( uid = generateUid(), - source = sourceUid, + patchBundle = bundleUid, packageName = packageName ).also { dao.createSelection(it) }.uid @@ -28,12 +27,12 @@ class PatchSelectionRepository(db: AppDatabase) { suspend fun reset() = dao.reset() - suspend fun export(source: Source): SerializedSelection = dao.exportSelection(source.uid) + suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid) - suspend fun import(source: Source, selection: SerializedSelection) { - dao.clearForSource(source.uid) + suspend fun import(bundleUid: Int, selection: SerializedSelection) { + dao.clearForPatchBundle(bundleUid) dao.updateSelections(selection.entries.associate { (packageName, patches) -> - getOrCreateSelection(source.uid, packageName) to patches.toSet() + getOrCreateSelection(bundleUid, packageName) to patches.toSet() }) } } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/ReVancedRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/ReVancedRepository.kt index 38202808..5d35fddd 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/ReVancedRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/ReVancedRepository.kt @@ -1,13 +1,25 @@ package app.revanced.manager.domain.repository +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.network.api.MissingAssetException +import app.revanced.manager.network.dto.Asset +import app.revanced.manager.network.dto.ReVancedReleases import app.revanced.manager.network.service.ReVancedService +import app.revanced.manager.network.utils.getOrThrow class ReVancedRepository( - private val service: ReVancedService + private val service: ReVancedService, + private val prefs: PreferencesManager ) { - suspend fun getAssets() = service.getAssets() + private suspend fun apiUrl() = prefs.api.get() - suspend fun getContributors() = service.getContributors() + suspend fun getContributors() = service.getContributors(apiUrl()) - suspend fun findAsset(repo: String, file: String) = service.findAsset(repo, file) + suspend fun getAssets() = Assets(service.getAssets(apiUrl()).getOrThrow()) +} + +class Assets(private val releases: ReVancedReleases): List by releases.tools { + fun find(repo: String, file: String) = find { asset -> + asset.name.contains(file) && asset.repository.contains(repo) + } ?: throw MissingAssetException() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/SourcePersistenceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/SourcePersistenceRepository.kt deleted file mode 100644 index fd380e4a..00000000 --- a/app/src/main/java/app/revanced/manager/domain/repository/SourcePersistenceRepository.kt +++ /dev/null @@ -1,55 +0,0 @@ -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.sources.SourceEntity -import app.revanced.manager.data.room.sources.SourceLocation -import app.revanced.manager.data.room.sources.VersionInfo -import app.revanced.manager.util.apiURL -import io.ktor.http.* - -class SourcePersistenceRepository(db: AppDatabase) { - private val dao = db.sourceDao() - - private companion object { - val defaultSource = SourceEntity( - uid = generateUid(), - name = "Official", - versionInfo = VersionInfo("", ""), - location = SourceLocation.Remote(Url(apiURL)) - ) - } - - suspend fun loadConfiguration(): List { - val all = dao.all() - if (all.isEmpty()) { - dao.add(defaultSource) - return listOf(defaultSource) - } - - return all - } - - suspend fun clear() = dao.purge() - - suspend fun create(name: String, location: SourceLocation): Int { - val uid = generateUid() - dao.add( - SourceEntity( - uid = uid, - name = name, - versionInfo = VersionInfo("", ""), - location = location, - ) - ) - - return uid - } - - suspend fun delete(uid: Int) = dao.remove(uid) - - suspend fun updateVersion(uid: Int, patches: String, integrations: String) = - dao.updateVersion(uid, patches, integrations) - - suspend fun getVersion(id: Int) = dao.getVersionById(id).let { it.patches to it.integrations } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/SourceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/SourceRepository.kt deleted file mode 100644 index 82c4d743..00000000 --- a/app/src/main/java/app/revanced/manager/domain/repository/SourceRepository.kt +++ /dev/null @@ -1,101 +0,0 @@ -package app.revanced.manager.domain.repository - -import android.app.Application -import android.content.Context -import android.util.Log -import app.revanced.manager.data.room.sources.SourceEntity -import app.revanced.manager.data.room.sources.SourceLocation -import app.revanced.manager.domain.sources.LocalSource -import app.revanced.manager.domain.sources.RemoteSource -import app.revanced.manager.domain.sources.Source -import app.revanced.manager.util.flatMapLatestAndCombine -import app.revanced.manager.util.tag -import io.ktor.http.* -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.withContext -import java.io.File -import java.io.InputStream - -class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) { - private val sourcesDir = app.getDir("sources", Context.MODE_PRIVATE) - - private val _sources: MutableStateFlow> = MutableStateFlow(emptyMap()) - val sources = _sources.map { it.values.toList() } - - val bundles = sources.flatMapLatestAndCombine( - combiner = { it.toMap() } - ) { - it.bundle.map { bundle -> it.uid to bundle } - } - - /** - * Get the directory of the [Source] with the specified [uid], creating it if needed. - */ - private fun directoryOf(uid: Int) = sourcesDir.resolve(uid.toString()).also { it.mkdirs() } - - private fun SourceEntity.load(dir: File) = when (location) { - is SourceLocation.Local -> LocalSource(name, uid, dir) - is SourceLocation.Remote -> RemoteSource(name, uid, dir) - } - - suspend fun loadSources() = withContext(Dispatchers.Default) { - val sourcesConfig = persistenceRepo.loadConfiguration().onEach { - Log.d(tag, "Source: $it") - } - - val sources = sourcesConfig.associate { - val dir = directoryOf(it.uid) - val source = it.load(dir) - - it.uid to source - } - - _sources.emit(sources) - } - - suspend fun resetConfig() = withContext(Dispatchers.Default) { - persistenceRepo.clear() - _sources.emit(emptyMap()) - sourcesDir.apply { - deleteRecursively() - mkdirs() - } - - loadSources() - } - - suspend fun remove(source: Source) = withContext(Dispatchers.Default) { - persistenceRepo.delete(source.uid) - directoryOf(source.uid).deleteRecursively() - - _sources.update { - it.filterValues { value -> - value.uid != source.uid - } - } - } - - private fun addSource(source: Source) = - _sources.update { it.toMutableMap().apply { put(source.uid, source) } } - - suspend fun createLocalSource(name: String, patches: InputStream, integrations: InputStream?) { - val id = persistenceRepo.create(name, SourceLocation.Local) - val source = LocalSource(name, id, directoryOf(id)) - - addSource(source) - - source.replace(patches, integrations) - } - - suspend fun createRemoteSource(name: String, apiUrl: Url) { - val id = persistenceRepo.create(name, SourceLocation.Remote(apiUrl)) - addSource(RemoteSource(name, id, directoryOf(id))) - } - - suspend fun redownloadRemoteSources() = - sources.first().filterIsInstance().forEach { it.downloadLatest() } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt deleted file mode 100644 index e7c6bcc9..00000000 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.revanced.manager.domain.sources - -import androidx.compose.runtime.Stable -import app.revanced.manager.network.api.ManagerAPI -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.koin.core.component.get -import java.io.File - -@Stable -class RemoteSource(name: String, id: Int, directory: File) : Source(name, id, directory) { - private val api: ManagerAPI = get() - suspend fun downloadLatest() = withContext(Dispatchers.IO) { - api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) -> - saveVersion(patchesVer, integrationsVer) - _bundle.emit(loadBundle { err -> throw err }) - } - - return@withContext - } - - suspend fun update() = withContext(Dispatchers.IO) { - val currentVersion = getVersion() - if (!hasInstalled() || currentVersion != api.getLatestBundleVersion()) { - downloadLatest() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/sources/Source.kt b/app/src/main/java/app/revanced/manager/domain/sources/Source.kt deleted file mode 100644 index 34869562..00000000 --- a/app/src/main/java/app/revanced/manager/domain/sources/Source.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.revanced.manager.domain.sources - -import android.util.Log -import androidx.compose.runtime.Stable -import app.revanced.manager.patcher.patch.PatchBundle -import app.revanced.manager.domain.repository.SourcePersistenceRepository -import app.revanced.manager.util.tag -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import java.io.File - -/** - * A [PatchBundle] source. - */ -@Stable -sealed class Source(val name: String, val uid: Int, directory: File) : KoinComponent { - private val configRepository: SourcePersistenceRepository by inject() - protected companion object { - /** - * A placeholder [PatchBundle]. - */ - val emptyPatchBundle = PatchBundle(emptyList(), null) - fun logError(err: Throwable) { - Log.e(tag, "Failed to load bundle", err) - } - } - - protected val patchesJar = directory.resolve("patches.jar") - protected val integrations = directory.resolve("integrations.apk") - - /** - * Returns true if the bundle has been downloaded to local storage. - */ - fun hasInstalled() = patchesJar.exists() - - protected suspend fun getVersion() = configRepository.getVersion(uid) - protected suspend fun saveVersion(patches: String, integrations: String) = - configRepository.updateVersion(uid, patches, integrations) - - // TODO: Communicate failure states better. - protected fun loadBundle(onFail: (Throwable) -> Unit = ::logError) = if (!hasInstalled()) emptyPatchBundle - else try { - PatchBundle(patchesJar, integrations.takeIf { it.exists() }) - } catch (err: Throwable) { - onFail(err) - emptyPatchBundle - } - - protected val _bundle = MutableStateFlow(loadBundle()) - val bundle = _bundle.asStateFlow() -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/api/ManagerAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ManagerAPI.kt index 1c64051f..49c6f360 100644 --- a/app/src/main/java/app/revanced/manager/network/api/ManagerAPI.kt +++ b/app/src/main/java/app/revanced/manager/network/api/ManagerAPI.kt @@ -1,59 +1,43 @@ package app.revanced.manager.network.api -import android.app.Application -import android.os.Environment -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import app.revanced.manager.domain.repository.Assets import app.revanced.manager.domain.repository.ReVancedRepository +import app.revanced.manager.network.dto.Asset +import app.revanced.manager.network.service.HttpService import app.revanced.manager.util.* -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.util.cio.* -import io.ktor.utils.io.* +import io.ktor.client.plugins.onDownload +import io.ktor.client.request.url import java.io.File +// TODO: merge ReVancedRepository into this class class ManagerAPI( - private val client: HttpClient, + private val http: HttpService, private val revancedRepository: ReVancedRepository ) { var downloadProgress: Float? by mutableStateOf(null) var downloadedSize: Long? by mutableStateOf(null) var totalSize: Long? by mutableStateOf(null) - private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) { - client.get(downloadUrl) { + private suspend fun downloadAsset(asset: Asset, saveLocation: File) { + http.download(saveLocation) { + url(asset.downloadUrl) onDownload { bytesSentTotal, contentLength -> downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat()) downloadedSize = bytesSentTotal totalSize = contentLength } - }.bodyAsChannel().copyAndClose(saveLocation.writeChannel()) + } downloadProgress = null } - private suspend fun patchesAsset() = revancedRepository.findAsset(ghPatches, ".jar") - private suspend fun integrationsAsset() = revancedRepository.findAsset(ghIntegrations, ".apk") - - suspend fun getLatestBundleVersion() = patchesAsset().version to integrationsAsset().version - - suspend fun downloadBundle(patchBundle: File, integrations: File): Pair { - val patchBundleAsset = patchesAsset() - val integrationsAsset = integrationsAsset() - - downloadAsset(patchBundleAsset.downloadUrl, patchBundle) - downloadAsset(integrationsAsset.downloadUrl, integrations) - - return patchBundleAsset.version to integrationsAsset.version - } - suspend fun downloadManager(location: File) { - val managerAsset = revancedRepository.findAsset(ghManager, ".apk") - downloadAsset(managerAsset.downloadUrl, location) + val managerAsset = revancedRepository.getAssets().find(ghManager, ".apk") + downloadAsset(managerAsset, location) } + } class MissingAssetException : Exception() \ No newline at end of file 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 new file mode 100644 index 00000000..e2b56a87 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt @@ -0,0 +1,9 @@ +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/service/ReVancedService.kt b/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt index 539c6f7b..55a8bd78 100644 --- a/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt +++ b/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt @@ -6,7 +6,6 @@ import app.revanced.manager.network.dto.ReVancedReleases import app.revanced.manager.network.dto.ReVancedRepositories import app.revanced.manager.network.utils.APIResponse import app.revanced.manager.network.utils.getOrThrow -import app.revanced.manager.util.apiURL import io.ktor.client.request.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -14,33 +13,20 @@ import kotlinx.coroutines.withContext class ReVancedService( private val client: HttpService, ) { - suspend fun getAssets(): APIResponse { + suspend fun getAssets(api: String): APIResponse { return withContext(Dispatchers.IO) { client.request { - url("$apiUrl/tools") + url("$api/tools") } } } - suspend fun getContributors(): APIResponse { + suspend fun getContributors(api: String): APIResponse { return withContext(Dispatchers.IO) { client.request { - url("$apiUrl/contributors") + url("$api/contributors") } } } - suspend fun findAsset(repo: String, file: String): Asset { - val releases = getAssets().getOrThrow() - - val asset = releases.tools.find { asset -> - (asset.name.contains(file) && asset.repository.contains(repo)) - } ?: throw MissingAssetException() - - return asset - } - - private companion object { - private const val apiUrl = apiURL - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 363ac2f2..16b99e96 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 @@ -16,7 +16,7 @@ import androidx.work.WorkerParameters import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.DownloadedAppRepository -import app.revanced.manager.domain.repository.SourceRepository +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.patcher.Session @@ -43,7 +43,7 @@ class PatcherWorker( parameters: WorkerParameters ) : Worker(context, parameters), KoinComponent { - private val sourceRepository: SourceRepository by inject() + private val patchBundleRepository: PatchBundleRepository by inject() private val workerRepository: WorkerRepository by inject() private val prefs: PreferencesManager by inject() private val downloadedAppRepository: DownloadedAppRepository by inject() @@ -124,7 +124,7 @@ class PatcherWorker( val frameworkPath = applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath - val bundles = sourceRepository.bundles.first() + val bundles = patchBundleRepository.bundles.first() val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations } val downloadProgress = MutableStateFlow?>(null) 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 new file mode 100644 index 00000000..78fcbeb2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt @@ -0,0 +1,115 @@ +package app.revanced.manager.ui.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +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.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +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 +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@Composable +fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) { + var patchesEnabled by rememberSaveable { mutableStateOf(true) } + var managerEnabled by rememberSaveable { mutableStateOf(true) } + + AlertDialog( + onDismissRequest = {}, + confirmButton = { + TextButton( + onClick = { onSubmit(managerEnabled, patchesEnabled) } + ) { + Text(stringResource(R.string.save)) + } + }, + icon = { + Icon(Icons.Outlined.Update, null) + }, + title = { + Text( + text = stringResource(R.string.auto_updates_dialog_title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(R.string.auto_updates_dialog_description), + style = MaterialTheme.typography.bodyMedium, + ) + + AutoUpdatesItem( + headline = R.string.auto_updates_dialog_manager, + icon = Icons.Outlined.Update, + checked = managerEnabled, + onCheckedChange = { managerEnabled = it } + ) + Divider() + AutoUpdatesItem( + headline = R.string.auto_updates_dialog_patches, + icon = Icons.Outlined.Source, + checked = patchesEnabled, + onCheckedChange = { patchesEnabled = it } + ) + + Text( + text = stringResource(R.string.auto_updates_dialog_note), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth() + ) + } + } + ) +} + +@Composable +private fun AutoUpdatesItem( + @StringRes headline: Int, + icon: ImageVector, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + ListItem( + leadingContent = { Icon(icon, null, tint = MaterialTheme.colorScheme.onSurface) }, + headlineContent = { + Text( + text = stringResource(headline), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + }, + trailingContent = { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange + ) + }, + modifier = Modifier.clickable { onCheckedChange(!checked) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt b/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt deleted file mode 100644 index c66457cf..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt +++ /dev/null @@ -1,96 +0,0 @@ -package app.revanced.manager.ui.component - - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -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.platform.LocalContext -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.revanced.manager.R -import app.revanced.manager.domain.sources.RemoteSource -import app.revanced.manager.domain.sources.Source -import app.revanced.manager.ui.component.bundle.BundleInformationDialog -import app.revanced.manager.ui.viewmodel.SourcesViewModel -import app.revanced.manager.util.uiSafe -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -@Composable -fun SourceItem( - source: Source, onDelete: () -> Unit, - coroutineScope: CoroutineScope, -) { - var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } - - val bundle by source.bundle.collectAsStateWithLifecycle() - val patchCount = bundle.patches.size - val padding = PaddingValues(16.dp, 0.dp) - - val androidContext = LocalContext.current - - if (viewBundleDialogPage) { - BundleInformationDialog( - onDismissRequest = { viewBundleDialogPage = false }, - onDeleteRequest = { - viewBundleDialogPage = false - onDelete() - }, - source = source, - patchCount = patchCount, - onRefreshButton = { - coroutineScope.launch { - uiSafe( - androidContext, - R.string.source_download_fail, - SourcesViewModel.failLogMsg - ) { - if (source is RemoteSource) source.update() - } - } - }, - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .height(64.dp) - .fillMaxWidth() - .clickable { - viewBundleDialogPage = true - } - ) { - Text( - text = source.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(padding) - ) - - Spacer( - modifier = Modifier.weight(1f) - ) - - Text( - text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(padding) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt new file mode 100644 index 00000000..07ae4532 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -0,0 +1,149 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowRight +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@Composable +fun BaseBundleDialog( + modifier: Modifier = Modifier, + isDefault: Boolean, + name: String, + onNameChange: (String) -> Unit = {}, + remoteUrl: String?, + onRemoteUrlChange: (String) -> Unit = {}, + patchCount: Int, + version: String?, + autoUpdate: Boolean, + onAutoUpdateChange: (Boolean) -> Unit, + onPatchesClick: () -> Unit, + onBundleTypeClick: () -> Unit = {}, + extraFields: @Composable ColumnScope.() -> Unit = {} +) = Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .then(modifier) +) { + Column( + modifier = Modifier.padding( + start = 24.dp, + top = 16.dp, + end = 24.dp, + ) + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = name, + onValueChange = onNameChange, + label = { + Text(stringResource(R.string.bundle_input_name)) + } + ) + remoteUrl?.takeUnless { isDefault }?.let { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = it, + onValueChange = onRemoteUrlChange, + label = { + Text(stringResource(R.string.bundle_input_source_url)) + } + ) + } + + extraFields() + } + + Column( + Modifier.padding( + start = 8.dp, + top = 8.dp, + end = 4.dp, + ) + ) Info@{ + if (remoteUrl != null) { + BundleListItem( + headlineText = stringResource(R.string.automatically_update), + supportingText = stringResource(R.string.automatically_update_description), + trailingContent = { + Switch( + checked = autoUpdate, + onCheckedChange = onAutoUpdateChange + ) + } + ) + } + + BundleListItem( + headlineText = stringResource(R.string.bundle_type), + supportingText = stringResource(R.string.bundle_type_description) + ) { + FilledTonalButton( + onClick = onBundleTypeClick, + content = { + if (remoteUrl == null) { + Text(stringResource(R.string.local)) + } else { + Text(stringResource(R.string.remote)) + } + } + ) + } + + if (version == null && patchCount < 1) return@Info + + Text( + text = stringResource(R.string.information), + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 12.dp + ), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + + BundleListItem( + headlineText = stringResource(R.string.patches), + supportingText = if (patchCount == 0) stringResource(R.string.no_patches) + else stringResource(R.string.patches_available, patchCount), + trailingContent = { + if (patchCount > 0) { + IconButton(onClick = onPatchesClick) { + Icon( + Icons.Outlined.ArrowRight, + stringResource(R.string.patches) + ) + } + } + } + ) + + if (version == null) return@Info + + BundleListItem( + headlineText = stringResource(R.string.version), + supportingText = version, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt deleted file mode 100644 index 0241c5e9..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt +++ /dev/null @@ -1,87 +0,0 @@ -package app.revanced.manager.ui.component.bundle - -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowRight -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import app.revanced.manager.R - -@Composable -fun BundleInfoContent( - switchChecked: Boolean, - onCheckedChange: (Boolean) -> Unit, - patchInfoText: String, - patchCount: Int, - onArrowClick: () -> Unit, - isLocal: Boolean, - tonalButtonOnClick: () -> Unit = {}, - tonalButtonContent: @Composable RowScope.() -> Unit, -) { - if(!isLocal) { - BundleInfoListItem( - headlineText = stringResource(R.string.automatically_update), - supportingText = stringResource(R.string.automatically_update_description), - trailingContent = { - Switch( - checked = switchChecked, - onCheckedChange = onCheckedChange - ) - } - ) - } - - BundleInfoListItem( - headlineText = stringResource(R.string.bundle_type), - supportingText = stringResource(R.string.bundle_type_description) - ) { - FilledTonalButton( - onClick = tonalButtonOnClick, - content = tonalButtonContent, - ) - } - - Text( - text = stringResource(R.string.information), - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 12.dp - ), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - - BundleInfoListItem( - headlineText = stringResource(R.string.patches), - supportingText = patchInfoText, - trailingContent = { - if (patchCount > 0) { - IconButton(onClick = onArrowClick) { - Icon( - Icons.Outlined.ArrowRight, - stringResource(R.string.patches) - ) - } - } - } - ) - - BundleInfoListItem( - headlineText = stringResource(R.string.patches_version), - supportingText = "1.0.0", - ) - - BundleInfoListItem( - headlineText = stringResource(R.string.integrations_version), - supportingText = "1.0.0", - ) -} \ No newline at end of file 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 ead9650d..e2498e39 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,10 +1,6 @@ package app.revanced.manager.ui.component.bundle -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.outlined.DeleteOutline @@ -13,46 +9,51 @@ 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.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R -import app.revanced.manager.domain.sources.LocalSource -import app.revanced.manager.domain.sources.RemoteSource -import app.revanced.manager.domain.sources.Source +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.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull +import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault +import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundleInformationDialog( onDismissRequest: () -> Unit, onDeleteRequest: () -> Unit, - source: Source, - remoteName: String = "", - patchCount: Int = 0, + bundle: PatchBundleSource, onRefreshButton: () -> Unit, ) { - var checked by remember { mutableStateOf(true) } + val composableScope = rememberCoroutineScope() var viewCurrentBundlePatches by remember { mutableStateOf(false) } - - val isLocal = source is LocalSource - - val patchInfoText = if (patchCount == 0) stringResource(R.string.no_patches) - else stringResource(R.string.patches_available, patchCount) + val isLocal = bundle is LocalPatchBundle + val patchCount by remember(bundle) { + bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } + }.collectAsStateWithLifecycle(0) + val props by remember(bundle) { + bundle.propsOrNullFlow() + }.collectAsStateWithLifecycle(null) if (viewCurrentBundlePatches) { BundlePatchesDialog( onDismissRequest = { viewCurrentBundlePatches = false }, - source = source, + bundle = bundle, ) } @@ -75,13 +76,15 @@ fun BundleInformationDialog( ) }, actions = { - IconButton(onClick = onDeleteRequest) { - Icon( - Icons.Outlined.DeleteOutline, - stringResource(R.string.delete) - ) + if (!bundle.isDefault) { + IconButton(onClick = onDeleteRequest) { + Icon( + Icons.Outlined.DeleteOutline, + stringResource(R.string.delete) + ) + } } - if(!isLocal) { + if (!isLocal) { IconButton(onClick = onRefreshButton) { Icon( Icons.Outlined.Refresh, @@ -93,51 +96,23 @@ fun BundleInformationDialog( ) }, ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - ) { - Column( - modifier = Modifier.padding( - start = 24.dp, - top = 16.dp, - end = 24.dp, - ) - ) { - BundleTextContent( - name = source.name, - isLocal = isLocal, - remoteUrl = remoteName, - ) - } - - Column( - Modifier.padding( - start = 8.dp, - top = 8.dp, - end = 4.dp, - ) - ) { - BundleInfoContent( - switchChecked = checked, - onCheckedChange = { checked = it }, - patchInfoText = patchInfoText, - patchCount = patchCount, - isLocal = isLocal, - onArrowClick = { - viewCurrentBundlePatches = true - }, - tonalButtonContent = { - when(source) { - is RemoteSource -> Text(stringResource(R.string.remote)) - is LocalSource -> Text(stringResource(R.string.local)) - } - }, - ) - } - } + BaseBundleDialog( + modifier = Modifier.padding(paddingValues), + isDefault = bundle.isDefault, + name = bundle.name, + remoteUrl = bundle.asRemoteOrNull?.endpoint, + patchCount = patchCount, + version = props?.versionInfo?.patches, + autoUpdate = props?.autoUpdate ?: false, + onAutoUpdateChange = { + composableScope.launch { + bundle.asRemoteOrNull?.setAutoUpdate(it) + } + }, + onPatchesClick = { + viewCurrentBundlePatches = true + }, + ) } } } 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 new file mode 100644 index 00000000..04f3f64a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -0,0 +1,108 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.Modifier +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.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow +import kotlinx.coroutines.flow.map + +@Composable +fun BundleItem( + bundle: PatchBundleSource, + onDelete: () -> Unit, + onUpdate: () -> Unit +) { + var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } + val state by bundle.state.collectAsStateWithLifecycle() + + val version by remember(bundle) { + bundle.propsOrNullFlow().map { props -> props?.versionInfo?.patches } + }.collectAsStateWithLifecycle(null) + + if (viewBundleDialogPage) { + BundleInformationDialog( + onDismissRequest = { viewBundleDialogPage = false }, + onDeleteRequest = { + viewBundleDialogPage = false + onDelete() + }, + bundle = bundle, + onRefreshButton = onUpdate, + ) + } + + ListItem( + modifier = Modifier + .height(64.dp) + .fillMaxWidth() + .clickable { + viewBundleDialogPage = true + }, + headlineContent = { + Text( + text = bundle.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + }, + supportingContent = { + state.patchBundleOrNull()?.patches?.size?.let { patchCount -> + Text( + text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + trailingContent = { + Row { + val icon = remember(state) { + when (state) { + is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error + is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing + is PatchBundleSource.State.Loaded -> null + } + } + + icon?.let { (vector, description) -> + Icon( + imageVector = vector, + contentDescription = stringResource(description), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.error + ) + } + + version?.let { txt -> + Text( + text = txt, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt similarity index 97% rename from app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt rename to app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt index 18675700..8ef9db10 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable @Composable -fun BundleInfoListItem( +fun BundleListItem( headlineText: String, supportingText: String = "", trailingContent: @Composable (() -> Unit)? = null, diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index 3dd1fb30..2ff2a555 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -28,17 +28,17 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R -import app.revanced.manager.domain.sources.Source +import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.ui.component.NotificationCard @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundlePatchesDialog( onDismissRequest: () -> Unit, - source: Source, + bundle: PatchBundleSource, ) { var informationCardVisible by remember { mutableStateOf(true) } - val bundle by source.bundle.collectAsStateWithLifecycle() + val state by bundle.state.collectAsStateWithLifecycle() Dialog( onDismissRequest = onDismissRequest, @@ -84,27 +84,29 @@ fun BundlePatchesDialog( } } - items(bundle.patches.size) { bundleIndex -> - val patch = bundle.patches[bundleIndex] - ListItem( - headlineContent = { - Text( - text = patch.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - }, - supportingContent = { - patch.description?.let { + state.patchBundleOrNull()?.let { bundle -> + items(bundle.patches.size) { bundleIndex -> + val patch = bundle.patches[bundleIndex] + ListItem( + headlineContent = { Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = patch.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) + }, + supportingContent = { + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - } - ) - Divider() + ) + Divider() + } } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt similarity index 87% rename from app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt rename to app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt index ce190c82..55da7e0f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt @@ -15,18 +15,18 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.revanced.manager.domain.sources.Source +import app.revanced.manager.domain.bundles.PatchBundleSource @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SourceSelector(sources: List, onFinish: (Source?) -> Unit) { - LaunchedEffect(sources) { - if (sources.size == 1) { - onFinish(sources[0]) +fun BundleSelector(bundles: List, onFinish: (PatchBundleSource?) -> Unit) { + LaunchedEffect(bundles) { + if (bundles.size == 1) { + onFinish(bundles[0]) } } - if (sources.size < 2) { + if (bundles.size < 2) { return } @@ -50,7 +50,7 @@ fun SourceSelector(sources: List, onFinish: (Source?) -> Unit) { color = MaterialTheme.colorScheme.onSurface ) } - sources.forEach { + bundles.forEach { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt deleted file mode 100644 index 26940e7a..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.revanced.manager.ui.component.bundle - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import app.revanced.manager.R - -@Composable -fun BundleTextContent( - name: String, - onNameChange: (String) -> Unit = {}, - isLocal: Boolean, - remoteUrl: String, - onRemoteUrlChange: (String) -> Unit = {}, -) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - value = name, - onValueChange = onNameChange, - label = { - Text(stringResource(R.string.bundle_input_name)) - } - ) - if (!isLocal) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - value = remoteUrl, - onValueChange = onRemoteUrlChange, - label = { - Text(stringResource(R.string.bundle_input_source_url)) - } - ) - } -} \ No newline at end of file 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 5dc62328..e854ae4c 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 @@ -1,24 +1,21 @@ package app.revanced.manager.ui.component.bundle import android.net.Uri +import android.webkit.URLUtil import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Topic import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -34,20 +31,17 @@ import androidx.compose.ui.window.DialogProperties import app.revanced.manager.R import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.JAR_MIMETYPE -import app.revanced.manager.util.parseUrlOrNull -import io.ktor.http.Url @OptIn(ExperimentalMaterial3Api::class) @Composable fun ImportBundleDialog( onDismissRequest: () -> Unit, - onRemoteSubmit: (String, Url) -> Unit, - onLocalSubmit: (String, Uri, Uri?) -> Unit, - patchCount: Int = 0, + onRemoteSubmit: (String, String, Boolean) -> Unit, + onLocalSubmit: (String, Uri, Uri?) -> Unit ) { var name by rememberSaveable { mutableStateOf("") } var remoteUrl by rememberSaveable { mutableStateOf("") } - var checked by remember { mutableStateOf(true) } + var autoUpdate by rememberSaveable { mutableStateOf(true) } var isLocal by rememberSaveable { mutableStateOf(false) } var patchBundle by rememberSaveable { mutableStateOf(null) } var integrations by rememberSaveable { mutableStateOf(null) } @@ -58,8 +52,10 @@ fun ImportBundleDialog( val inputsAreValid by remember { derivedStateOf { val nameSize = name.length - nameSize in 4..19 && if (isLocal) patchBundle != null else { - remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null + when { + nameSize !in 1..19 -> false + isLocal -> patchBundle != null + else -> remoteUrl.isNotEmpty() && URLUtil.isValidUrl(remoteUrl) } } } @@ -68,19 +64,11 @@ fun ImportBundleDialog( rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { patchBundle = it } } - val integrationsActivityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { integrations = it } } - val onPatchLauncherClick = { - patchActivityLauncher.launch(JAR_MIMETYPE) - } - - val onIntegrationLauncherClick = { - integrationsActivityLauncher.launch(APK_MIMETYPE) - } Dialog( onDismissRequest = onDismissRequest, properties = DialogProperties( @@ -100,115 +88,89 @@ fun ImportBundleDialog( ) }, actions = { - Text( - text = stringResource(R.string.import_), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .clickable { - if (inputsAreValid) { - if (isLocal) { - onLocalSubmit(name, patchBundle!!, integrations) - } else { - onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!) - } - } + TextButton( + enabled = inputsAreValid, + onClick = { + if (isLocal) { + onLocalSubmit(name, patchBundle!!, integrations) + } else { + onRemoteSubmit( + name, + remoteUrl, + autoUpdate + ) } - ) + }, + modifier = Modifier.padding(end = 16.dp) + ) { + Text(stringResource(R.string.import_)) + } } ) }, ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) + BaseBundleDialog( + modifier = Modifier.padding(paddingValues), + isDefault = false, + name = name, + onNameChange = { name = it }, + remoteUrl = remoteUrl.takeUnless { isLocal }, + onRemoteUrlChange = { remoteUrl = it }, + patchCount = 0, + version = null, + autoUpdate = autoUpdate, + onAutoUpdateChange = { autoUpdate = it }, + onPatchesClick = {}, + onBundleTypeClick = { isLocal = !isLocal }, ) { - Column( - modifier = Modifier.padding( - start = 24.dp, - top = 16.dp, - end = 24.dp, - ) - ) { - BundleTextContent( - name = name, - onNameChange = { name = it }, - isLocal = isLocal, - remoteUrl = remoteUrl, - onRemoteUrlChange = { remoteUrl = it }, - ) - - if(isLocal) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - value = patchBundleText, - onValueChange = {}, - label = { - Text("Patches Source File") - }, - trailingIcon = { - IconButton( - onClick = onPatchLauncherClick - ) { - Icon( - imageVector = Icons.Default.Topic, - contentDescription = null - ) + if (isLocal) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = patchBundleText, + onValueChange = {}, + label = { + Text("Patches Source File") + }, + trailingIcon = { + IconButton( + onClick = { + patchActivityLauncher.launch(JAR_MIMETYPE) } + ) { + Icon( + imageVector = Icons.Default.Topic, + contentDescription = null + ) } - ) + } + ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - value = integrationText, - onValueChange = {}, - label = { - Text("Integrations Source File") - }, - trailingIcon = { - IconButton(onClick = onIntegrationLauncherClick) { - Icon( - imageVector = Icons.Default.Topic, - contentDescription = null - ) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = integrationText, + onValueChange = {}, + label = { + Text("Integrations Source File") + }, + trailingIcon = { + IconButton( + onClick = { + integrationsActivityLauncher.launch(APK_MIMETYPE) } + ) { + Icon( + imageVector = Icons.Default.Topic, + contentDescription = null + ) } - ) - } - } - - Column( - Modifier.padding( - start = 8.dp, - top = 8.dp, - end = 4.dp, - ) - ) { - BundleInfoContent( - switchChecked = checked, - onCheckedChange = { checked = it }, - patchInfoText = stringResource(R.string.no_patches), - patchCount = patchCount, - onArrowClick = {}, - tonalButtonContent = { - if (isLocal) { - Text(stringResource(R.string.local)) - } else { - Text(stringResource(R.string.remote)) - } - }, - tonalButtonOnClick = { isLocal = !isLocal }, - isLocal = isLocal, + } ) } } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt new file mode 100644 index 00000000..a12b222c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt @@ -0,0 +1,35 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.ui.component.bundle.BundleItem +import app.revanced.manager.ui.viewmodel.BundlesViewModel +import org.koin.androidx.compose.getViewModel + +@Composable +fun BundlesScreen( + vm: BundlesViewModel = getViewModel(), +) { + 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) + } + ) + } + } +} \ 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 af70853a..d43a3c1c 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 @@ -23,34 +23,66 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext 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.ui.component.AppTopBar +import app.revanced.manager.ui.component.bundle.ImportBundleDialog +import app.revanced.manager.ui.viewmodel.DashboardViewModel +import app.revanced.manager.util.toast import kotlinx.coroutines.launch +import org.koin.androidx.compose.getViewModel enum class DashboardPage( val titleResId: Int, val icon: ImageVector ) { DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps), - SOURCES(R.string.tab_sources, Icons.Outlined.Source), + BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), } @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun DashboardScreen( + vm: DashboardViewModel = getViewModel(), onAppSelectorClick: () -> Unit, onSettingsClick: () -> Unit, ) { + var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } val pages: Array = DashboardPage.values() + val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) + val androidContext = LocalContext.current val pagerState = rememberPagerState() val composableScope = rememberCoroutineScope() + if (showImportBundleDialog) { + fun dismiss() { + showImportBundleDialog = false + } + + ImportBundleDialog( + onDismissRequest = ::dismiss, + onLocalSubmit = { name, patches, integrations -> + dismiss() + vm.createLocalSource(name, patches, integrations) + }, + onRemoteSubmit = { name, url, autoUpdate -> + dismiss() + vm.createRemoteSource(name, url, autoUpdate) + }, + ) + } + Scaffold( topBar = { AppTopBar( @@ -66,10 +98,28 @@ fun DashboardScreen( ) }, floatingActionButton = { - FloatingActionButton(onClick = { - if (pagerState.currentPage == DashboardPage.DASHBOARD.ordinal) - onAppSelectorClick() - } + FloatingActionButton( + onClick = { + when (pagerState.currentPage) { + DashboardPage.DASHBOARD.ordinal -> { + if (availablePatches < 1) { + androidContext.toast(androidContext.getString(R.string.patches_unavailable)) + composableScope.launch { + pagerState.animateScrollToPage( + DashboardPage.BUNDLES.ordinal + ) + } + return@FloatingActionButton + } + + onAppSelectorClick() + } + + DashboardPage.BUNDLES.ordinal -> { + showImportBundleDialog = true + } + } + } ) { Icon(Icons.Default.Add, stringResource(R.string.add)) } @@ -103,8 +153,8 @@ fun DashboardScreen( InstalledAppsScreen() } - DashboardPage.SOURCES -> { - SourcesScreen() + DashboardPage.BUNDLES -> { + BundlesScreen() } } } 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 1c7fea53..d7849c55 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 @@ -68,7 +68,7 @@ fun PatchesSelectorScreen( val pagerState = rememberPagerState() val composableScope = rememberCoroutineScope() - val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyArray()) + val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) if (vm.compatibleVersions.isNotEmpty()) UnsupportedDialog( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt deleted file mode 100644 index 28e822c3..00000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt +++ /dev/null @@ -1,69 +0,0 @@ -package app.revanced.manager.ui.screen - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope -import app.revanced.manager.R -import app.revanced.manager.ui.component.bundle.ImportBundleDialog -import app.revanced.manager.ui.component.SourceItem -import app.revanced.manager.ui.viewmodel.SourcesViewModel -import org.koin.androidx.compose.getViewModel - -@Composable -fun SourcesScreen( - vm: SourcesViewModel = getViewModel(), -) { - var showNewSourceDialog by rememberSaveable { mutableStateOf(false) } - val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) - - if (showNewSourceDialog) { - ImportBundleDialog( - onDismissRequest = { showNewSourceDialog = false }, - onLocalSubmit = { name, patches, integrations -> - showNewSourceDialog = false - vm.addLocal(name, patches, integrations) - }, - onRemoteSubmit = { name, url -> - showNewSourceDialog = false - vm.addRemote(name, url) - }, - ) - } - - Column( - modifier = Modifier - .fillMaxSize(), - ) { - sources.forEach { - SourceItem( - source = it, - onDelete = { - vm.delete(it) - }, - coroutineScope = vm.viewModelScope - ) - } - - Button(onClick = vm::redownloadAllSources) { - Text(stringResource(R.string.reload_sources)) - } - - Button(onClick = { showNewSourceDialog = true }) { - Text("Create new source") - } - - Button(onClick = vm::deleteAllSources) { - Text("Reset everything.") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 79151301..e390332a 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 @@ -1,34 +1,59 @@ package app.revanced.manager.ui.screen.settings import android.app.ActivityManager -import android.content.Context import android.os.Build +import androidx.compose.foundation.clickable +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.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Http +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold 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 import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel +import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AdvancedSettingsScreen(onBackClick: () -> Unit) { +fun AdvancedSettingsScreen( + onBackClick: () -> Unit, + vm: AdvancedSettingsViewModel = getViewModel() +) { val context = LocalContext.current val memoryLimit = remember { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - context.getString(R.string.device_memory_limit_format, activityManager.memoryClass, activityManager.largeMemoryClass) + val activityManager = context.getSystemService()!! + context.getString( + R.string.device_memory_limit_format, + activityManager.memoryClass, + activityManager.largeMemoryClass + ) } + Scaffold( topBar = { AppTopBar( @@ -43,6 +68,37 @@ fun AdvancedSettingsScreen(onBackClick: () -> Unit) { .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { + val apiUrl by vm.apiUrl.getAsState() + var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } + + if (showApiUrlDialog) { + APIUrlDialog(apiUrl) { + showApiUrlDialog = false + it?.let(vm::setApiUrl) + } + } + ListItem( + headlineContent = { Text(stringResource(R.string.api_url)) }, + supportingContent = { Text(apiUrl) }, + modifier = Modifier.clickable { + showApiUrlDialog = true + } + ) + + GroupHeader(stringResource(R.string.patch_bundles_section)) + ListItem( + headlineContent = { Text(stringResource(R.string.patch_bundles_redownload)) }, + modifier = Modifier.clickable { + vm.redownloadBundles() + } + ) + ListItem( + headlineContent = { Text(stringResource(R.string.patch_bundles_reset)) }, + modifier = Modifier.clickable { + vm.resetBundles() + } + ) + GroupHeader(stringResource(R.string.device)) ListItem( headlineContent = { Text(stringResource(R.string.device_model)) }, @@ -62,4 +118,58 @@ fun AdvancedSettingsScreen(onBackClick: () -> Unit) { ) } } +} + +@Composable +private fun APIUrlDialog(currentUrl: String, onSubmit: (String?) -> Unit) { + var url by rememberSaveable(currentUrl) { mutableStateOf(currentUrl) } + + AlertDialog( + onDismissRequest = { onSubmit(null) }, + confirmButton = { + TextButton( + onClick = { + onSubmit(url) + } + ) { + Text(stringResource(R.string.api_url_dialog_save)) + } + }, + dismissButton = { + TextButton(onClick = { onSubmit(null) }) { + Text(stringResource(R.string.cancel)) + } + }, + icon = { + Icon(Icons.Outlined.Http, null) + }, + title = { + Text( + text = stringResource(R.string.api_url_dialog_title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.api_url_dialog_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.api_url_dialog_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text(stringResource(R.string.api_url)) } + ) + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 17dee23d..80db8178 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -31,7 +31,7 @@ import app.revanced.manager.ui.viewmodel.ImportExportViewModel import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.PasswordField -import app.revanced.manager.ui.component.bundle.SourceSelector +import app.revanced.manager.ui.component.bundle.BundleSelector import app.revanced.manager.util.toast import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel @@ -63,12 +63,12 @@ fun ImportExportSettingsScreen( } } - if (vm.selectedSource == null) { - SourceSelector(sources) { + if (vm.selectedBundle == null) { + BundleSelector(sources) { if (it == null) { vm.clearSelectionAction() } else { - vm.selectSource(it) + vm.selectBundle(it) launcher.launch(action.activityArg) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt new file mode 100644 index 00000000..6b084005 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class AdvancedSettingsViewModel( + prefs: PreferencesManager, + private val app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + val apiUrl = prefs.api + + fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) { + if (value == apiUrl.get()) return@launch + + apiUrl.update(value) + patchBundleRepository.reloadApiBundles() + } + + fun redownloadBundles() = viewModelScope.launch { + uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { + patchBundleRepository.redownloadRemoteBundles() + } + } + + fun resetBundles() = viewModelScope.launch { + patchBundleRepository.reset() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt new file mode 100644 index 00000000..89d2876a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt @@ -0,0 +1,33 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.launch + +class BundlesViewModel( + private val app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + val sources = patchBundleRepository.sources + + fun delete(bundle: PatchBundleSource) = + viewModelScope.launch { patchBundleRepository.remove(bundle) } + + fun update(bundle: PatchBundleSource) = viewModelScope.launch { + if (bundle !is RemotePatchBundle) return@launch + + uiSafe( + app, + R.string.source_download_fail, + RemotePatchBundle.updateFailMsg + ) { + bundle.update() + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..362aff44 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -0,0 +1,35 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.domain.repository.PatchBundleRepository +import io.ktor.http.Url +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class DashboardViewModel( + app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + val availablePatches = + patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } + private val contentResolver: ContentResolver = app.contentResolver + + fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = + viewModelScope.launch { + contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> + val integrationsStream = integrations?.let { contentResolver.openInputStream(it) } + try { + patchBundleRepository.createLocal(name, patchesStream, integrationsStream) + } finally { + integrationsStream?.close() + } + } + } + + fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) = + viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt index 87a44d62..2bdee157 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -14,8 +14,8 @@ import app.revanced.manager.R import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SerializedSelection -import app.revanced.manager.domain.repository.SourceRepository -import app.revanced.manager.domain.sources.Source +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.util.JSON_MIMETYPE import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe @@ -37,11 +37,11 @@ class ImportExportViewModel( private val app: Application, private val keystoreManager: KeystoreManager, private val selectionRepository: PatchSelectionRepository, - sourceRepository: SourceRepository + patchBundleRepository: PatchBundleRepository ) : ViewModel() { private val contentResolver = app.contentResolver - val sources = sourceRepository.sources - var selectedSource by mutableStateOf(null) + val sources = patchBundleRepository.sources + var selectedBundle by mutableStateOf(null) private set var selectionAction by mutableStateOf(null) private set @@ -107,20 +107,20 @@ class ImportExportViewModel( } fun executeSelectionAction(target: Uri) = viewModelScope.launch { - val source = selectedSource!! + val source = selectedBundle!! val action = selectionAction!! clearSelectionAction() - action.execute(source, target) + action.execute(source.uid, target) } - fun selectSource(source: Source) { - selectedSource = source + fun selectBundle(bundle: PatchBundleSource) { + selectedBundle = bundle } fun clearSelectionAction() { selectionAction = null - selectedSource = null + selectedBundle = null } fun importSelection() = clearSelectionAction().also { @@ -132,7 +132,7 @@ class ImportExportViewModel( } sealed interface SelectionAction { - suspend fun execute(source: Source, location: Uri) + suspend fun execute(bundleUid: Int, location: Uri) val activityContract: ActivityResultContract val activityArg: String } @@ -140,7 +140,7 @@ class ImportExportViewModel( private inner class Import : SelectionAction { override val activityContract = ActivityResultContracts.GetContent() override val activityArg = JSON_MIMETYPE - override suspend fun execute(source: Source, location: Uri) = uiSafe( + override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( app, R.string.restore_patches_selection_fail, "Failed to restore patches selection" @@ -151,19 +151,19 @@ class ImportExportViewModel( } } - selectionRepository.import(source, selection) + selectionRepository.import(bundleUid, selection) } } private inner class Export : SelectionAction { override val activityContract = ActivityResultContracts.CreateDocument(JSON_MIMETYPE) override val activityArg = "selection.json" - override suspend fun execute(source: Source, location: Uri) = uiSafe( + override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( app, R.string.backup_patches_selection_fail, "Failed to backup patches selection" ) { - val selection = selectionRepository.export(source) + val selection = selectionRepository.export(bundleUid) withContext(Dispatchers.IO) { contentResolver.openOutputStream(location, "wt")!!.use { 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 f985bbff..5592d3e7 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 @@ -2,16 +2,31 @@ package app.revanced.manager.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.revanced.manager.domain.repository.SourceRepository +import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class MainViewModel( - sourceRepository: SourceRepository + private val patchBundleRepository: PatchBundleRepository, + val prefs: PreferencesManager ) : ViewModel() { - init { - with(viewModelScope) { - launch { - sourceRepository.loadSources() + + fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch { + prefs.showAutoUpdatesDialog.update(false) + + prefs.managerAutoUpdates.update(manager) + if (patches) { + with(patchBundleRepository) { + sources + .first() + .find { it.uid == 0 } + ?.asRemoteOrNull + ?.setAutoUpdate(true) + + updateCheck() } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 023a5359..407520f1 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -15,7 +15,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchSelectionRepository -import app.revanced.manager.domain.repository.SourceRepository +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options @@ -43,21 +43,23 @@ class PatchesSelectorViewModel( private val savedStateHandle: SavedStateHandle = get() val allowExperimental = get().allowExperimental - val bundlesFlow = get().sources.flatMapLatestAndCombine( - combiner = { it } + val bundlesFlow = get().sources.flatMapLatestAndCombine( + combiner = { it.filterNotNull() } ) { source -> // Regenerate bundle information whenever this source updates. - source.bundle.map { bundle -> + source.state.map { state -> + val bundle = state.patchBundleOrNull() ?: return@map null + val supported = mutableListOf() val unsupported = mutableListOf() val universal = mutableListOf() bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach { - val targetList = - if (it.compatiblePackages == null) universal else if (it.supportsVersion(selectedApp.version)) - supported - else - unsupported + val targetList = when { + it.compatiblePackages == null -> universal + it.supportsVersion(selectedApp.version) -> supported + else -> unsupported + } targetList.add(it) } @@ -66,30 +68,36 @@ class PatchesSelectorViewModel( } } - private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = { - val map: SnapshotStatePatchesSelection = mutableStateMapOf() - viewModelScope.launch(Dispatchers.Default) { - val bundles = bundlesFlow.first() - val filteredSelection = - selectionRepository.getSelection(selectedApp.packageName).mapValues { (uid, patches) -> - // Filter out patches that don't exist. - val filteredPatches = bundles.singleOrNull { it.uid == uid } - ?.let { bundle -> - val allPatches = bundle.all.map { it.name } - patches.filter { allPatches.contains(it) } + private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable( + saver = patchesSelectionSaver, + init = { + val map: SnapshotStatePatchesSelection = mutableStateMapOf() + viewModelScope.launch(Dispatchers.Default) { + val bundles = bundlesFlow.first() + val filteredSelection = + selectionRepository.getSelection(selectedApp.packageName) + .mapValues { (uid, patches) -> + // Filter out patches that don't exist. + val filteredPatches = bundles.singleOrNull { it.uid == uid } + ?.let { bundle -> + val allPatches = bundle.all.map { it.name } + patches.filter { allPatches.contains(it) } + } + ?: patches + + filteredPatches.toMutableStateSet() } - ?: patches - filteredPatches.toMutableStateSet() + withContext(Dispatchers.Main) { + map.putAll(filteredSelection) } - - withContext(Dispatchers.Main) { - map.putAll(filteredSelection) } - } - return@saveable map - }) - private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(saver = optionsSaver, init = ::mutableStateMapOf) + return@saveable map + }) + private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable( + saver = optionsSaver, + init = ::mutableStateMapOf + ) /** * Show the patch options dialog for this patch. diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SourcesViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SourcesViewModel.kt deleted file mode 100644 index 68f34d2a..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SourcesViewModel.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.app.Application -import android.content.ContentResolver -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.revanced.manager.R -import app.revanced.manager.domain.sources.Source -import app.revanced.manager.domain.repository.SourceRepository -import app.revanced.manager.util.uiSafe -import io.ktor.http.* -import kotlinx.coroutines.launch - -class SourcesViewModel( - private val app: Application, - private val sourceRepository: SourceRepository -) : ViewModel() { - val sources = sourceRepository.sources - private val contentResolver: ContentResolver = app.contentResolver - - companion object { - const val failLogMsg = "Failed to update patch bundle(s)" - } - - fun redownloadAllSources() = viewModelScope.launch { - uiSafe(app, R.string.source_download_fail, failLogMsg) { - sourceRepository.redownloadRemoteSources() - } - } - - fun addLocal(name: String, patchBundle: Uri, integrations: Uri?) = viewModelScope.launch { - contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> - val integrationsStream = integrations?.let { contentResolver.openInputStream(it) } - try { - sourceRepository.createLocalSource(name, patchesStream, integrationsStream) - } finally { - integrationsStream?.close() - } - } - } - - fun addRemote(name: String, apiUrl: Url) = - viewModelScope.launch { sourceRepository.createRemoteSource(name, apiUrl) } - - fun delete(source: Source) = viewModelScope.launch { sourceRepository.remove(source) } - - fun deleteAllSources() = viewModelScope.launch { - sourceRepository.resetConfig() - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt index aac27f11..504f471e 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.domain.repository.DownloadedAppRepository -import app.revanced.manager.domain.repository.SourceRepository +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 @@ -28,7 +28,7 @@ class VersionSelectorViewModel( val packageName: String ) : ViewModel(), KoinComponent { private val downloadedAppRepository: DownloadedAppRepository by inject() - private val sourceRepository: SourceRepository by inject() + private val patchBundleRepository: PatchBundleRepository by inject() private val pm: PM by inject() private val appDownloader: AppDownloader = APKMirror() @@ -41,7 +41,7 @@ class VersionSelectorViewModel( val downloadableVersions = mutableStateSetOf() - val supportedVersions = sourceRepository.bundles.map { bundles -> + val supportedVersions = patchBundleRepository.bundles.map { bundles -> var patchesWithoutVersions = 0 bundles.flatMap { (_, bundle) -> 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 faaca7bb..983a7c42 100644 --- a/app/src/main/java/app/revanced/manager/util/Constants.kt +++ b/app/src/main/java/app/revanced/manager/util/Constants.kt @@ -8,7 +8,6 @@ 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 apiURL = "https://releases.revanced.app" const val JAR_MIMETYPE = "application/java-archive" const val APK_MIMETYPE = "application/vnd.android.package-archive" 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 c1e0d477..f8ea2dad 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -13,7 +13,7 @@ import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable -import app.revanced.manager.domain.repository.SourceRepository +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.service.InstallService import app.revanced.manager.service.UninstallService import kotlinx.coroutines.CoroutineScope @@ -40,11 +40,11 @@ data class AppInfo( @Suppress("DEPRECATION") class PM( private val app: Application, - sourceRepository: SourceRepository + patchBundleRepository: PatchBundleRepository ) { private val scope = CoroutineScope(Dispatchers.IO) - val appList = sourceRepository.bundles.map { bundles -> + val appList = patchBundleRepository.bundles.map { bundles -> val compatibleApps = scope.async { val compatiblePackages = bundles.values .flatMap { it.patches } 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 a35797f7..a6b478ac 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -11,7 +11,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import io.ktor.http.Url import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -33,12 +32,6 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) { Toast.makeText(this, string, duration).show() } -fun String.parseUrlOrNull() = try { - Url(this) -} catch (_: Throwable) { - null -} - /** * Safely perform an operation that may fail to avoid crashing the app. * If [block] fails, the error will be logged and a toast will be shown to the user to inform them that the action failed. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98dd93ec..3fd66f17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,11 +12,20 @@ Select patches Import - Import Bundle + Import patch bundle Bundle information Bundle patches + Missing + Error + Select version + + Select updates to receive + Periodically connect to update providers to check for updates. + Manager updates + Patches + These settings can be changed later. General General settings @@ -90,22 +99,30 @@ Dark Appearance Downloaded apps + API URL + Set custom API URL + You may have issues with features when using a custom API URL. + Only use API\'s you trust! + Set Device Android version Model CPU Architectures Memory limits Normal: %1$d MB, Large: %2$d MB + Patch bundles + Redownload all patch bundles + Reset patch bundles Patching Signing Storage + No patches are available. Check your bundles Apps - Sources + Patch bundles Delete Refresh Remote Local - Reload all sources Continue anyways Download another version Download app @@ -181,8 +198,6 @@ Automatically update this bundle when ReVanced starts Bundle type Choose the type of bundle you want - Patches version - Integrations version About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes! @@ -199,6 +214,7 @@ Downloading update… Failed to download update: %s Cancel + Save Update Tap on Update when prompted. \n ReVanced Manager will close when updating. \ No newline at end of file