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 fd83a51ed1..17ef86523c 100644
--- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
+++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
@@ -23,8 +23,7 @@
{
"fieldPath": "version",
"columnName": "version",
- "affinity": "TEXT",
- "notNull": false
+ "affinity": "TEXT"
},
{
"fieldPath": "source",
@@ -44,9 +43,7 @@
"columnNames": [
"uid"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "patch_selections",
@@ -127,7 +124,6 @@
"patch_name"
]
},
- "indices": [],
"foreignKeys": [
{
"table": "patch_selections",
@@ -177,9 +173,7 @@
"package_name",
"version"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "installed_app",
@@ -215,9 +209,7 @@
"columnNames": [
"current_package_name"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "applied_patch",
@@ -378,7 +370,6 @@
"key"
]
},
- "indices": [],
"foreignKeys": [
{
"table": "option_groups",
@@ -415,12 +406,9 @@
"columnNames": [
"package_name"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
}
],
- "views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/2.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/2.json
new file mode 100644
index 0000000000..96091a6e91
--- /dev/null
+++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/2.json
@@ -0,0 +1,448 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "799b1231ff07c511b2c9a27b4c28c19d",
+ "entities": [
+ {
+ "tableName": "patch_bundles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `version` TEXT, `search_update` INTEGER NOT NULL, `auto_update` INTEGER NOT NULL, `changelog` TEXT, `publish_date` TEXT, `latest_changelog` TEXT, `latest_publish_date` TEXT, `latest_version` TEXT, PRIMARY KEY(`uid`))",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "source",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "properties.version",
+ "columnName": "version",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "properties.searchUpdate",
+ "columnName": "search_update",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "properties.autoUpdate",
+ "columnName": "auto_update",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteProperties.changelog",
+ "columnName": "changelog",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteProperties.publishDate",
+ "columnName": "publish_date",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteLatestProperties.latestChangelog",
+ "columnName": "latest_changelog",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteLatestProperties.latestPublishDate",
+ "columnName": "latest_publish_date",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteLatestProperties.latestVersion",
+ "columnName": "latest_version",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ },
+ {
+ "tableName": "patch_selections",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchBundle",
+ "columnName": "patch_bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_patch_selections_patch_bundle_package_name",
+ "unique": true,
+ "columnNames": [
+ "patch_bundle",
+ "package_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "patch_bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "selected_patches",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "selection",
+ "columnName": "selection",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "selection",
+ "patch_name"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "patch_selections",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "selection"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "downloaded_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "directory",
+ "columnName": "directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsed",
+ "columnName": "last_used",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name",
+ "version"
+ ]
+ }
+ },
+ {
+ "tableName": "installed_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
+ "fields": [
+ {
+ "fieldPath": "currentPackageName",
+ "columnName": "current_package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originalPackageName",
+ "columnName": "original_package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "installType",
+ "columnName": "install_type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "current_package_name"
+ ]
+ }
+ },
+ {
+ "tableName": "applied_patch",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bundle",
+ "columnName": "bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name",
+ "bundle",
+ "patch_name"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_applied_patch_bundle",
+ "unique": false,
+ "columnNames": [
+ "bundle"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "installed_app",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "package_name"
+ ],
+ "referencedColumns": [
+ "current_package_name"
+ ]
+ },
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "option_groups",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchBundle",
+ "columnName": "patch_bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_option_groups_patch_bundle_package_name",
+ "unique": true,
+ "columnNames": [
+ "patch_bundle",
+ "package_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "patch_bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "group",
+ "patch_name",
+ "key"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "option_groups",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "group"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "trusted_downloader_plugins",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "BLOB",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name"
+ ]
+ }
+ }
+ ],
+ "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, '799b1231ff07c511b2c9a27b4c28c19d')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0404d045d8..1709a7f73d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
android:description="@string/plugin_host_permission_description"
/>
+
diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
index 1d17e5ef61..be4f316cdb 100644
--- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt
+++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
@@ -4,17 +4,26 @@ import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
+import androidx.work.Configuration
import app.revanced.manager.data.platform.Filesystem
-import app.revanced.manager.di.*
+import app.revanced.manager.di.databaseModule
+import app.revanced.manager.di.httpModule
+import app.revanced.manager.di.managerModule
+import app.revanced.manager.di.preferencesModule
+import app.revanced.manager.di.repositoryModule
+import app.revanced.manager.di.rootModule
+import app.revanced.manager.di.serviceModule
+import app.revanced.manager.di.viewModelModule
+import app.revanced.manager.di.workerModule
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag
-import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.BuilderImpl
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
@@ -25,13 +34,17 @@ import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
-class ManagerApplication : Application() {
+class ManagerApplication : Application(), Configuration.Provider {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val fs: Filesystem by inject()
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
+ .build()
+
override fun onCreate() {
super.onCreate()
@@ -51,7 +64,6 @@ class ManagerApplication : Application() {
rootModule
)
}
-
val pixels = 512
Coil.setImageLoader(
ImageLoader.Builder(this)
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 403bd1cf71..8bff26a528 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
@@ -22,7 +22,7 @@ import kotlin.random.Random
@Database(
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
- version = 1
+ version = 2
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
index d9955a702b..ee01d9f273 100644
--- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
@@ -2,21 +2,37 @@ package app.revanced.manager.data.room.bundles
import androidx.room.*
import kotlinx.coroutines.flow.Flow
+import kotlinx.datetime.LocalDateTime
@Dao
interface PatchBundleDao {
@Query("SELECT * FROM patch_bundles")
suspend fun all(): List
- @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
+ @Query("SELECT version, search_update, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow
+ @Query("SELECT latest_version, latest_changelog, latest_publish_date FROM patch_bundles WHERE uid = :uid")
+ fun getLatestPropsById(uid: Int): Flow
+
+ @Query("SELECT changelog, publish_date FROM patch_bundles WHERE uid = :uid")
+ fun getInstalledProps(uid: Int): Flow
+
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?)
+ @Query("UPDATE patch_bundles SET changelog = :changelog, publish_date = :createdAt WHERE uid = :uid")
+ suspend fun updateInstallationProps(uid: Int, changelog: String, createdAt: String)
+
+ @Query("UPDATE patch_bundles SET latest_version = :version, latest_changelog = :changelog, latest_publish_date = :createdAt WHERE uid = :uid")
+ suspend fun updateLatestRemoteInfo(uid: Int, version: String, changelog: String, createdAt: String)
+
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean)
+ @Query("UPDATE patch_bundles SET search_update = :value WHERE uid = :uid")
+ suspend fun setSearchUpdate(uid: Int, value: Boolean)
+
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
suspend fun setName(uid: Int, value: String)
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
index 8ba5f64a96..4664aac56d 100644
--- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
@@ -33,12 +33,25 @@ sealed class Source {
data class PatchBundleEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String,
- @ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "source") val source: Source,
- @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
+ @Embedded val properties: BundleProperties,
+ @Embedded val remoteProperties: RemoteBundleProperties? = null,
+ @Embedded val remoteLatestProperties: RemoteLatestBundleProperties? = null
)
data class BundleProperties(
@ColumnInfo(name = "version") val version: String? = null,
+ @ColumnInfo(name = "search_update") val searchUpdate: Boolean,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
+)
+
+data class RemoteBundleProperties(
+ @ColumnInfo(name = "changelog") val changelog: String? = null,
+ @ColumnInfo(name = "publish_date") val publishDate: String? = null
+)
+
+data class RemoteLatestBundleProperties(
+ @ColumnInfo(name = "latest_version") val latestVersion: String? = null,
+ @ColumnInfo(name = "latest_changelog") val latestChangelog: String? = null,
+ @ColumnInfo(name = "latest_publish_date") val latestPublishDate: String? = null
)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
index 37d8c05dd6..465630b714 100644
--- a/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
@@ -2,12 +2,33 @@ package app.revanced.manager.di
import android.content.Context
import androidx.room.Room
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
import app.revanced.manager.data.room.AppDatabase
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
+
+val MIGRATION_1_2 = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ // Add columns for RemoteBundleProperties
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN search_update INTEGER NOT NULL DEFAULT 0")
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN changelog TEXT")
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN publish_date TEXT")
+
+ // Add columns for RemoteLatestBundleProperties
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN latest_changelog TEXT")
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN latest_publish_date TEXT")
+ database.execSQL("ALTER TABLE patch_bundles ADD COLUMN latest_version TEXT")
+ }
+}
+
val databaseModule = module {
- fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build()
+ fun provideAppDatabase(context: Context) =
+ Room
+ .databaseBuilder(context, AppDatabase::class.java, "manager")
+ .addMigrations(MIGRATION_1_2)
+ .build()
single {
provideAppDatabase(androidContext())
diff --git a/app/src/main/java/app/revanced/manager/di/WorkerModule.kt b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt
index d5d9112e9b..56963d5c3d 100644
--- a/app/src/main/java/app/revanced/manager/di/WorkerModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt
@@ -1,9 +1,11 @@
package app.revanced.manager.di
+import app.revanced.manager.patcher.worker.BundleUpdateNotificationWorker
import app.revanced.manager.patcher.worker.PatcherWorker
import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module
val workerModule = module {
workerOf(::PatcherWorker)
+ workerOf(::BundleUpdateNotificationWorker)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
index bcbc59cf83..a150eb75fe 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt
@@ -15,7 +15,7 @@ class LocalPatchBundle(name: String, id: Int, directory: File) :
}
reload()?.also {
- saveVersion(it.readManifestAttribute("Version"))
+ updateVersion(it.readManifestAttribute("Version"))
}
}
}
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
index 308e2a56dd..c32aa6f920 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt
@@ -84,10 +84,18 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!!
- suspend fun currentVersion() = getProps().version
- protected suspend fun saveVersion(version: String?) =
+ fun installedPropsFlow() = configRepository.getInstalledProps(uid).flowOn(Dispatchers.Default)
+ suspend fun getInstalledProps() = installedPropsFlow().first()!!
+
+ fun latestPropsFlow() = configRepository.getLatestProps(uid).flowOn(Dispatchers.Default)
+ suspend fun getLatestProps() = latestPropsFlow().first()!!
+
+ protected suspend fun updateVersion(version: String?) =
configRepository.updateVersion(uid, version)
+ protected suspend fun updateInstallationProps(changelog: String, createdAt: String) =
+ configRepository.updateInstallationProps(uid, changelog, createdAt)
+
suspend fun setName(name: String) {
configRepository.setName(uid, name)
_nameFlow.value = name
diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
index 9deb7bbe22..5986a87fb9 100644
--- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
+++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt
@@ -10,12 +10,33 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import java.io.File
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+
+
+class RemotePatchBundleFetchResponse(
+ val response: ReVancedAsset,
+ oldVersion: String?,
+ oldLatestVersion: String?
+) {
+ val isNewLatestVersion = response.version != oldLatestVersion
+
+ val isLatestInstalled = response.version == oldVersion
+}
@Stable
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
PatchBundleSource(name, id, directory) {
protected val http: HttpService by inject()
+ fun canUpdateVersionFlow(): Flow =
+ combine(propsFlow(), latestPropsFlow()) { current, latest ->
+ current?.version != latest?.latestVersion
+ }
+
+ suspend fun canUpdateVersion() = canUpdateVersionFlow().first()
+
protected abstract suspend fun getLatestInfo(): ReVancedAsset
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
@@ -25,7 +46,8 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
}
}
- saveVersion(info.version)
+ updateVersion(info.version)
+ updateInstallationProps(info.description, info.createdAt.toString())
reload()
}
@@ -33,12 +55,25 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
download(getLatestInfo())
}
+ suspend fun fetchLatestRemoteInfo(): RemotePatchBundleFetchResponse = withContext(Dispatchers.Default) {
+ getLatestInfo().let {
+ val result = RemotePatchBundleFetchResponse(it, getProps().version, getLatestProps().latestVersion)
+ configRepository.updateLatestRemoteInfo(
+ uid,
+ it.version,
+ it.description,
+ it.createdAt.toString()
+ )
+ result
+ }
+ }
+
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
- val info = getLatestInfo()
- if (hasInstalled() && info.version == currentVersion())
+ val fetchedInfo = fetchLatestRemoteInfo()
+ if (!canUpdateVersion())
return@withContext false
- download(info)
+ download(fetchedInfo.response)
true
}
@@ -49,6 +84,8 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
+ suspend fun setSearchUpdate(value: Boolean) = configRepository.setSearchUpdate(uid, value)
+
companion object {
const val updateFailMsg = "Failed to update patch bundle(s)"
}
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 90f45a1c87..d94cbacceb 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
@@ -1,10 +1,19 @@
package app.revanced.manager.domain.manager
import android.content.Context
+import app.revanced.manager.R
import app.revanced.manager.domain.manager.base.BasePreferencesManager
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.isDebuggable
+
+enum class SearchForUpdatesBackgroundInterval(val displayName: Int, val value: Long) {
+ NEVER(R.string.never, 0),
+ MIN15(R.string.minutes_15, 15),
+ HOUR(R.string.hourly, 60),
+ DAY(R.string.daily, 60 * 24)
+}
+
class PreferencesManager(
context: Context
) : BasePreferencesManager(context, "settings") {
@@ -22,6 +31,7 @@ class PreferencesManager(
val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
+ val searchForUpdatesBackgroundInterval = enumPreference("background_bundle_update_time", SearchForUpdatesBackgroundInterval.NEVER)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val disableSelectionWarning = booleanPreference("disable_selection_warning", 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
index 5711d99758..faa1a40b61 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt
@@ -2,9 +2,11 @@ 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.BundleProperties
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.bundles.Source
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.datetime.LocalDateTime
class PatchBundlePersistenceRepository(db: AppDatabase) {
private val dao = db.patchBundleDao()
@@ -21,13 +23,16 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun reset() = dao.reset()
- suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
+ suspend fun create(name: String, source: Source, searchUpdate: Boolean = false, autoUpdate: Boolean = false) =
PatchBundleEntity(
uid = generateUid(),
name = name,
- version = null,
source = source,
- autoUpdate = autoUpdate
+ properties = BundleProperties(
+ version = null,
+ searchUpdate = searchUpdate,
+ autoUpdate = autoUpdate
+ )
).also {
dao.add(it)
}
@@ -37,19 +42,34 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun updateVersion(uid: Int, version: String?) =
dao.updateVersion(uid, version)
+ suspend fun updateInstallationProps(uid: Int, changelog: String, createdAt: String) =
+ dao.updateInstallationProps(uid, changelog, createdAt)
+
+ suspend fun updateLatestRemoteInfo(uid: Int, version: String, changelog: String, createdAt: String) =
+ dao.updateLatestRemoteInfo(uid, version, changelog, createdAt)
+
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
+ suspend fun setSearchUpdate(uid: Int, value: Boolean) = dao.setSearchUpdate(uid, value)
+
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
+ fun getLatestProps(id: Int) = dao.getLatestPropsById(id).distinctUntilChanged()
+
+ fun getInstalledProps(id: Int) = dao.getInstalledProps(id).distinctUntilChanged()
+
private companion object {
val defaultSource = PatchBundleEntity(
uid = 0,
name = "",
- version = null,
- source = Source.API,
- autoUpdate = false
+ properties = BundleProperties(
+ version = null,
+ searchUpdate = false,
+ autoUpdate = false
+ ),
+ source = Source.API
)
}
}
\ 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
index 79bb5cea64..7b73d72278 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt
@@ -1,6 +1,8 @@
package app.revanced.manager.domain.repository
import android.app.Application
+import android.app.Notification
+import android.app.NotificationManager
import android.content.Context
import android.util.Log
import app.revanced.library.mostCommonCompatibleVersions
@@ -9,13 +11,13 @@ 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.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.util.flatMapLatestAndCombine
+import app.revanced.manager.util.permission.hasNotificationPermission
import app.revanced.manager.util.tag
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.Dispatchers
@@ -27,6 +29,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InputStream
+import app.revanced.manager.data.room.bundles.Source as SourceInfo
class PatchBundleRepository(
private val app: Application,
@@ -145,8 +148,8 @@ class PatchBundleRepository(
addBundle(bundle)
}
- suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
- val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
+ suspend fun createRemote(url: String, searchUpdate: Boolean, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
+ val entity = persistenceRepo.create("", SourceInfo.from(url), searchUpdate, autoUpdate)
addBundle(entity.load())
}
@@ -181,4 +184,23 @@ class PatchBundleRepository(
}
}
}
+
+ suspend fun fetchUpdatesAndNotify(context: Context, notificationBlock: (bundleName: String, bundleVersion: String) -> Pair) {
+ coroutineScope {
+ getBundlesByType().forEach { bundle ->
+ Log.d(tag, "Running fetchUpdatesAndNotify for bundle: ${bundle.getName()}")
+ if (!bundle.getProps().searchUpdate || !context.hasNotificationPermission())
+ return@forEach
+
+ var fetchResponse = bundle.fetchLatestRemoteInfo()
+ if (
+ !fetchResponse.isNewLatestVersion|| // Already notified
+ fetchResponse.isLatestInstalled // Latest is already installed
+ )
+ return@forEach
+
+ notificationBlock(bundle.getName(), fetchResponse.response.version)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt
index 222a31c48b..6732ad1997 100644
--- a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt
@@ -1,11 +1,25 @@
package app.revanced.manager.domain.worker
import android.app.Application
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.util.Log
+import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
+import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
+import app.revanced.manager.R
+import app.revanced.manager.domain.manager.SearchForUpdatesBackgroundInterval
+import app.revanced.manager.patcher.worker.BundleUpdateNotificationWorker
import java.util.UUID
+import java.util.concurrent.TimeUnit
class WorkerRepository(app: Application) {
val workManager = WorkManager.getInstance(app)
@@ -33,4 +47,52 @@ class WorkerRepository(app: Application) {
workManager.enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request)
return request.id
}
+
+ inline fun createNotification(
+ context: Context,
+ notificationChannel: NotificationChannel,
+ title: String,
+ description: String
+ ): Pair {
+ val notificationIntent = Intent(context, T::class.java)
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notificationManager = context
+ .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(notificationChannel)
+
+ return Pair(
+ Notification.Builder(context, notificationChannel.id)
+ .setContentTitle(title)
+ .setContentText(description)
+ .setLargeIcon(Icon.createWithResource(context, R.drawable.ic_notification))
+ .setSmallIcon(Icon.createWithResource(context, R.drawable.ic_notification))
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .build(),
+ notificationManager
+ )
+ }
+
+ fun scheduleBundleUpdateNotificationWork(bundleUpdateTime: SearchForUpdatesBackgroundInterval) {
+ val workId = "BundleUpdateNotificationWork"
+ if(bundleUpdateTime == SearchForUpdatesBackgroundInterval.NEVER) {
+ workManager.cancelUniqueWork(workId)
+ Log.d("WorkManager","Cancelled job with workId $workId.")
+ } else {
+ val workRequest =
+ PeriodicWorkRequestBuilder(bundleUpdateTime.value, TimeUnit.MINUTES)
+ .build()
+
+ workManager
+ .enqueueUniquePeriodicWork(
+ workId,
+ ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
+ workRequest
+ )
+ Log.d("WorkManager", "Periodic work $workId updated with time ${bundleUpdateTime.value}.")
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/BundleUpdateNotificationWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/BundleUpdateNotificationWorker.kt
new file mode 100644
index 0000000000..b2074187d6
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/BundleUpdateNotificationWorker.kt
@@ -0,0 +1,69 @@
+package app.revanced.manager.patcher.worker
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import androidx.work.WorkerParameters
+import app.revanced.manager.MainActivity
+import app.revanced.manager.R
+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.plugin.downloader.PluginHostApi
+import app.revanced.manager.util.permission.hasNotificationPermission
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+@OptIn(PluginHostApi::class)
+class BundleUpdateNotificationWorker(
+ context: Context,
+ parameters: WorkerParameters
+) : Worker(context, parameters), KoinComponent {
+ private val patchBundleRepository: PatchBundleRepository by inject()
+ private val workerRepository: WorkerRepository by inject()
+
+ class Args()
+
+ val notificationChannel = NotificationChannel(
+ "background-bundle-update-channel", "Background Check Notifications", NotificationManager.IMPORTANCE_HIGH
+ )
+ companion object {
+ const val LOG_TAG = "BundleAutoUpdateWorker"
+ }
+
+ override suspend fun doWork(): Result {
+ /**
+ * If the user did not consent to be notified, there is no point in checking in background.
+ * The auto update will still happen on app opening.
+ **/
+ if (!applicationContext.hasNotificationPermission())
+ return Result.success()
+
+ Log.d(LOG_TAG, "Searching for updates.")
+ return try {
+ patchBundleRepository.fetchUpdatesAndNotify(applicationContext) { bundleName, bundleVersion ->
+ workerRepository.createNotification(
+ applicationContext,
+ notificationChannel,
+ applicationContext.getString(R.string.bundle_update_available),
+ applicationContext.getString(
+ R.string.bundle_update_description_available,
+ bundleName,
+ bundleVersion
+ )
+ ).also { (notification, notificationManager) ->
+ if (applicationContext.hasNotificationPermission())
+ notificationManager.notify(
+ "$bundleName-$bundleVersion".hashCode(),
+ notification
+ )
+ }
+ }
+ Result.success()
+ } catch (e: Exception) {
+ Log.d(LOG_TAG, "Error during work: ${e.message}")
+ Result.failure()
+ }
+ }
+}
\ 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 5096170caa..af8642e06b 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -1,20 +1,16 @@
package app.revanced.manager.patcher.worker
import android.app.Activity
-import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
-import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
-import android.graphics.drawable.Icon
import android.os.Build
import android.os.Parcelable
import android.os.PowerManager
import android.util.Log
import androidx.activity.result.ActivityResult
-import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
@@ -79,32 +75,23 @@ class PatcherWorker(
) {
val packageName get() = input.packageName
}
+ val notificationChannel = NotificationChannel(
+ "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
+ )
override suspend fun getForegroundInfo() =
- ForegroundInfo(
- 1,
- createNotification(),
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0
- )
-
- private fun createNotification(): Notification {
- val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
- val pendingIntent: PendingIntent = PendingIntent.getActivity(
- applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
- )
- val channel = NotificationChannel(
- "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
- )
- val notificationManager =
- ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
- notificationManager!!.createNotificationChannel(channel)
- return Notification.Builder(applicationContext, channel.id)
- .setContentTitle(applicationContext.getText(R.string.app_name))
- .setContentText(applicationContext.getText(R.string.patcher_notification_message))
- .setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
- .setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
- .setContentIntent(pendingIntent).build()
- }
+ workerRepository.createNotification(
+ applicationContext,
+ notificationChannel,
+ applicationContext.getString(R.string.app_name),
+ applicationContext.getString(R.string.patcher_notification_message)
+ ).first.let {
+ ForegroundInfo(
+ 1,
+ it,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0
+ )
+ }
override suspend fun doWork(): Result {
if (runAttemptCount > 0) {
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
index dfc63735b9..cf95d4a4ff 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt
@@ -2,16 +2,26 @@ package app.revanced.manager.ui.component.bundle
import android.webkit.URLUtil
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.Extension
import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material.icons.outlined.Sell
-import androidx.compose.material3.*
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+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.Alignment
@@ -20,10 +30,14 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
+import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.manager.SearchForUpdatesBackgroundInterval
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.TextInputDialog
import app.revanced.manager.ui.component.haptics.HapticSwitch
+import org.koin.compose.koinInject
@Composable
fun BaseBundleDialog(
@@ -36,9 +50,16 @@ fun BaseBundleDialog(
version: String?,
autoUpdate: Boolean,
onAutoUpdateChange: (Boolean) -> Unit,
+ searchUpdate: Boolean,
+ onSearchUpdateChange: (Boolean) -> Unit,
onPatchesClick: () -> Unit,
- extraFields: @Composable ColumnScope.() -> Unit = {}
+ extraFields: @Composable ColumnScope.() -> Unit = {},
+ prefs: PreferencesManager = koinInject()
) {
+ val searchUpdatesScheduledJobInterval by remember {
+ prefs.searchForUpdatesBackgroundInterval.flow
+ }.collectAsStateWithLifecycle(null)
+
ColumnWithScrollbar(
modifier = Modifier
.fillMaxWidth()
@@ -85,7 +106,33 @@ fun BaseBundleDialog(
color = MaterialTheme.colorScheme.outlineVariant
)
- if (remoteUrl != null) {
+ if (
+ remoteUrl != null &&
+ searchUpdatesScheduledJobInterval != null
+ ) {
+ BundleListItem(
+ headlineText = stringResource(R.string.bundle_search_update),
+ supportingText = stringResource(R.string.bundle_search_update_description),
+ //TODO imrpove description if it's off
+ trailingContent = {
+ if (searchUpdatesScheduledJobInterval != SearchForUpdatesBackgroundInterval.NEVER) {
+ HapticSwitch(
+ checked = searchUpdate,
+ onCheckedChange = onSearchUpdateChange
+ )
+ } else {
+ HapticSwitch(
+ checked = false,
+ onCheckedChange = onSearchUpdateChange,
+ enabled = false
+ )
+ }
+ },
+ modifier = Modifier.clickable {
+ onSearchUpdateChange(!searchUpdate)
+ }
+ )
+
BundleListItem(
headlineText = stringResource(R.string.bundle_auto_update),
supportingText = stringResource(R.string.bundle_auto_update_description),
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 400bdd40e8..7ee3be24b4 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,17 +1,26 @@
package app.revanced.manager.ui.component.bundle
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.DeleteOutline
+import androidx.compose.material.icons.outlined.InstallMobile
+import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+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.data.platform.NetworkInfo
@@ -20,9 +29,17 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
+import app.revanced.manager.domain.bundles.RemotePatchBundle
+import app.revanced.manager.ui.component.AppTopBar
+import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.FullscreenDialog
+import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
+import app.revanced.manager.ui.component.settings.Changelog
+import app.revanced.manager.util.relativeTime
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
+import kotlinx.datetime.LocalDateTime
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@@ -31,29 +48,33 @@ fun BundleInformationDialog(
onDismissRequest: () -> Unit,
onDeleteRequest: () -> Unit,
bundle: PatchBundleSource,
+ onSearchUpdate: () -> Unit,
onUpdate: () -> Unit,
+ fromUpdateClick: Boolean
) {
val networkInfo = koinInject()
val hasNetwork = remember { networkInfo.isConnected() }
val composableScope = rememberCoroutineScope()
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
+ var viewChangelogDialog by remember { mutableStateOf(false) }
+ var updateBundleDialog by remember { mutableStateOf(fromUpdateClick) }
val isLocal = bundle is LocalPatchBundle
val state by bundle.state.collectAsStateWithLifecycle()
val props by remember(bundle) {
bundle.propsFlow()
}.collectAsStateWithLifecycle(null)
+ val installedProps by remember(bundle) {
+ bundle.installedPropsFlow()
+ }.collectAsStateWithLifecycle(null)
+ val latestProps by remember(bundle) {
+ bundle.latestPropsFlow()
+ }.collectAsStateWithLifecycle(null)
val patchCount = remember(state) {
state.patchBundleOrNull()?.patches?.size ?: 0
}
-
- if (viewCurrentBundlePatches) {
- BundlePatchesDialog(
- onDismissRequest = {
- viewCurrentBundlePatches = false
- },
- bundle = bundle,
- )
- }
+ val canUpdateState by remember(bundle) {
+ if (bundle is RemotePatchBundle) bundle.canUpdateVersionFlow() else flowOf(false)
+ }.collectAsStateWithLifecycle(null)
FullscreenDialog(
onDismissRequest = onDismissRequest,
@@ -80,14 +101,36 @@ fun BundleInformationDialog(
)
}
}
- if (!isLocal && hasNetwork) {
- IconButton(onClick = onUpdate) {
+
+ if (!isLocal) {
+ IconButton(onClick = onSearchUpdate) {
Icon(
- Icons.Outlined.Update,
+ Icons.Outlined.Refresh,
stringResource(R.string.refresh)
)
}
}
+
+ val canUpdate = bundle is RemotePatchBundle && canUpdateState == true
+
+ IconButton(
+ onClick = {
+ if (canUpdateState == true) {
+ updateBundleDialog = true
+ }
+ },
+ enabled = canUpdateState == true
+ ) {
+ if (canUpdateState == true) {
+ BadgedBox(badge = {
+ Badge(modifier = Modifier.size(6.dp))
+ }) {
+ Icon(Icons.Outlined.Update, stringResource(R.string.update))
+ }
+ } else {
+ Icon(Icons.Outlined.Update, stringResource(R.string.update))
+ }
+ }
}
)
},
@@ -105,6 +148,12 @@ fun BundleInformationDialog(
bundle.asRemoteOrNull?.setAutoUpdate(it)
}
},
+ searchUpdate = props?.searchUpdate ?: false,
+ onSearchUpdateChange = {
+ composableScope.launch {
+ bundle.asRemoteOrNull?.setSearchUpdate(it)
+ }
+ },
onPatchesClick = {
viewCurrentBundlePatches = true
},
@@ -131,6 +180,20 @@ fun BundleInformationDialog(
)
}
+ if (!isLocal) {
+ BundleListItem(
+ headlineText = stringResource(R.string.changelog),
+ supportingText = stringResource(R.string.changelog_description),
+ trailingContent = {
+ Icon(
+ Icons.AutoMirrored.Outlined.ArrowRight,
+ null
+ )
+ },
+ modifier = Modifier.clickable { viewChangelogDialog = true }
+ )
+ }
+
if (state is PatchBundleSource.State.Missing && !isLocal) {
BundleListItem(
headlineText = stringResource(R.string.bundle_error),
@@ -142,4 +205,105 @@ fun BundleInformationDialog(
)
}
}
+
+ if (viewCurrentBundlePatches) {
+ BundlePatchesDialog(
+ onDismissRequest = {
+ viewCurrentBundlePatches = false
+ },
+ bundle = bundle,
+ )
+ }
+
+ if (viewChangelogDialog) {
+ val publishDate = installedProps?.publishDate
+ val changelog = installedProps?.changelog
+ val version = props?.version
+ FullscreenDialog(
+ onDismissRequest = { viewChangelogDialog = false },
+ ) {
+ Scaffold(
+ topBar = {
+ AppTopBar(
+ title = stringResource(R.string.changelog),
+ onBackClick = { viewChangelogDialog = false }
+ )
+ }
+ ) { paddingValues ->
+ ColumnWithScrollbar(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (publishDate != null && version != null && changelog != null) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Changelog(
+ markdown = changelog.replace("`", ""),
+ version = version,
+ publishDate = LocalDateTime.parse(publishDate).relativeTime(LocalContext.current)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (updateBundleDialog) {
+ val publishDate = latestProps?.latestPublishDate
+ val changelog = latestProps?.latestChangelog
+ val version = latestProps?.latestVersion
+
+ FullscreenDialog(
+ onDismissRequest = { updateBundleDialog = false },
+ ) {
+ Scaffold(
+ topBar = {
+ AppTopBar(
+ title = stringResource(R.string.update_available),
+ onBackClick = { updateBundleDialog = false }
+ )
+ }
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize()
+ ) {
+ // Scrollable content
+ ColumnWithScrollbar(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(bottom = 80.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (publishDate != null && version != null && changelog != null) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Changelog(
+ markdown = changelog.replace("`", ""),
+ version = version,
+ publishDate = LocalDateTime
+ .parse(publishDate)
+ .relativeTime(LocalContext.current)
+ )
+ }
+ }
+ }
+ if (hasNetwork)
+ HapticExtendedFloatingActionButton(
+ onClick = {
+ onUpdate()
+ updateBundleDialog = false
+ },
+ icon = { Icon(Icons.Outlined.InstallMobile, null) },
+ text = { Text(stringResource(R.string.download)) },
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp)
+ )
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
index d1644df45f..1fa3ef2a1d 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt
@@ -3,24 +3,32 @@ package app.revanced.manager.ui.component.bundle
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.NewReleases
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -28,8 +36,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
+import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.haptics.HapticCheckbox
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class)
@@ -37,6 +47,7 @@ import kotlinx.coroutines.flow.map
fun BundleItem(
bundle: PatchBundleSource,
onDelete: () -> Unit,
+ onSearchUpdate: () -> Unit,
onUpdate: () -> Unit,
selectable: Boolean,
onSelect: () -> Unit,
@@ -44,12 +55,18 @@ fun BundleItem(
toggleSelection: (Boolean) -> Unit,
) {
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
+ var fromChangelogClick by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) {
bundle.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
+
+ val canUpdateState by remember(bundle) {
+ if (bundle is RemotePatchBundle) bundle.canUpdateVersionFlow() else flowOf(false)
+ }.collectAsStateWithLifecycle(null)
+
val name by bundle.nameState
if (viewBundleDialogPage) {
@@ -58,6 +75,8 @@ fun BundleItem(
onDeleteRequest = { showDeleteConfirmationDialog = true },
bundle = bundle,
onUpdate = onUpdate,
+ onSearchUpdate = onSearchUpdate,
+ fromUpdateClick = fromChangelogClick
)
}
@@ -79,7 +98,10 @@ fun BundleItem(
.height(64.dp)
.fillMaxWidth()
.combinedClickable(
- onClick = { viewBundleDialogPage = true },
+ onClick = {
+ viewBundleDialogPage = true
+ fromChangelogClick = false
+ },
onLongClick = onSelect,
),
leadingContent = if (selectable) {
@@ -98,7 +120,9 @@ fun BundleItem(
}
},
trailingContent = {
- Row {
+ Row (
+ verticalAlignment = Alignment.CenterVertically
+ ) {
val icon = remember(state) {
when (state) {
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error
@@ -116,6 +140,26 @@ fun BundleItem(
)
}
+ if (bundle is RemotePatchBundle && canUpdateState == true) {
+ IconButton(
+ onClick = {
+ fromChangelogClick = true
+ viewBundleDialogPage = true
+ },
+ modifier = Modifier
+ .clip(CircleShape)
+ .fillMaxHeight()
+ .padding(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.NewReleases,
+ contentDescription = "Update available",
+ modifier = Modifier
+ .size(24.dp),
+ )
+ }
+ }
+
version?.let { Text(text = it) }
}
},
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 37d9ed1ad0..ed38c15489 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
@@ -30,7 +30,7 @@ import app.revanced.manager.util.transparentListItemColors
@Composable
fun ImportPatchBundleDialog(
onDismiss: () -> Unit,
- onRemoteSubmit: (String, Boolean) -> Unit,
+ onRemoteSubmit: (String, Boolean, Boolean) -> Unit,
onLocalSubmit: (Uri) -> Unit
) {
var currentStep by rememberSaveable { mutableIntStateOf(0) }
@@ -38,6 +38,7 @@ fun ImportPatchBundleDialog(
var patchBundle by rememberSaveable { mutableStateOf(null) }
var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(false) }
+ var searchUpdate by rememberSaveable { mutableStateOf(false) }
val patchActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
@@ -60,9 +61,11 @@ fun ImportPatchBundleDialog(
patchBundle,
remoteUrl,
autoUpdate,
+ searchUpdate,
{ launchPatchActivity() },
{ remoteUrl = it },
- { autoUpdate = it }
+ { autoUpdate = it },
+ { searchUpdate = it }
)
}
)
@@ -89,7 +92,7 @@ fun ImportPatchBundleDialog(
onClick = {
when (bundleType) {
BundleType.Local -> patchBundle?.let(onLocalSubmit)
- BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
+ BundleType.Remote -> onRemoteSubmit(remoteUrl, searchUpdate, autoUpdate)
}
}
) {
@@ -173,9 +176,11 @@ fun ImportBundleStep(
patchBundle: Uri?,
remoteUrl: String,
autoUpdate: Boolean,
+ searchUpdate: Boolean,
launchPatchActivity: () -> Unit,
onRemoteUrlChange: (String) -> Unit,
- onAutoUpdateChange: (Boolean) -> Unit
+ onAutoUpdateChange: (Boolean) -> Unit,
+ onSearchUpdateChange: (Boolean) -> Unit,
) {
Column {
when (bundleType) {
@@ -230,6 +235,24 @@ fun ImportBundleStep(
},
colors = transparentListItemColors
)
+ ListItem(
+ modifier = Modifier.clickable(
+ role = Role.Checkbox,
+ onClick = { onSearchUpdateChange(!searchUpdate) }
+ ),
+ headlineContent = { Text(stringResource(R.string.auto_update)) },
+ leadingContent = {
+ CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
+ HapticCheckbox(
+ checked = searchUpdate,
+ onCheckedChange = {
+ onSearchUpdateChange(!searchUpdate)
+ }
+ )
+ }
+ },
+ colors = transparentListItemColors
+ )
}
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
index 0cd3b73935..298e589f08 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt
@@ -79,7 +79,7 @@ data class BundleInfo(
targetList.add(it)
}
- BundleInfo(source.getName(), source.currentVersion(), source.uid, compatible, incompatible, universal)
+ BundleInfo(source.getName(), source.getProps().version, source.uid, compatible, incompatible, universal)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
index 8704a06428..7dafee41bc 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt
@@ -9,12 +9,14 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.revanced.manager.domain.bundles.PatchBundleSource
+import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.bundle.BundleItem
@Composable
fun BundleListScreen(
onDelete: (PatchBundleSource) -> Unit,
+ onSearchUpdate: (PatchBundleSource) -> Unit,
onUpdate: (PatchBundleSource) -> Unit,
sources: List,
selectedSources: SnapshotStateList,
@@ -40,6 +42,9 @@ fun BundleListScreen(
onDelete = {
onDelete(source)
},
+ onSearchUpdate = {
+ onSearchUpdate(source)
+ },
onUpdate = {
onUpdate(source)
},
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 0d14cf9ec3..9ec7b9e283 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
@@ -58,7 +58,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt
-import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog
@@ -69,6 +68,7 @@ import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.viewmodel.DashboardViewModel
+import app.revanced.manager.util.permission.PermissionRequestHandler
import app.revanced.manager.util.RequestInstallAppsContract
import app.revanced.manager.util.toast
import kotlinx.coroutines.launch
@@ -120,9 +120,9 @@ fun DashboardScreen(
showAddBundleDialog = false
vm.createLocalSource(patches)
},
- onRemoteSubmit = { url, autoUpdate ->
+ onRemoteSubmit = { url, searchUpdate, autoUpdate ->
showAddBundleDialog = false
- vm.createRemoteSource(url, autoUpdate)
+ vm.createRemoteSource(url, searchUpdate, autoUpdate)
}
)
}
@@ -142,19 +142,19 @@ fun DashboardScreen(
}
var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) }
- val installAppsPermissionLauncher =
- rememberLauncherForActivityResult(RequestInstallAppsContract) { granted ->
- showAndroid11Dialog = false
- if (granted) onAppSelectorClick()
- }
- if (showAndroid11Dialog) Android11Dialog(
- onDismissRequest = {
- showAndroid11Dialog = false
- },
- onContinue = {
- installAppsPermissionLauncher.launch(androidContext.packageName)
- }
- )
+
+ if(showAndroid11Dialog)
+ PermissionRequestHandler(
+ contract = RequestInstallAppsContract,
+ input = androidContext.packageName,
+ title = stringResource(R.string.android_11_bug_dialog_title),
+ description = stringResource(R.string.android_11_bug_dialog_description),
+ icon = Icons.Outlined.BugReport,
+ onDismissRequest = { showAndroid11Dialog = false },
+ onResult = { granted ->
+ if (granted) onAppSelectorClick()
+ }
+ )
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
if (showDeleteConfirmationDialog) {
@@ -355,6 +355,9 @@ fun DashboardScreen(
onDelete = {
vm.delete(it)
},
+ onSearchUpdate = {
+ vm.searchUpdate(it)
+ },
onUpdate = {
vm.update(it)
},
@@ -386,25 +389,4 @@ fun Notifications(
}
}
}
-}
-
-@Composable
-fun Android11Dialog(onDismissRequest: () -> Unit, onContinue: () -> Unit) {
- AlertDialogExtended(
- onDismissRequest = onDismissRequest,
- confirmButton = {
- TextButton(onClick = onContinue) {
- Text(stringResource(R.string.continue_))
- }
- },
- title = {
- Text(stringResource(R.string.android_11_bug_dialog_title))
- },
- icon = {
- Icon(Icons.Outlined.BugReport, null)
- },
- text = {
- Text(stringResource(R.string.android_11_bug_dialog_description))
- }
- )
}
\ No newline at end of file
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 8218713ea0..6c732d7bdb 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
@@ -230,8 +230,8 @@ fun PatchesSelectorScreen(
// Show selection warning if enabled
viewModel.selectionWarningEnabled -> showSelectionWarning = true
- // Show universal warning if enabled
- viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
+ // Show universal warning if universal patch is selected and the toggle is off
+ patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
// Toggle the patch otherwise
else -> viewModel.togglePatch(uid, patch)
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt
index 8fe11934df..3cfffb7beb 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt
@@ -1,27 +1,50 @@
package app.revanced.manager.ui.screen.settings.update
+import android.Manifest
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Notifications
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
+import app.revanced.manager.domain.manager.SearchForUpdatesBackgroundInterval
+import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
+import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
+import app.revanced.manager.util.enabled
+import app.revanced.manager.util.permission.PermissionRequestHandler
+import app.revanced.manager.util.permission.hasNotificationPermission
import app.revanced.manager.util.toast
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -34,6 +57,14 @@ fun UpdatesSettingsScreen(
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
+ var showBackgroundUpdateDialog by rememberSaveable { mutableStateOf(false) }
+
+ if (showBackgroundUpdateDialog) {
+ BackgroundBundleUpdateTimeDialog(
+ onDismiss = { showBackgroundUpdateDialog = false },
+ onConfirm = { vm.updateBackgroundBundleUpdateTime(it) }
+ )
+ }
Scaffold(
topBar = {
@@ -89,6 +120,82 @@ fun UpdatesSettingsScreen(
headline = R.string.show_manager_update_dialog_on_launch,
description = R.string.update_checking_manager_description
)
+ SettingsListItem(
+ headlineContent = stringResource(R.string.background_bundle_update),
+ supportingContent = stringResource(R.string.background_bundle_update_description),
+ modifier = Modifier.clickable(
+ onClick = { showBackgroundUpdateDialog = true }
+ )
+ )
}
}
-}
\ No newline at end of file
+}
+
+@Composable
+private fun BackgroundBundleUpdateTimeDialog(
+ onDismiss: () -> Unit,
+ onConfirm: (SearchForUpdatesBackgroundInterval) -> Unit,
+ prefs: PreferencesManager = koinInject()
+) {
+ var context = LocalContext.current
+ var selected by rememberSaveable { mutableStateOf(prefs.searchForUpdatesBackgroundInterval.getBlocking()) }
+
+ var askNotificationPermission by rememberSaveable { mutableStateOf(false) }
+
+ fun onApply() {
+ onConfirm(selected)
+ onDismiss()
+ }
+
+ if (askNotificationPermission) {
+ PermissionRequestHandler(
+ contract = ActivityResultContracts.RequestPermission(),
+ input = Manifest.permission.POST_NOTIFICATIONS,
+ title = stringResource(R.string.background_bundle_ask_notification),
+ description = stringResource(R.string.background_bundle_ask_notification_description),
+ icon = Icons.Outlined.Notifications,
+ onDismissRequest = { askNotificationPermission = false },
+ onResult = { granted ->
+ askNotificationPermission = false
+ onApply()
+ }
+ )
+ }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(R.string.background_radio_menu_title)) },
+ text = {
+ Column {
+ SearchForUpdatesBackgroundInterval.entries.forEach {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { selected = it },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ HapticRadioButton(
+ selected = selected == it,
+ onClick = { selected = it })
+ Text(stringResource(it.displayName))
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ Log.i("testtt", "selected ${selected.toString()}")
+ Log.i("testtt", "hasNotificationPermission ${context.hasNotificationPermission().toString()}")
+ if (selected != SearchForUpdatesBackgroundInterval.NEVER &&
+ !context.hasNotificationPermission()
+ ) askNotificationPermission = true
+ else
+ onApply()
+ }
+ ) {
+ Text(stringResource(R.string.apply))
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
index 0783e94d63..449bb74011 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt
@@ -5,6 +5,7 @@ import android.content.ContentResolver
import android.net.Uri
import android.os.Build
import android.os.PowerManager
+import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -70,10 +71,6 @@ class DashboardViewModel(
downloaderPluginRepository.acknowledgeAllNewPlugins()
}
- fun dismissUpdateDialog() {
- updatedManagerVersion = null
- }
-
private suspend fun checkForManagerUpdates() {
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
@@ -124,8 +121,8 @@ class DashboardViewModel(
}
}
- fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) =
- viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) }
+ fun createRemoteSource(apiUrl: String, searchUpdate: Boolean, autoUpdate: Boolean) =
+ viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, searchUpdate, autoUpdate) }
fun delete(bundle: PatchBundleSource) =
viewModelScope.launch { patchBundleRepository.remove(bundle) }
@@ -144,4 +141,19 @@ class DashboardViewModel(
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
}
}
+
+ fun searchUpdate(bundle: PatchBundleSource) = viewModelScope.launch {
+ if (bundle !is RemotePatchBundle) return@launch
+
+ uiSafe(
+ app,
+ R.string.source_download_fail,//TODO
+ RemotePatchBundle.updateFailMsg
+ ) {
+ if (bundle.canUpdateVersion())
+ app.toast(app.getString(R.string.bundle_update_success, bundle.getName()))
+ else
+ app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt
index 51764e5067..0de033de67 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt
@@ -2,22 +2,34 @@ 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.data.platform.NetworkInfo
+import app.revanced.manager.domain.manager.SearchForUpdatesBackgroundInterval
import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
+import kotlinx.coroutines.launch
class UpdatesSettingsViewModel(
- prefs: PreferencesManager,
+ val prefs: PreferencesManager,
private val app: Application,
private val reVancedAPI: ReVancedAPI,
private val network: NetworkInfo,
+ private val workerRepository: WorkerRepository
) : ViewModel() {
val managerAutoUpdates = prefs.managerAutoUpdates
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
+ fun updateBackgroundBundleUpdateTime(searchForUpdatesBackgroundInterval: SearchForUpdatesBackgroundInterval) {
+ viewModelScope.launch {
+ prefs.searchForUpdatesBackgroundInterval.update(searchForUpdatesBackgroundInterval)
+ workerRepository.scheduleBundleUpdateNotificationWork(searchForUpdatesBackgroundInterval)
+ }
+ }
+
val isConnected: Boolean
get() = network.isConnected()
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 2c509aa51c..3772e12a30 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -1,8 +1,12 @@
package app.revanced.manager.util
+import android.Manifest
+import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread
@@ -25,6 +29,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
+import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -33,6 +38,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R
+import app.revanced.manager.util.permission.PermissionHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/app/revanced/manager/util/permission/PermissionContextExtension.kt b/app/src/main/java/app/revanced/manager/util/permission/PermissionContextExtension.kt
new file mode 100644
index 0000000000..62e13ae5fb
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/util/permission/PermissionContextExtension.kt
@@ -0,0 +1,22 @@
+package app.revanced.manager.util.permission
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.os.Build
+import app.revanced.manager.util.permission.PermissionHelper.PermissionState
+
+fun Context.hasNotificationPermission(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ PermissionHelper(this).isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS)
+ else
+ true
+}
+
+fun Context.isNotificationPermissionDenied(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ PermissionHelper(this).getPermissionState(this as Activity, Manifest.permission.POST_NOTIFICATIONS) ==
+ PermissionState.DeniedPermanently
+ else
+ false
+}
diff --git a/app/src/main/java/app/revanced/manager/util/permission/PermissionHelper.kt b/app/src/main/java/app/revanced/manager/util/permission/PermissionHelper.kt
new file mode 100644
index 0000000000..20aafed7c7
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/util/permission/PermissionHelper.kt
@@ -0,0 +1,50 @@
+package app.revanced.manager.util.permission
+
+import android.app.Activity
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.edit
+
+class PermissionHelper(private val context: Context) {
+ private val prefs by lazy {
+ context.getSharedPreferences("permissions_pref", Context.MODE_PRIVATE)
+ }
+
+ fun isPermissionGranted(permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+ }
+
+ fun isFirstTimeAsking(permission: String): Boolean {
+ val firstTime = prefs.getBoolean(permission, true)
+ if (firstTime) {
+ prefs.edit { putBoolean(permission, false) }
+ }
+ return firstTime
+ }
+
+ fun shouldShowRationale(activity: Activity, permission: String): Boolean {
+ return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
+ }
+
+ fun resetPermission(permission: String) {
+ prefs.edit { putBoolean(permission, true) }
+ }
+
+ fun getPermissionState(activity: Activity, permission: String): PermissionState {
+ return when {
+ isPermissionGranted(permission) -> PermissionState.Granted
+ shouldShowRationale(activity, permission) -> PermissionState.DeniedWithRationale
+ isFirstTimeAsking(permission) -> PermissionState.FirstTime
+ else -> PermissionState.DeniedPermanently
+ }
+ }
+
+ enum class PermissionState {
+ Granted,
+ FirstTime,
+ DeniedWithRationale,
+ DeniedPermanently
+ }
+}
diff --git a/app/src/main/java/app/revanced/manager/util/permission/RequestPermissionComponent.kt b/app/src/main/java/app/revanced/manager/util/permission/RequestPermissionComponent.kt
new file mode 100644
index 0000000000..0ddf2f08ff
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/util/permission/RequestPermissionComponent.kt
@@ -0,0 +1,89 @@
+package app.revanced.manager.util.permission
+
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.core.app.ActivityCompat
+import app.revanced.manager.R
+import app.revanced.manager.ui.component.AlertDialogExtended
+
+@Composable
+fun PermissionRequestHandler(
+ contract: ActivityResultContract,
+ input: String,
+ onDismissRequest: () -> Unit,
+ onResult: (Boolean) -> Unit,
+ onContinue: () -> Unit = {},
+ title: String,
+ description: String,
+ icon: ImageVector
+) {
+ val context = LocalContext.current
+ val activity = context as? Activity ?: return
+ val permissionHelper = PermissionHelper(context)
+
+ val launcher = rememberLauncherForActivityResult(contract) { result ->
+ Log.d("testtt", "rememberLauncherForActivityResult $result")
+ onResult(result)
+ }
+
+ when (permissionHelper.getPermissionState(activity, input)) {
+ PermissionHelper.PermissionState.Granted -> {
+ onResult(true)
+ }
+ PermissionHelper.PermissionState.FirstTime,
+ PermissionHelper.PermissionState.DeniedWithRationale -> {
+ Log.d("testtt", "DeniedWithRationale or FirstTime")
+ PermissionDialog(
+ title = title,
+ description = description,
+ icon = icon,
+ onDismissRequest = onDismissRequest,
+ onContinue = {
+ onContinue()
+ launcher.launch(input)
+ }
+ )
+ }
+ PermissionHelper.PermissionState.DeniedPermanently -> {
+ Log.d("testtt", "DeniedPermanently")
+ //TODO Handle the "go to settings" case if needed
+ onResult(false)
+ }
+ }
+}
+
+@Composable
+private fun PermissionDialog(
+ title: String,
+ description: String,
+ icon: ImageVector,
+ onDismissRequest: () -> Unit,
+ onContinue: () -> Unit
+) {
+ AlertDialogExtended(
+ onDismissRequest = onDismissRequest,
+ title = { Text(title) },
+ text = { Text(description) },
+ icon = { Icon(icon, null) },
+ confirmButton = {
+ TextButton(onClick = onContinue) {
+ Text(stringResource(R.string.ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(R.string.cancel))
+ }
+ }
+ )
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a1ab4a8fed..82785210cd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,6 +34,8 @@
Bundle has not been downloaded. Click here to download it
Default
Unnamed
+ Bundle update available
+ A new version %s for the bundle %s was just released
Android 11 bug
The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.
@@ -181,6 +183,15 @@
This is faster and allows Patcher to use more memory.
Patcher process memory limit
The max amount of memory that the Patcher process can use (in megabytes)
+ Enable bundles auto update in background
+ Automatically checks for bundles updates and update them in the background
+ Check for bundles updates
+ Stay updated
+ ReVanced will send you a notification when a bundle was updated. Press "OK" if you wish to be notified.
+ Never
+ Every 15 minutes
+ Hourly
+ Daily
Export debug logs
Failed to read logs (exit code %d)
Failed to export logs
@@ -335,6 +346,8 @@
Source URL
Successfully updated %s
No update available for %s
+ Search for update
+ Search for updates in background and send a notification when an update is available
Auto update
Automatically update this bundle when ReVanced starts
View patches
diff --git a/package.json b/package.json
index 246f579a8d..7b7287ff4e 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,6 @@
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
- "@semantic-release/git": "^10.0.1",
+ "@semantic-release/git": "^10.0.1"
}
}