diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6bba16a5..cb80b2ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,20 +5,20 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) alias(libs.plugins.devtools) alias(libs.plugins.about.libraries) - alias(libs.plugins.compose.compiler) } android { namespace = "app.revanced.manager" - compileSdk = 34 - buildToolsVersion = "34.0.0" + compileSdk = 35 + buildToolsVersion = "35.0.0" defaultConfig { applicationId = "app.revanced.manager" minSdk = 26 - targetSdk = 34 + targetSdk = 35 versionCode = 1 versionName = "0.0.1" vectorDrawables.useSupportLibrary = true @@ -195,6 +195,10 @@ dependencies { // Scrollbars implementation(libs.scrollbars) + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) + // Reorderable lists implementation(libs.reorderable) 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 c339f1c4..360827e9 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": "393045599eb516fc7ef99f142485e9a2", + "identityHash": "167d15a56dd60ffcebf1f93aa6948a93", "entities": [ { "tableName": "patch_bundles", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))", "fields": [ { "fieldPath": "uid", @@ -20,6 +20,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, { "fieldPath": "source", "columnName": "source", @@ -31,18 +37,6 @@ "columnName": "auto_update", "affinity": "INTEGER", "notNull": true - }, - { - "fieldPath": "versionInfo.patches", - "columnName": "version", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "versionInfo.integrations", - "columnName": "integrations_version", - "affinity": "TEXT", - "notNull": false } ], "primaryKey": { @@ -423,7 +417,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, '393045599eb516fc7ef99f142485e9a2')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '167d15a56dd60ffcebf1f93aa6948a93')" ] } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt index ad7033dd..290a226d 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -9,7 +9,7 @@ import kotlinx.parcelize.Parcelize enum class InstallType(val stringResource: Int) { DEFAULT(R.string.default_install), - ROOT(R.string.root_install) + MOUNT(R.string.mount_install) } @Parcelize diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt index 77de9b03..d9955a70 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -8,11 +8,11 @@ interface PatchBundleDao { @Query("SELECT * FROM patch_bundles") suspend fun all(): List - @Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid") + @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid") fun getPropsById(uid: Int): Flow - @Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid") - suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) + @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid") + suspend fun updateVersion(uid: Int, patches: String?) @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") suspend fun setAutoUpdate(uid: Int, value: Boolean) @@ -26,7 +26,7 @@ interface PatchBundleDao { @Transaction suspend fun reset() { purgeCustomBundles() - updateVersion(0, null, null) // Reset the main source + updateVersion(0, null) // Reset the main source } @Query("DELETE FROM patch_bundles WHERE uid = :uid") diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index d120abf5..8ba5f64a 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -29,21 +29,16 @@ sealed class Source { } } -data class VersionInfo( - @ColumnInfo(name = "version") val patches: String? = null, - @ColumnInfo(name = "integrations_version") val integrations: String? = null, -) - @Entity(tableName = "patch_bundles") data class PatchBundleEntity( @PrimaryKey val uid: Int, @ColumnInfo(name = "name") val name: String, - @Embedded val versionInfo: VersionInfo, + @ColumnInfo(name = "version") val version: String? = null, @ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean ) data class BundleProperties( - @Embedded val versionInfo: VersionInfo, + @ColumnInfo(name = "version") val version: String? = null, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt index b59dbd16..44bc3d40 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt @@ -20,6 +20,8 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf @Entity( tableName = "options", @@ -46,8 +48,8 @@ data class Option( val errorMessage = "Cannot deserialize value as ${option.type}" try { - if (option.type.endsWith("Array")) { - val elementType = option.type.removeSuffix("Array") + if (option.type.classifier == List::class) { + val elementType = option.type.arguments.first().type!! return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) } } @@ -67,12 +69,17 @@ data class Option( allowSpecialFloatingPointValues = true } - private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) { - "Boolean" -> value.boolean - "Int" -> value.int - "Long" -> value.long - "Float" -> value.float - "String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") } + private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) { + typeOf() -> value.boolean + typeOf() -> value.int + typeOf() -> value.long + typeOf() -> value.float + typeOf() -> value.content.also { + if (!value.isString) throw SerializationException( + "Expected value to be a string: $value" + ) + } + else -> throw SerializationException("Unknown type: $type") } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt index 1d8b41f3..bcbc59cf 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -4,29 +4,18 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream -import java.nio.file.Files -import java.nio.file.StandardCopyOption class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) { - suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) { + suspend fun replace(patches: InputStream) { withContext(Dispatchers.IO) { - patches?.let { inputStream -> - patchBundleOutputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - integrations?.let { - Files.copy( - it, - this@LocalPatchBundle.integrationsFile.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) + patchBundleOutputStream().use { outputStream -> + patches.copyTo(outputStream) } } reload()?.also { - saveVersion(it.readManifestAttribute("Version"), null) + saveVersion(it.readManifestAttribute("Version")) } } } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index 1ded6d43..308e2a56 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -28,7 +28,6 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil protected val configRepository: PatchBundlePersistenceRepository by inject() private val app: Application by inject() protected val patchesFile = directory.resolve("patches.jar") - protected val integrationsFile = directory.resolve("integrations.apk") private val _state = MutableStateFlow(load()) val state = _state.asStateFlow() @@ -58,7 +57,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil if (!hasInstalled()) return State.Missing return try { - State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) + State.Loaded(PatchBundle(patchesFile)) } catch (t: Throwable) { Log.e(tag, "Failed to load patch bundle with UID $uid", t) State.Failed(t) @@ -85,9 +84,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) suspend fun getProps() = propsFlow().first()!! - suspend fun currentVersion() = getProps().versionInfo - protected suspend fun saveVersion(patches: String?, integrations: String?) = - configRepository.updateVersion(uid, patches, integrations) + suspend fun currentVersion() = getProps().version + protected suspend fun saveVersion(version: String?) = + configRepository.updateVersion(uid, version) suspend fun setName(name: String) { configRepository.setName(uid, name) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 8bbc230d..e3214db9 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -1,20 +1,12 @@ package app.revanced.manager.domain.bundles import androidx.compose.runtime.Stable -import app.revanced.manager.data.room.bundles.VersionInfo import app.revanced.manager.network.api.ReVancedAPI -import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType -import app.revanced.manager.network.dto.BundleAsset -import app.revanced.manager.network.dto.BundleInfo +import app.revanced.manager.network.dto.PatchBundleInfo import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.utils.getOrThrow -import app.revanced.manager.util.APK_MIMETYPE -import app.revanced.manager.util.JAR_MIMETYPE import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.inject import java.io.File @@ -24,27 +16,17 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo PatchBundleSource(name, id, directory) { protected val http: HttpService by inject() - protected abstract suspend fun getLatestInfo(): BundleInfo + protected abstract suspend fun getLatestInfo(): PatchBundleInfo - private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) { - val (patches, integrations) = info - coroutineScope { - launch { - patchBundleOutputStream().use { - http.streamTo(it) { - url(patches.url) - } - } - } - - launch { - http.download(integrationsFile) { - url(integrations.url) - } + private suspend fun download(info: PatchBundleInfo) = withContext(Dispatchers.IO) { + val (version, url) = info + patchBundleOutputStream().use { + http.streamTo(it) { + url(url) } } - saveVersion(patches.version, integrations.version) + saveVersion(version) reload() } @@ -54,20 +36,15 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo suspend fun update(): Boolean = withContext(Dispatchers.IO) { val info = getLatestInfo() - if (hasInstalled() && VersionInfo( - info.patches.version, - info.integrations.version - ) == currentVersion() - ) { + if (hasInstalled() && info.version == currentVersion()) return@withContext false - } download(info) true } suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { - arrayOf(patchesFile, integrationsFile).forEach(File::delete) + patchesFile.delete() reload() } @@ -81,7 +58,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) : RemotePatchBundle(name, id, directory, endpoint) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { - http.request { + http.request { url(endpoint) }.getOrThrow() } @@ -91,22 +68,10 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) : RemotePatchBundle(name, id, directory, endpoint) { private val api: ReVancedAPI by inject() - override suspend fun getLatestInfo() = coroutineScope { - fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) { - api - .getLatestRelease(repo) - .getOrThrow() - .let { - BundleAsset(it.version, it.findAssetByType(mime).downloadUrl) - } + override suspend fun getLatestInfo() = api + .getLatestRelease("revanced-patches") + .getOrThrow() + .let { + PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl) } - - val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE) - val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE) - - BundleInfo( - patches.await(), - integrations.await() - ) - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 9ca6cd9b..293484ca 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -43,7 +43,7 @@ class RootInstaller( } } - return withTimeoutOrNull(Duration.ofSeconds(120L)) { + return withTimeoutOrNull(Duration.ofSeconds(20L)) { remoteFS.await() } ?: throw RootServiceException() } @@ -58,6 +58,10 @@ class RootInstaller( fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> + File(path, "su").canExecute() + } ?: false + suspend fun isAppInstalled(packageName: String) = awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() @@ -105,7 +109,12 @@ class RootInstaller( stockAPK?.let { stockApp -> pm.getPackageInfo(packageName)?.let { packageInfo -> - if (packageInfo.versionName <= version) + // TODO: get user id programmatically + if (pm.getVersionCode(packageInfo) <= pm.getVersionCode( + pm.getPackageInfo(patchedAPK) + ?: error("Failed to get package info for patched app") + ) + ) execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app") } diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt index c1c4700d..4f9dc5a3 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt @@ -12,6 +12,8 @@ import java.io.InputStream import java.io.OutputStream import java.nio.file.Files import java.security.UnrecoverableKeyException +import java.util.Date +import kotlin.time.Duration.Companion.days class KeystoreManager(app: Application, private val prefs: PreferencesManager) { companion object Constants { @@ -19,6 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { * Default alias and password for the keystore. */ const val DEFAULT = "ReVanced" + private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24) } private val keystorePath = @@ -29,23 +32,26 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { prefs.keystorePass.value = pass } - private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions( + private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails( keyStore = path, keyStorePassword = null, alias = prefs.keystoreCommonName.get(), - signer = prefs.keystoreCommonName.get(), password = prefs.keystorePass.get() ) suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) { - ApkUtils.sign(input, output, signingOptions()) + ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails()) } suspend fun regenerate() = withContext(Dispatchers.Default) { + val keyCertPair = ApkSigner.newPrivateKeyCertificatePair( + prefs.keystoreCommonName.get(), + eightYearsFromNow + ) val ks = ApkSigner.newKeyStore( setOf( ApkSigner.KeyStoreEntry( - DEFAULT, DEFAULT + DEFAULT, DEFAULT, keyCertPair ) ) ) @@ -64,7 +70,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { try { val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null) - ApkSigner.readKeyCertificatePair(ks, cn, pass) + ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass) } catch (_: UnrecoverableKeyException) { return false } catch (_: IllegalArgumentException) { diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index 5b8bed3b..dbf2f100 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -12,7 +12,6 @@ class PreferencesManager( val api = stringPreference("api_url", "https://api.revanced.app") - val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true) val useProcessRuntime = booleanPreference("use_process_runtime", false) val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index 191ef67d..785c5f12 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -102,7 +102,7 @@ class DownloadedAppRepository(private val app: Application, db: AppDatabase, pri dao.insert( DownloadedApp( packageName = pkgInfo.packageName, - version = pkgInfo.versionName, + version = pkgInfo.versionName!!, directory = relativePath, ) ) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt index 8579ab69..791a09ac 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt @@ -87,11 +87,11 @@ class DownloaderPluginRepository( return try { val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!! - val className = packageInfo.applicationInfo.metaData.getString(METADATA_PLUGIN_CLASS) + val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS) ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS") val classLoader = - PathClassLoader(packageInfo.applicationInfo.sourceDir, app.classLoader) + PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader) val pluginContext = app.createPackageContext(packageName, 0) val downloader = classLoader @@ -109,7 +109,7 @@ class DownloaderPluginRepository( LoadedDownloaderPlugin( packageName, with(pm) { packageInfo.label() }, - packageInfo.versionName, + packageInfo.versionName!!, downloader.get, downloader.download, classLoader diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt index 4b853ecf..5711d997 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt @@ -4,7 +4,6 @@ import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.Source -import app.revanced.manager.data.room.bundles.VersionInfo import kotlinx.coroutines.flow.distinctUntilChanged class PatchBundlePersistenceRepository(db: AppDatabase) { @@ -26,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) { PatchBundleEntity( uid = generateUid(), name = name, - versionInfo = VersionInfo(), + version = null, source = source, autoUpdate = autoUpdate ).also { @@ -35,8 +34,8 @@ class PatchBundlePersistenceRepository(db: AppDatabase) { suspend fun delete(uid: Int) = dao.remove(uid) - suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) = - dao.updateVersion(uid, patches, integrations) + suspend fun updateVersion(uid: Int, version: String?) = + dao.updateVersion(uid, version) suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) @@ -48,7 +47,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) { val defaultSource = PatchBundleEntity( uid = 0, name = "", - versionInfo = VersionInfo(), + version = null, source = Source.API, autoUpdate = false ) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index f40d6c0b..79bb5cea 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -3,7 +3,7 @@ package app.revanced.manager.domain.repository import android.app.Application import android.content.Context import android.util.Log -import app.revanced.library.PatchUtils +import app.revanced.library.mostCommonCompatibleVersions import app.revanced.manager.R import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.room.bundles.PatchBundleEntity @@ -55,7 +55,7 @@ class PatchBundleRepository( val allPatches = it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() - PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true) + allPatches.mostCommonCompatibleVersions(countUnusedPatches = true) .mapValues { (_, versions) -> if (versions.keys.size < 2) return@mapValues versions.keys.firstOrNull() @@ -137,11 +137,11 @@ class PatchBundleRepository( private fun addBundle(patchBundle: PatchBundleSource) = _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } - suspend fun createLocal(patches: InputStream, integrations: InputStream?) = withContext(Dispatchers.Default) { + suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) { val uid = persistenceRepo.create("", SourceInfo.Local).uid val bundle = LocalPatchBundle("", uid, directoryOf(uid)) - bundle.replace(patches, integrations) + bundle.replace(patches) addBundle(bundle) } diff --git a/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt deleted file mode 100644 index e2b56a87..00000000 --- a/app/src/main/java/app/revanced/manager/network/dto/BundleInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.revanced.manager.network.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset) - -@Serializable -data class BundleAsset(val version: String, val url: String) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt new file mode 100644 index 00000000..02d89919 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/dto/PatchBundleInfo.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.network.dto + +import kotlinx.serialization.Serializable + +@Serializable +// TODO: replace this +data class PatchBundleInfo(val version: String, val url: String) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index 4393794d..d1368f24 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -22,7 +22,6 @@ class Session( cacheDir: String, frameworkDir: String, aaptPath: String, - multithreadingDexFileWriter: Boolean, private val androidContext: Context, private val logger: Logger, private val input: File, @@ -38,8 +37,7 @@ class Session( apkFile = input, temporaryFilesPath = tempDir, frameworkFileDirectory = frameworkDir, - aaptBinaryPath = aaptPath, - multithreadingDexFileWriter = multithreadingDexFileWriter, + aaptBinaryPath = aaptPath ) ) @@ -51,7 +49,7 @@ class Session( state = State.RUNNING ) - this.apply(true).collect { (patch, exception) -> + this().collect { (patch, exception) -> if (patch !in selectedPatches) return@collect if (exception != null) { @@ -89,7 +87,7 @@ class Session( ) } - suspend fun run(output: File, selectedPatches: PatchList, integrations: List) { + suspend fun run(output: File, selectedPatches: PatchList) { updateProgress(state = State.COMPLETED) // Unpacking java.util.logging.Logger.getLogger("").apply { @@ -103,8 +101,7 @@ class Session( with(patcher) { logger.info("Merging integrations") - acceptIntegrations(integrations.toSet()) - acceptPatches(selectedPatches.toSet()) + this += selectedPatches.toSet() logger.info("Applying patches...") applyPatchesVerbose(selectedPatches.sortedBy { it.name }) diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index 8dbcf153..2b93a829 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -2,17 +2,17 @@ package app.revanced.manager.patcher.patch import android.util.Log import app.revanced.manager.util.tag -import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchLoader import java.io.File import java.io.IOException import java.util.jar.JarFile -class PatchBundle(val patchesJar: File, val integrations: File?) { +class PatchBundle(val patchesJar: File) { private val loader = object : Iterable> { private fun load(): Iterable> { patchesJar.setReadOnly() - return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null) + return PatchLoader.Dex(setOf(patchesJar)) } override fun iterator(): Iterator> = load().iterator() @@ -41,12 +41,12 @@ class PatchBundle(val patchesJar: File, val integrations: File?) { /** * Load all patches compatible with the specified package. */ - fun patchClasses(packageName: String) = loader.filter { patch -> + fun patches(packageName: String) = loader.filter { patch -> val compatiblePackages = patch.compatiblePackages ?: // The patch has no compatibility constraints, which means it is universal. return@filter true - if (!compatiblePackages.any { it.name == packageName }) { + if (!compatiblePackages.any { (name, _) -> name == packageName }) { // Patch is not compatible with this package. return@filter false } diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt index 8f4b2ee6..2babc7f4 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -1,14 +1,14 @@ package app.revanced.manager.patcher.patch import androidx.compose.runtime.Immutable -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.options.PatchOption +import app.revanced.patcher.patch.Option as PatchOption +import app.revanced.patcher.patch.resourcePatch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet +import kotlin.reflect.KType data class PatchInfo( val name: String, @@ -21,7 +21,12 @@ data class PatchInfo( patch.name.orEmpty(), patch.description, patch.use, - patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(), + patch.compatiblePackages?.map { (pkgName, versions) -> + CompatiblePackage( + pkgName, + versions?.toImmutableSet() + ) + }?.toImmutableList(), patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList() ) @@ -44,37 +49,19 @@ data class PatchInfo( * The resulting patch cannot be executed. * This is necessary because some functions in ReVanced Library only accept full [Patch] objects. */ - fun toPatcherPatch(): Patch<*> = object : ResourcePatch( - name = name, - description = description, - compatiblePackages = compatiblePackages - ?.map(app.revanced.manager.patcher.patch.CompatiblePackage::toPatcherCompatiblePackage) - ?.toSet(), - use = include, - ) { - override fun execute(context: ResourceContext) = - throw Exception("Metadata patches cannot be executed") - } + fun toPatcherPatch(): Patch<*> = + resourcePatch(name = name, description = description, use = include) { + compatiblePackages?.let { pkgs -> + compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray()) + } + } } @Immutable data class CompatiblePackage( val packageName: String, val versions: ImmutableSet? -) { - constructor(pkg: Patch.CompatiblePackage) : this( - pkg.name, - pkg.versions?.toImmutableSet() - ) - - /** - * Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher. - */ - fun toPatcherCompatiblePackage() = Patch.CompatiblePackage( - name = packageName, - versions = versions, - ) -} +) @Immutable data class Option( @@ -82,7 +69,7 @@ data class Option( val key: String, val description: String, val required: Boolean, - val type: String, + val type: KType, val default: T?, val presets: Map?, val validator: (T?) -> Boolean, @@ -92,7 +79,7 @@ data class Option( option.key, option.description.orEmpty(), option.required, - option.valueType, + option.type, option.default, option.values, { option.validator(option, it) }, diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt index e2aed2ee..3780e899 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -27,15 +27,13 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { val selectedBundles = selectedPatches.keys val allPatches = bundles.filterKeys { selectedBundles.contains(it) } - .mapValues { (_, bundle) -> bundle.patchClasses(packageName) } + .mapValues { (_, bundle) -> bundle.patches(packageName) } val patchList = selectedPatches.flatMap { (bundle, selected) -> allPatches[bundle]?.filter { selected.contains(it.name) } ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") } - val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations } - // Set all patch options. options.forEach { (bundle, bundlePatchOptions) -> val patches = allPatches[bundle] ?: return@forEach @@ -53,7 +51,6 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { cacheDir, frameworkPath, aaptPath, - enableMultithreadedDexWriter(), context, logger, File(inputFile), @@ -62,8 +59,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { ).use { session -> session.run( File(outputFile), - patchList, - integrations + patchList ) } } diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 389d5201..ada1d943 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -70,7 +70,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { onProgress: ProgressEventHandler, ) = coroutineScope { // Get the location of our own Apk. - val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo.sourceDir + val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir val limit = "${prefs.patcherProcessMemoryLimit.get()}M" val propOverride = resolvePropOverride(context)?.absolutePath @@ -148,13 +148,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { packageName = packageName, inputFile = inputFile, outputFile = outputFile, - enableMultithrededDexWriter = enableMultithreadedDexWriter(), configurations = selectedPatches.map { (id, patches) -> val bundle = bundles[id]!! PatchConfiguration( bundle.patchesJar.absolutePath, - bundle.integrations?.absolutePath, patches, options[id].orEmpty() ) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt index fd39c3f3..434c97c6 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -26,7 +26,6 @@ sealed class Runtime(context: Context) : KoinComponent { context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath protected suspend fun bundles() = patchBundlesRepo.bundles.first() - protected suspend fun enableMultithreadedDexWriter() = prefs.multithreadingDexFileWriter.get() abstract suspend fun execute( inputFile: String, diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt index c669c875..b00d558a 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt @@ -12,14 +12,12 @@ data class Parameters( val packageName: String, val inputFile: String, val outputFile: String, - val enableMultithrededDexWriter: Boolean, val configurations: List, ) : Parcelable @Parcelize data class PatchConfiguration( val bundlePath: String, - val integrationsPath: String?, val patches: Set, val options: @RawValue Map> ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt index 4467f3ae..b0f8e248 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -54,13 +54,11 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") - val integrations = - parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) } val patchList = parameters.configurations.flatMap { config -> - val bundle = PatchBundle(File(config.bundlePath), null) + val bundle = PatchBundle(File(config.bundlePath)) val patches = - bundle.patchClasses(parameters.packageName).filter { it.name in config.patches } + bundle.patches(parameters.packageName).filter { it.name in config.patches } .associateBy { it.name } config.options.forEach { (patchName, opts) -> @@ -81,7 +79,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { cacheDir = parameters.cacheDir, aaptPath = parameters.aaptPath, frameworkDir = parameters.frameworkDir, - multithreadingDexFileWriter = parameters.enableMultithrededDexWriter, androidContext = context, logger = logger, input = File(parameters.inputFile), @@ -90,7 +87,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { events.progress(name, state?.name, message) } ).use { - it.run(File(parameters.outputFile), patchList, integrations) + it.run(File(parameters.outputFile), patchList) } events.finished(null) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index e84f8047..a7f0bfce 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 @@ -149,7 +149,7 @@ class PatcherWorker( return try { if (args.input is SelectedApp.Installed) { installedAppRepository.get(args.packageName)?.let { - if (it.installType == InstallType.ROOT) { + if (it.installType == InstallType.MOUNT) { rootInstaller.unmount(args.packageName) } } @@ -209,7 +209,7 @@ class PatcherWorker( } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } - is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir) + is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir) } val runtime = if (prefs.useProcessRuntime.get()) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt index 2f146c92..29f2f970 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt @@ -8,7 +8,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Update import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -24,6 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.util.transparentListItemColors @Composable @@ -77,7 +77,7 @@ private fun AutoUpdatesItem( ) = ListItem( leadingContent = { Icon(icon, null) }, headlineContent = { Text(stringResource(headline)) }, - trailingContent = { Checkbox(checked = checked, onCheckedChange = null) }, + trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) }, modifier = Modifier.clickable { onCheckedChange(!checked) }, colors = transparentListItemColors -) \ No newline at end of file +) diff --git a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt index 7059ad0d..4a684c1e 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.ui.component.haptics.HapticCheckbox @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -70,7 +71,7 @@ fun AvailableUpdateDialog( }, leadingContent = { CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { - Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) } } ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt new file mode 100644 index 00000000..a31a813e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -0,0 +1,159 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInstaller +import androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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 +import com.github.materiiapps.enumutil.FromValue + +private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) +private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit + +interface InstallerModel { + fun reinstall() + fun install() +} + +interface InstallerStatusDialogModel : InstallerModel { + var packageInstallerStatus: Int? +} + +@Composable +fun InstallerStatusDialog(model: InstallerStatusDialogModel) { + val dialogKind = remember { + DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE + } + + AlertDialog( + onDismissRequest = { + model.packageInstallerStatus = null + }, + confirmButton = { + dialogKind.confirmButton(model) + }, + dismissButton = { + dialogKind.dismissButton?.invoke(model) + }, + icon = { + Icon(dialogKind.icon, null) + }, + title = { + Text( + text = stringResource(dialogKind.title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(stringResource(dialogKind.contentStringResId)) + } + } + ) +} + +private fun installerStatusDialogButton( + @StringRes buttonStringResId: Int, + buttonHandler: InstallerStatusDialogButtonHandler = { }, +): InstallerStatusDialogButton = { model -> + TextButton( + onClick = { + model.packageInstallerStatus = null + buttonHandler(model) + } + ) { + Text(stringResource(buttonStringResId)) + } +} + +@FromValue("flag") +enum class DialogKind( + val flag: Int, + val title: Int, + @StringRes val contentStringResId: Int, + val icon: ImageVector = Icons.Outlined.ErrorOutline, + val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), + val dismissButton: InstallerStatusDialogButton? = null, +) { + FAILURE( + flag = PackageInstaller.STATUS_FAILURE, + title = R.string.installation_failed_dialog_title, + contentStringResId = R.string.installation_failed_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_ABORTED( + flag = PackageInstaller.STATUS_FAILURE_ABORTED, + title = R.string.installation_cancelled_dialog_title, + contentStringResId = R.string.installation_aborted_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_BLOCKED( + flag = PackageInstaller.STATUS_FAILURE_BLOCKED, + title = R.string.installation_blocked_dialog_title, + contentStringResId = R.string.installation_blocked_description, + ), + FAILURE_CONFLICT( + flag = PackageInstaller.STATUS_FAILURE_CONFLICT, + title = R.string.installation_conflict_dialog_title, + contentStringResId = R.string.installation_conflict_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_INCOMPATIBLE( + flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, + title = R.string.installation_incompatible_dialog_title, + contentStringResId = R.string.installation_incompatible_description, + ), + FAILURE_INVALID( + flag = PackageInstaller.STATUS_FAILURE_INVALID, + title = R.string.installation_invalid_dialog_title, + contentStringResId = R.string.installation_invalid_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_STORAGE( + flag = PackageInstaller.STATUS_FAILURE_STORAGE, + title = R.string.installation_storage_issue_dialog_title, + contentStringResId = R.string.installation_storage_issue_description, + ), + + @RequiresApi(34) + FAILURE_TIMEOUT( + flag = PackageInstaller.STATUS_FAILURE_TIMEOUT, + title = R.string.installation_timeout_dialog_title, + contentStringResId = R.string.installation_timeout_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + }, + ); + // Needed due to the @FromValue annotation. + companion object +} 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 2b678603..dfc63735 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -2,37 +2,34 @@ package app.revanced.manager.ui.component.bundle import android.webkit.URLUtil import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text +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.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.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.TextInputDialog -import app.revanced.manager.util.isDebuggable +import app.revanced.manager.ui.component.haptics.HapticSwitch @Composable fun BaseBundleDialog( modifier: Modifier = Modifier, isDefault: Boolean, name: String?, - onNameChange: ((String) -> Unit)? = null, remoteUrl: String?, onRemoteUrlChange: ((String) -> Unit)? = null, patchCount: Int, @@ -40,39 +37,66 @@ fun BaseBundleDialog( autoUpdate: Boolean, onAutoUpdateChange: (Boolean) -> Unit, onPatchesClick: () -> Unit, - onBundleTypeClick: () -> Unit = {}, extraFields: @Composable ColumnScope.() -> Unit = {} ) { ColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .then(modifier) + .then(modifier), ) { - if (name != null) { - var showNameInputDialog by rememberSaveable { - mutableStateOf(false) - } - if (showNameInputDialog) { - TextInputDialog( - initial = name, - title = stringResource(R.string.bundle_input_name), - onDismissRequest = { - showNameInputDialog = false - }, - onConfirm = { - showNameInputDialog = false - onNameChange?.invoke(it) - }, - validator = { - it.length in 1..19 - } + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Inventory2, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) ) + name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), + color = MaterialTheme.colorScheme.primary, + ) + } } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 2.dp) + ) { + version?.let { + Tag(Icons.Outlined.Sell, it) + } + Tag(Icons.Outlined.Extension, patchCount.toString()) + } + } + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + + if (remoteUrl != null) { BundleListItem( - headlineText = stringResource(R.string.bundle_input_name), - supportingText = name.ifEmpty { stringResource(R.string.field_not_set) }, - modifier = Modifier.clickable(enabled = onNameChange != null) { - showNameInputDialog = true + headlineText = stringResource(R.string.bundle_auto_update), + supportingText = stringResource(R.string.bundle_auto_update_description), + trailingContent = { + HapticSwitch( + checked = autoUpdate, + onCheckedChange = onAutoUpdateChange + ) + }, + modifier = Modifier.clickable { + onAutoUpdateChange(!autoUpdate) } ) } @@ -99,81 +123,59 @@ fun BaseBundleDialog( } BundleListItem( - modifier = Modifier.clickable(enabled = onRemoteUrlChange != null) { - showUrlInputDialog = true - }, - headlineText = stringResource(R.string.bundle_input_source_url), - supportingText = url.ifEmpty { stringResource(R.string.field_not_set) } - ) - } - - extraFields() - - if (remoteUrl != null) { - BundleListItem( - headlineText = stringResource(R.string.bundle_auto_update), - supportingText = stringResource(R.string.bundle_auto_update_description), - trailingContent = { - Switch( - checked = autoUpdate, - onCheckedChange = onAutoUpdateChange - ) - }, - modifier = Modifier.clickable { - onAutoUpdateChange(!autoUpdate) - } - ) - } - - BundleListItem( - headlineText = stringResource(R.string.bundle_type), - supportingText = stringResource(R.string.bundle_type_description), - modifier = Modifier.clickable { - onBundleTypeClick() - } - ) { - FilledTonalButton( - onClick = onBundleTypeClick, - content = { - if (remoteUrl == null) { - Text(stringResource(R.string.local)) - } else { - Text(stringResource(R.string.remote)) + modifier = Modifier.clickable( + enabled = onRemoteUrlChange != null, + onClick = { + showUrlInputDialog = true } + ), + headlineText = stringResource(R.string.bundle_input_source_url), + supportingText = url.ifEmpty { + stringResource(R.string.field_not_set) } ) } - if (version != null || patchCount > 0) { - Text( - text = stringResource(R.string.information), - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 12.dp - ), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - } - - val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0 + val patchesClickable = patchCount > 0 BundleListItem( headlineText = stringResource(R.string.patches), - supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount), - modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick) + supportingText = stringResource(R.string.bundle_view_patches), + modifier = Modifier.clickable( + enabled = patchesClickable, + onClick = onPatchesClick + ) ) { - if (patchesClickable) + if (patchesClickable) { Icon( Icons.AutoMirrored.Outlined.ArrowRight, stringResource(R.string.patches) ) + } } - version?.let { - BundleListItem( - headlineText = stringResource(R.string.version), - supportingText = it, - ) - } + extraFields() + } +} + +@Composable +private fun Tag( + icon: ImageVector, + text: String +) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) } } \ 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 1129abb1..eaebd834 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -7,17 +7,9 @@ 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.Update -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -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.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.Dialog @@ -72,7 +64,7 @@ fun BundleInformationDialog( Scaffold( topBar = { BundleTopBar( - title = bundleName, + title = stringResource(R.string.patch_bundle_field), onBackClick = onDismissRequest, backIcon = { Icon( @@ -105,10 +97,9 @@ fun BundleInformationDialog( modifier = Modifier.padding(paddingValues), isDefault = bundle.isDefault, name = bundleName, - onNameChange = { composableScope.launch { bundle.setName(it) } }, remoteUrl = bundle.asRemoteOrNull?.endpoint, patchCount = patchCount, - version = props?.versionInfo?.patches, + version = props?.version, autoUpdate = props?.autoUpdate ?: false, onAutoUpdateChange = { composableScope.launch { diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index 617c384f..6f3ae914 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -27,6 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import kotlinx.coroutines.flow.map @@ -45,7 +45,7 @@ fun BundleItem( val state by bundle.state.collectAsStateWithLifecycle() val version by remember(bundle) { - bundle.propsFlow().map { props -> props?.versionInfo?.patches } + bundle.propsFlow().map { props -> props?.version } }.collectAsStateWithLifecycle(null) val name by bundle.nameState @@ -71,7 +71,7 @@ fun BundleItem( ), leadingContent = if (selectable) { { - Checkbox( + HapticCheckbox( checked = isBundleSelected, onCheckedChange = toggleSelection, ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index a4fbce81..99201949 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 @@ -1,33 +1,35 @@ package app.revanced.manager.ui.component.bundle import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.outlined.Lightbulb -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.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.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow 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.bundles.PatchBundleSource +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LazyColumnWithScrollbar -import app.revanced.manager.ui.component.NotificationCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -35,7 +37,8 @@ fun BundlePatchesDialog( onDismissRequest: () -> Unit, bundle: PatchBundleSource, ) { - var informationCardVisible by remember { mutableStateOf(true) } + var showAllVersions by rememberSaveable { mutableStateOf(false) } + var showOptions by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() Dialog( @@ -62,44 +65,212 @@ fun BundlePatchesDialog( LazyColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .padding(paddingValues) - .padding(16.dp) + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp) ) { - item { - AnimatedVisibility(visible = informationCardVisible) { - NotificationCard( - icon = Icons.Outlined.Lightbulb, - text = stringResource(R.string.tap_on_patches), - onDismiss = { informationCardVisible = false } - ) - } - } - state.patchBundleOrNull()?.let { bundle -> - 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 { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + items(bundle.patches) { patch -> + PatchItem( + patch, + showAllVersions, + onExpandVersions = { showAllVersions = !showAllVersions }, + showOptions, + onExpandOptions = { showOptions = !showOptions } ) - HorizontalDivider() } } } } } } + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PatchItem( + patch: PatchInfo, + expandVersions: Boolean, + onExpandVersions: () -> Unit, + expandOptions: Boolean, + onExpandOptions: () -> Unit +) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .then( + if (patch.options.isNullOrEmpty()) Modifier else Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onExpandOptions), + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = patch.name, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + if (!patch.options.isNullOrEmpty()) { + ArrowButton(expanded = expandOptions, onClick = null) + } + } + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (patch.compatiblePackages.isNullOrEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PatchInfoChip( + text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" + ) + PatchInfoChip( + text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" + ) + } + } else { + patch.compatiblePackages.forEach { compatiblePackage -> + val packageName = compatiblePackage.packageName + val versions = compatiblePackage.versions.orEmpty().reversed() + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$PACKAGE_ICON $packageName" + ) + + if (versions.isNotEmpty()) { + if (expandVersions) { + versions.forEach { version -> + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON $version" + ) + } + } else { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON ${versions.first()}" + ) + } + if (versions.size > 1) { + PatchInfoChip( + onClick = onExpandVersions, + text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}" + ) + } + } + } + } + } + } + if (!patch.options.isNullOrEmpty()) { + AnimatedVisibility(visible = expandOptions) { + val options = patch.options + + Column { + options.forEachIndexed { i, option -> + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), shape = when { + options.size == 1 -> RoundedCornerShape(8.dp) + i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + i == options.lastIndex -> RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + + else -> RoundedCornerShape(0.dp) + } + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = option.title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = option.description, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } + } + } + } +} + +@Composable +fun PatchInfoChip( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + text: String +) { + val shape = RoundedCornerShape(8.0.dp) + val cardModifier = if (onClick != null) { + Modifier + .clip(shape) + .clickable(onClick = onClick) + } else { + Modifier + } + + OutlinedCard( + modifier = modifier.then(cardModifier), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), + shape = shape, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f)) + ) { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text, + overflow = TextOverflow.Ellipsis, + softWrap = false, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +const val PACKAGE_ICON = "\uD83D\uDCE6" +const val VERSION_ICON = "\uD83C\uDFAF" \ 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 2de48a56..cbc699ec 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -10,26 +10,9 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Topic -import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -37,20 +20,21 @@ import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.TextHorizontalPadding +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.model.BundleType -import app.revanced.manager.util.APK_MIMETYPE -import app.revanced.manager.util.JAR_MIMETYPE +import app.revanced.manager.util.BIN_MIMETYPE +import app.revanced.manager.util.transparentListItemColors @Composable fun ImportPatchBundleDialog( onDismiss: () -> Unit, onRemoteSubmit: (String, Boolean) -> Unit, - onLocalSubmit: (Uri, Uri?) -> Unit + onLocalSubmit: (Uri) -> Unit ) { var currentStep by rememberSaveable { mutableIntStateOf(0) } var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } var patchBundle by rememberSaveable { mutableStateOf(null) } - var integrations by rememberSaveable { mutableStateOf(null) } var remoteUrl by rememberSaveable { mutableStateOf("") } var autoUpdate by rememberSaveable { mutableStateOf(false) } @@ -60,16 +44,7 @@ fun ImportPatchBundleDialog( } fun launchPatchActivity() { - patchActivityLauncher.launch(JAR_MIMETYPE) - } - - val integrationsActivityLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { integrations = it } - } - - fun launchIntegrationsActivity() { - integrationsActivityLauncher.launch(APK_MIMETYPE) + patchActivityLauncher.launch(BIN_MIMETYPE) } val steps = listOf<@Composable () -> Unit>( @@ -82,11 +57,9 @@ fun ImportPatchBundleDialog( ImportBundleStep( bundleType, patchBundle, - integrations, remoteUrl, autoUpdate, { launchPatchActivity() }, - { launchIntegrationsActivity() }, { remoteUrl = it }, { autoUpdate = it } ) @@ -114,13 +87,7 @@ fun ImportPatchBundleDialog( enabled = inputsAreValid, onClick = { when (bundleType) { - BundleType.Local -> patchBundle?.let { - onLocalSubmit( - it, - integrations - ) - } - + BundleType.Local -> patchBundle?.let(onLocalSubmit) BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate) } } @@ -170,11 +137,12 @@ fun SelectBundleTypeStep( overlineContent = { Text(stringResource(R.string.recommended)) }, supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, leadingContent = { - RadioButton( + HapticRadioButton( selected = bundleType == BundleType.Remote, onClick = null ) - } + }, + colors = transparentListItemColors ) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) ListItem( @@ -186,11 +154,12 @@ fun SelectBundleTypeStep( supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, overlineContent = { }, leadingContent = { - RadioButton( + HapticRadioButton( selected = bundleType == BundleType.Local, onClick = null ) - } + }, + colors = transparentListItemColors ) } } @@ -201,11 +170,9 @@ fun SelectBundleTypeStep( fun ImportBundleStep( bundleType: BundleType, patchBundle: Uri?, - integrations: Uri?, remoteUrl: String, autoUpdate: Boolean, launchPatchActivity: () -> Unit, - launchIntegrationsActivity: () -> Unit, onRemoteUrlChange: (String) -> Unit, onAutoUpdateChange: (Boolean) -> Unit ) { @@ -225,19 +192,8 @@ fun ImportBundleStep( Icon(imageVector = Icons.Default.Topic, contentDescription = null) } }, - modifier = Modifier.clickable { launchPatchActivity() } - ) - ListItem( - headlineContent = { - Text(stringResource(R.string.integrations_field)) - }, - supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) }, - trailingContent = { - IconButton(onClick = launchIntegrationsActivity) { - Icon(imageVector = Icons.Default.Topic, contentDescription = null) - } - }, - modifier = Modifier.clickable { launchIntegrationsActivity() } + modifier = Modifier.clickable { launchPatchActivity() }, + colors = transparentListItemColors ) } } @@ -263,7 +219,7 @@ fun ImportBundleStep( headlineContent = { Text(stringResource(R.string.auto_update)) }, leadingContent = { CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { - Checkbox( + HapticCheckbox( checked = autoUpdate, onCheckedChange = { onAutoUpdateChange(!autoUpdate) @@ -271,6 +227,7 @@ fun ImportBundleStep( ) } }, + colors = transparentListItemColors ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt new file mode 100644 index 00000000..fb5453f9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt @@ -0,0 +1,30 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxColors +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticCheckbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: CheckboxColors = CheckboxDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK), + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt new file mode 100644 index 00000000..4fc6ad30 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt @@ -0,0 +1,41 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.FloatingActionButtonElevation +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticExtendedFloatingActionButton ( + text: @Composable () -> Unit, + icon: @Composable () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, + expanded: Boolean = true, + shape: Shape = FloatingActionButtonDefaults.extendedFabShape, + containerColor: Color = FloatingActionButtonDefaults.containerColor, + contentColor: Color = contentColorFor(containerColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + ExtendedFloatingActionButton( + text = text, + icon = icon, + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + expanded = expanded, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt new file mode 100644 index 00000000..f4a2e153 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.FloatingActionButtonElevation +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticFloatingActionButton ( + onClick: () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = FloatingActionButtonDefaults.shape, + containerColor: Color = FloatingActionButtonDefaults.containerColor, + contentColor: Color = contentColorFor(containerColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit, +) { + FloatingActionButton( + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, + interactionSource = interactionSource, + content = content + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt new file mode 100644 index 00000000..63a9e582 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt @@ -0,0 +1,38 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView + +@Composable +fun HapticRadioButton( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: RadioButtonColors = RadioButtonDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val view = LocalView.current + + RadioButton( + selected = selected, + onClick = onClick?.let { + { + // Perform haptic feedback + if (!selected) view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + it() + } + }, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt new file mode 100644 index 00000000..c2491397 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt @@ -0,0 +1,41 @@ +package app.revanced.manager.ui.component.haptics + +import android.os.Build +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchColors +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +fun HapticSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + thumbContent: (@Composable () -> Unit)? = null, + enabled: Boolean = true, + colors: SwitchColors = SwitchDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + Switch( + checked = checked, + onCheckedChange = { newChecked -> + val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + when { + newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON + newChecked -> HapticFeedbackConstants.VIRTUAL_KEY + !newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF + !newChecked -> HapticFeedbackConstants.CLOCK_TICK + } + onCheckedChange(newChecked) + }, + modifier = modifier, + thumbContent = thumbContent, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt new file mode 100644 index 00000000..d0676951 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt @@ -0,0 +1,36 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Tab +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticTab ( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: @Composable (() -> Unit)? = null, + icon: @Composable (() -> Unit)? = null, + selectedContentColor: Color = LocalContentColor.current, + unselectedContentColor: Color = selectedContentColor, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Tab( + selected = selected, + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + enabled = enabled, + text = text, + icon = icon, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + interactionSource = interactionSource + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt index 497859c8..b86124d9 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -2,11 +2,7 @@ package app.revanced.manager.ui.component.patcher import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ListItem -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import app.revanced.manager.R import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.util.transparentListItemColors @Composable @@ -28,7 +25,7 @@ fun InstallPickerDialog( AlertDialog( onDismissRequest = onDismiss, dismissButton = { - Button(onClick = onDismiss) { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, @@ -49,7 +46,7 @@ fun InstallPickerDialog( ListItem( modifier = Modifier.clickable { selectedInstallType = it }, leadingContent = { - RadioButton( + HapticRadioButton( selected = selectedInstallType == it, onClick = null ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt index 25254d95..3c0504bc 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -20,53 +20,31 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.DragHandle -import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material.icons.outlined.Folder -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material.icons.outlined.Restore -import androidx.compose.material.icons.outlined.SelectAll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.TextButton +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog as ComposeDialog import androidx.compose.ui.window.DialogProperties import app.revanced.manager.R import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.patcher.patch.Option -import app.revanced.manager.ui.component.AlertDialogExtended -import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.ui.component.FloatInputDialog -import app.revanced.manager.ui.component.IntInputDialog -import app.revanced.manager.ui.component.LongInputDialog +import app.revanced.manager.ui.component.* +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.ui.component.haptics.HapticSwitch import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.mutableStateSetOf import app.revanced.manager.util.saver.snapshotStateListSaver @@ -81,6 +59,8 @@ import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyColumnState import java.io.Serializable import kotlin.random.Random +import kotlin.reflect.typeOf +import androidx.compose.ui.window.Dialog as ComposeDialog private class OptionEditorScope( private val editor: OptionEditor, @@ -118,17 +98,17 @@ private interface OptionEditor { fun Dialog(scope: OptionEditorScope) } +private inline fun OptionEditor.toMapEditorElements() = arrayOf( + typeOf() to this, + typeOf>() to ListOptionEditor(this) +) + private val optionEditors = mapOf( - "Boolean" to BooleanOptionEditor, - "String" to StringOptionEditor, - "Int" to IntOptionEditor, - "Long" to LongOptionEditor, - "Float" to FloatOptionEditor, - "BooleanArray" to ListOptionEditor(BooleanOptionEditor), - "StringArray" to ListOptionEditor(StringOptionEditor), - "IntArray" to ListOptionEditor(IntOptionEditor), - "LongArray" to ListOptionEditor(LongOptionEditor), - "FloatArray" to ListOptionEditor(FloatOptionEditor), + *BooleanOptionEditor.toMapEditorElements(), + *StringOptionEditor.toMapEditorElements(), + *IntOptionEditor.toMapEditorElements(), + *LongOptionEditor.toMapEditorElements(), + *FloatOptionEditor.toMapEditorElements() ) @Composable @@ -167,7 +147,7 @@ fun OptionItem(option: Option, value: T?, setValue: (T?) -> Unit) { val baseOptionEditor = optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor - if (option.type != "Boolean" && option.presets != null) PresetOptionEditor(baseOptionEditor) + if (option.type != typeOf() && option.presets != null) PresetOptionEditor(baseOptionEditor) else baseOptionEditor } @@ -336,7 +316,7 @@ private object BooleanOptionEditor : OptionEditor { @Composable override fun ListItemTrailingContent(scope: OptionEditorScope) { - Switch(checked = scope.current, onCheckedChange = scope.setValue) + HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue) } @Composable @@ -423,7 +403,7 @@ private class PresetOptionEditor(private val innerEditor: OptionEditor< headlineContent = { Text(title) }, supportingContent = value?.toString()?.let { { Text(it) } }, leadingContent = { - RadioButton( + HapticRadioButton( selected = selectedPreset == presetKey, onClick = { selectedPreset = presetKey } ) @@ -453,7 +433,7 @@ private class ListOptionEditor(private val elementEditor: Opti option.key, option.description, option.required, - option.type.removeSuffix("Array"), + option.type.arguments.first().type!!, null, null ) { true } @@ -570,7 +550,7 @@ private class ListOptionEditor(private val elementEditor: Opti floatingActionButton = { if (deleteMode) return@Scaffold - ExtendedFloatingActionButton( + HapticExtendedFloatingActionButton( text = { Text(stringResource(R.string.add)) }, icon = { Icon( diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt index 42e9a83e..0be1be91 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt @@ -2,13 +2,13 @@ package app.revanced.manager.ui.component.settings import androidx.annotation.StringRes import androidx.compose.foundation.clickable -import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import app.revanced.manager.domain.manager.base.Preference +import app.revanced.manager.ui.component.haptics.HapticSwitch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -45,7 +45,7 @@ fun BooleanItem( headlineContent = stringResource(headline), supportingContent = stringResource(description), trailingContent = { - Switch( + HapticSwitch( checked = value, onCheckedChange = onValueChange, ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt index 0df083b9..9086f39e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt @@ -116,7 +116,6 @@ fun AppSelectorScreen( }, colors = transparentListItemColors ) - } } } else { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 3c7ac522..a71e390c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -33,6 +33,8 @@ import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleTopBar +import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.util.toast @@ -81,9 +83,9 @@ fun DashboardScreen( if (showAddBundleDialog) { ImportPatchBundleDialog( onDismiss = { showAddBundleDialog = false }, - onLocalSubmit = { patches, integrations -> + onLocalSubmit = { patches -> showAddBundleDialog = false - vm.createLocalSource(patches, integrations) + vm.createLocalSource(patches) }, onRemoteSubmit = { url, autoUpdate -> showAddBundleDialog = false @@ -172,7 +174,7 @@ fun DashboardScreen( } }, floatingActionButton = { - FloatingActionButton( + HapticFloatingActionButton( onClick = { vm.cancelSourceSelection() @@ -185,7 +187,7 @@ fun DashboardScreen( DashboardPage.BUNDLES.ordinal ) } - return@FloatingActionButton + return@HapticFloatingActionButton } onAppSelectorClick() @@ -205,7 +207,7 @@ fun DashboardScreen( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) ) { DashboardPage.entries.forEachIndexed { index, page -> - Tab( + HapticTab( selected = pagerState.currentPage == index, onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, text = { Text(stringResource(page.titleResId)) }, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 2727e290..239aebbf 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -81,7 +81,7 @@ fun InstalledAppInfoScreen( AppInfo(viewModel.appInfo) { Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) - if (viewModel.installedApp.installType == InstallType.ROOT) { + if (viewModel.installedApp.installType == InstallType.MOUNT) { Text( text = if (viewModel.isMounted) { stringResource(R.string.mounted) @@ -112,7 +112,7 @@ fun InstalledAppInfoScreen( onClick = viewModel::uninstall ) - InstallType.ROOT -> { + InstallType.MOUNT -> { SegmentedButton( icon = Icons.Outlined.SettingsBackupRestore, text = stringResource(R.string.unpatch), @@ -138,7 +138,7 @@ fun InstalledAppInfoScreen( onPatchClick(viewModel.installedApp.originalPackageName, it) } }, - enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess() + enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess() ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index edd4f564..fb1a50e3 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -5,12 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -18,17 +13,8 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.Save -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -42,8 +28,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.InstallerStatusDialog +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.State @@ -96,6 +85,9 @@ fun PatcherScreen( onConfirm = vm::install ) + if (vm.installerStatusDialogModel.packageInstallerStatus != null) + InstallerStatusDialog(vm.installerStatusDialogModel) + val activityLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = vm::handleActivityResult @@ -139,7 +131,7 @@ fun PatcherScreen( actions = { IconButton( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = canInstall + enabled = patcherSucceeded == true ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } @@ -152,7 +144,7 @@ fun PatcherScreen( }, floatingActionButton = { AnimatedVisibility(visible = canInstall) { - ExtendedFloatingActionButton( + HapticExtendedFloatingActionButton( text = { Text( stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app) @@ -171,7 +163,8 @@ fun PatcherScreen( }, onClick = { if (vm.installedPackageName == null) - showInstallPicker = true + if (vm.isDeviceRooted()) showInstallPicker = true + else vm.install(InstallType.DEFAULT) else vm.open() } ) @@ -208,4 +201,4 @@ fun PatcherScreen( } } } -} \ 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 07ada19c..8e714422 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -35,6 +35,9 @@ import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.SearchView +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED @@ -299,7 +302,7 @@ fun PatchesSelectorScreen( floatingActionButton = { if (!showPatchButton) return@Scaffold - ExtendedFloatingActionButton( + HapticExtendedFloatingActionButton( text = { Text(stringResource(R.string.save)) }, icon = { Icon( @@ -327,7 +330,7 @@ fun PatchesSelectorScreen( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) ) { bundles.forEachIndexed { index, bundle -> - Tab( + HapticTab( selected = pagerState.currentPage == index, onClick = { composableScope.launch { @@ -444,7 +447,7 @@ private fun PatchItem( .clickable(onClick = onToggle) .fillMaxSize(), leadingContent = { - Checkbox( + HapticCheckbox( checked = selected, onCheckedChange = { onToggle() }, enabled = supported diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index e759d0bc..d57d3093 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -12,14 +12,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.filled.AutoFixHigh -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -38,6 +31,7 @@ import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.destination.SelectedAppInfoDestination import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.SelectedApp @@ -49,11 +43,7 @@ import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.enabled import app.revanced.manager.util.toast import app.revanced.manager.util.transparentListItemColors -import dev.olshevski.navigation.reimagined.AnimatedNavHost -import dev.olshevski.navigation.reimagined.NavBackHandler -import dev.olshevski.navigation.reimagined.navigate -import dev.olshevski.navigation.reimagined.pop -import dev.olshevski.navigation.reimagined.rememberNavController +import dev.olshevski.navigation.reimagined.* import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -110,7 +100,7 @@ fun SelectedAppInfoScreen( floatingActionButton = { if (error != null) return@Scaffold - ExtendedFloatingActionButton( + HapticExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch)) }, icon = { Icon( @@ -299,7 +289,7 @@ private fun AppSourceSelectorDialog( item(key = "installed") { val (usable, text) = when { // Mounted apps must be unpatched before patching, which cannot be done without root access. - meta?.installType == InstallType.ROOT && !hasRoot -> false to "Mounted apps cannot be patched again without root access" + meta?.installType == InstallType.MOUNT && !hasRoot -> false to "Mounted apps cannot be patched again without root access" // Patching already patched apps is not allowed because patches expect unpatched apps. meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched) else -> true to app.version diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index f43ecb4b..0d0bd46e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -66,7 +66,7 @@ fun SettingsScreen( ) to SettingsDestination.Advanced, Triple( R.string.about, - R.string.about_description, + R.string.app_name, Icons.Outlined.Info ) to SettingsDestination.About, ) 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 04a104ff..cb474f09 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -1,10 +1,15 @@ package app.revanced.manager.ui.screen.settings import android.app.ActivityManager +import android.content.ClipData +import android.content.ClipboardManager import android.os.Build +import android.view.HapticFeedbackConstants import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api @@ -28,9 +33,11 @@ import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel +import app.revanced.manager.util.toast +import app.revanced.manager.util.withHapticFeedback import org.koin.androidx.compose.koinViewModel -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun AdvancedSettingsScreen( onBackClick: () -> Unit, @@ -82,15 +89,6 @@ fun AdvancedSettingsScreen( } ) - val exportDebugLogsLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { - it?.let(vm::exportDebugLogs) - } - SettingsListItem( - headlineContent = stringResource(R.string.debug_logs_export), - modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) } - ) - GroupHeader(stringResource(R.string.patcher)) BooleanItem( preference = vm.prefs.useProcessRuntime, @@ -104,12 +102,6 @@ fun AdvancedSettingsScreen( headline = R.string.process_runtime_memory_limit, description = R.string.process_runtime_memory_limit_description, ) - BooleanItem( - preference = vm.prefs.multithreadingDexFileWriter, - coroutineScope = vm.viewModelScope, - headline = R.string.multithreaded_dex_file_writer, - description = R.string.multithreaded_dex_file_writer_description, - ) GroupHeader(stringResource(R.string.safeguards)) BooleanItem( @@ -138,16 +130,37 @@ fun AdvancedSettingsScreen( ) GroupHeader(stringResource(R.string.debugging)) + val exportDebugLogsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { + it?.let(vm::exportDebugLogs) + } SettingsListItem( - headlineContent = stringResource(R.string.about_device), - supportingContent = """ - **Version**: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) - **Build type**: ${BuildConfig.BUILD_TYPE} - **Model**: ${Build.MODEL} - **Android version**: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) - **Supported Archs**: ${Build.SUPPORTED_ABIS.joinToString(", ")} - **Memory limit**: $memoryLimit + headlineContent = stringResource(R.string.debug_logs_export), + modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) } + ) + val clipboard = remember { context.getSystemService()!! } + val deviceContent = """ + Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) + Build type: ${BuildConfig.BUILD_TYPE} + Model: ${Build.MODEL} + Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) + Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")} + Memory limit: $memoryLimit """.trimIndent() + SettingsListItem( + modifier = Modifier.combinedClickable( + onClick = { }, + onLongClickLabel = stringResource(R.string.copy_to_clipboard), + onLongClick = { + clipboard.setPrimaryClip( + ClipData.newPlainText("Device Information", deviceContent) + ) + + context.toast(context.getString(R.string.toast_copied_to_clipboard)) + }.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + ), + headlineContent = stringResource(R.string.about_device), + supportingContent = deviceContent ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt index d4995aad..0b3a7888 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt @@ -2,7 +2,20 @@ package app.revanced.manager.ui.screen.settings import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt index f8f1e0cf..1789329e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt @@ -32,7 +32,7 @@ fun DeveloperOptionsScreen( Column(modifier = Modifier.padding(paddingValues)) { GroupHeader(stringResource(R.string.patch_bundles_section)) SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_redownload), + headlineContent = stringResource(R.string.patch_bundles_force_download), modifier = Modifier.clickable(onClick = vm::redownloadBundles) ) SettingsListItem( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt index 56f4e3d2..68e02942 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -41,9 +40,9 @@ import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.DownloadsViewModel -import app.revanced.manager.util.PM import org.koin.androidx.compose.koinViewModel import java.security.MessageDigest @@ -178,7 +177,7 @@ fun DownloadsSettingsScreen( is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted } ), - trailingContent = { Text(packageInfo.versionName) } + trailingContent = { Text(packageInfo.versionName!!) } ) } } @@ -202,7 +201,7 @@ fun DownloadsSettingsScreen( modifier = Modifier.clickable { viewModel.toggleApp(app) }, headlineContent = app.packageName, leadingContent = (@Composable { - Checkbox( + HapticCheckbox( checked = selected, onCheckedChange = { viewModel.toggleApp(app) } ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt index f41e6a66..56242679 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt @@ -2,8 +2,18 @@ package app.revanced.manager.ui.screen.settings import android.os.Build import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +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.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.RadioButton +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 @@ -18,6 +28,7 @@ import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.theme.Theme @@ -96,14 +107,14 @@ private fun ThemePicker( title = { Text(stringResource(R.string.theme)) }, text = { Column { - Theme.values().forEach { + Theme.entries.forEach { Row( modifier = Modifier .fillMaxWidth() .clickable { selectedTheme = it }, verticalAlignment = Alignment.CenterVertically ) { - RadioButton( + HapticRadioButton( selected = selectedTheme == it, onClick = { selectedTheme = it }) Text(stringResource(it.displayName)) 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 1f15f469..84a96694 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 @@ -250,12 +250,17 @@ private fun PackageSelector(packages: Set, onFinish: (String?) -> Unit) } @Composable -private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) = +private fun GroupItem( + onClick: () -> Unit, + @StringRes headline: Int, + @StringRes description: Int? = null +) { SettingsListItem( modifier = Modifier.clickable { onClick() }, headlineContent = stringResource(headline), - supportingContent = stringResource(description) + supportingContent = description?.let { stringResource(it) } ) +} @Composable fun KeystoreCredentialsDialog( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt index 3a9bb824..76e2e964 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt @@ -1,8 +1,11 @@ package app.revanced.manager.ui.screen.settings -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index 7b91c4d3..610ee95d 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -79,7 +79,7 @@ class AppSelectorViewModel( pm.getPackageInfo(this)?.let { packageInfo -> SelectedApp.Local( packageName = packageInfo.packageName, - version = packageInfo.versionName, + version = packageInfo.versionName!!, file = this, temporary = true ) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 994897c0..99be81ec 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -103,13 +103,10 @@ class DashboardViewModel( selectedSources.clear() } - fun createLocalSource(patchBundle: Uri, integrations: Uri?) = + fun createLocalSource(patchBundle: Uri) = viewModelScope.launch { contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> - integrations?.let { contentResolver.openInputStream(it) } - .use { integrationsStream -> - patchBundleRepository.createLocal(patchesStream, integrationsStream) - } + patchBundleRepository.createLocal(patchesStream) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 7e610f55..93e2cb74 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -78,7 +78,7 @@ class InstalledAppInfoViewModel( when (installedApp.installType) { InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName) - InstallType.ROOT -> viewModelScope.launch { + InstallType.MOUNT -> viewModelScope.launch { rootInstaller.uninstall(installedApp.currentPackageName) installedAppRepository.delete(installedApp) onBackClick() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt index 27bec4c4..42ad08c7 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt @@ -30,7 +30,7 @@ class InstalledAppsViewModel( packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) { try { if ( - installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) + installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) ) { installedAppsRepository.delete(installedApp) return@withContext null @@ -39,7 +39,7 @@ class InstalledAppsViewModel( val packageInfo = pm.getPackageInfo(installedApp.currentPackageName) - if (packageInfo == null && installedApp.installType != InstallType.ROOT) { + if (packageInfo == null && installedApp.installType != InstallType.MOUNT) { installedAppsRepository.delete(installedApp) return@withContext null } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index c0f82541..c17a4d5a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -34,6 +34,8 @@ import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import app.revanced.manager.ui.component.InstallerStatusDialogModel import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State @@ -74,6 +76,20 @@ class PatcherViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() + val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel { + override var packageInstallerStatus: Int? by mutableStateOf(null) + + override fun reinstall() { + this@PatcherViewModel.reinstall() + } + + override fun install() { + // Since this is a package installer status dialog, + // InstallType.MOUNT is never used here. + install(InstallType.DEFAULT) + } + } + private var installedApp: InstalledApp? = null val packageName: String = input.selectedApp.packageName var installedPackageName by mutableStateOf(null) @@ -179,20 +195,25 @@ class PatcherViewModel( ) val patcherSucceeded = - workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> - when (workInfo.state) { + workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo? -> + when (workInfo?.state) { WorkInfo.State.SUCCEEDED -> true WorkInfo.State.FAILED -> false else -> null } } - private val installBroadcastReceiver = object : BroadcastReceiver() { + private val installerBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { InstallService.APP_INSTALL_ACTION -> { - val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) - val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + val pmStatus = intent.getIntExtra( + InstallService.EXTRA_INSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) if (pmStatus == PackageInstaller.STATUS_SUCCESS) { app.toast(app.getString(R.string.install_app_success)) @@ -203,14 +224,29 @@ class PatcherViewModel( installedPackageName!!, packageName, input.selectedApp.version - ?: pm.getPackageInfo(outputFile)!!.versionName, + ?: pm.getPackageInfo(outputFile)?.versionName!!, InstallType.DEFAULT, input.selectedPatches ) } - } else { - app.toast(app.getString(R.string.install_app_fail, extra)) - Log.e(tag, "Installation failed: $extra") + } + + installerStatusDialogModel.packageInstallerStatus = pmStatus + + isInstalling = false + } + + UninstallService.APP_UNINSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + UninstallService.EXTRA_UNINSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) + + if (pmStatus != PackageInstaller.STATUS_SUCCESS) { + installerStatusDialogModel.packageInstallerStatus = pmStatus } } } @@ -218,9 +254,15 @@ class PatcherViewModel( } init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. - ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) - }, ContextCompat.RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver( + app, + installerBroadcastReceiver, + IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + addAction(UninstallService.APP_UNINSTALL_ACTION) + }, + ContextCompat.RECEIVER_NOT_EXPORTED + ) viewModelScope.launch { installedApp = installedAppRepository.get(packageName) @@ -230,10 +272,10 @@ class PatcherViewModel( @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() - app.unregisterReceiver(installBroadcastReceiver) + app.unregisterReceiver(installerBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) - if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { + if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { GlobalScope.launch(Dispatchers.Main) { uiSafe(app, R.string.failed_to_mount, "Failed to mount") { withTimeout(Duration.ofMinutes(1L)) { @@ -246,6 +288,8 @@ class PatcherViewModel( tempDir.deleteRecursively() } + fun isDeviceRooted() = rootInstaller.isDeviceRooted() + fun rejectInteraction() { currentInteractionRequest?.complete(false) } @@ -308,34 +352,71 @@ class PatcherViewModel( fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { + var pmInstallStarted = false try { isInstalling = true + + val currentPackageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + + // If the app is currently installed + val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName) + if (existingPackageInfo != null) { + // Check if the app version is less than the installed version + if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) { + // Exit if the selected app version is less than the installed version + installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + } + when (installType) { InstallType.DEFAULT -> { + // Check if the app is mounted as root + // If it is, unmount it first, silently + if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { + rootInstaller.unmount(packageName) + } + + // Install regularly pm.installApp(listOf(outputFile)) + pmInstallStarted = true } - InstallType.ROOT -> { + InstallType.MOUNT -> { try { val packageInfo = pm.getPackageInfo(outputFile) ?: throw Exception("Failed to load application info") val label = with(pm) { packageInfo.label() } + // Check for base APK, first check if the app is already installed + if (existingPackageInfo == null) { + // If the app is not installed, check if the output file is a base apk + if (currentPackageInfo.splitNames.isNotEmpty()) { + // Exit if there is no base APK package + installerStatusDialogModel.packageInstallerStatus = + PackageInstaller.STATUS_FAILURE_INVALID + return@launch + } + } + // Install as root rootInstaller.install( outputFile, inputFile, packageName, - packageInfo.versionName, + // input.selectedApp.version? + packageInfo.versionName!!, label ) installedAppRepository.addOrUpdate( packageName, + // TODO: this seems wrong packageName, - packageInfo.versionName, - InstallType.ROOT, + packageInfo.versionName!!, + InstallType.MOUNT, input.selectedPatches ) @@ -354,8 +435,22 @@ class PatcherViewModel( } } } + } catch(e: Exception) { + Log.e(tag, "Failed to install", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } finally { - isInstalling = false + if (!pmInstallStarted) + isInstalling = false + } + } + + fun reinstall() = viewModelScope.launch { + uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { + pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } + ?: throw Exception("Failed to load application info") + + pm.installApp(listOf(outputFile)) + isInstalling = true } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index dfab990a..dfa9662e 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -100,7 +100,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { packageInfo.await()?.let { SelectedApp.Installed( packageName, - it.versionName + it.versionName!! ) to installedAppDeferred.await() } } diff --git a/app/src/main/java/app/revanced/manager/util/Constants.kt b/app/src/main/java/app/revanced/manager/util/Constants.kt index 983a7c42..000da463 100644 --- a/app/src/main/java/app/revanced/manager/util/Constants.kt +++ b/app/src/main/java/app/revanced/manager/util/Constants.kt @@ -1,14 +1,8 @@ package app.revanced.manager.util -private const val team = "revanced" -const val ghOrganization = "https://github.com/$team" -const val ghCli = "$team/revanced-cli" -const val ghPatches = "$team/revanced-patches" -const val ghPatcher = "$team/revanced-patcher" -const val ghManager = "$team/revanced-manager" -const val ghIntegrations = "$team/revanced-integrations" const val tag = "ReVanced Manager" const val JAR_MIMETYPE = "application/java-archive" const val APK_MIMETYPE = "application/vnd.android.package-archive" -const val JSON_MIMETYPE = "application/json" \ No newline at end of file +const val JSON_MIMETYPE = "application/json" +const val BIN_MIMETYPE = "application/octet-stream" \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index f275c5e7..f137e699 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -10,11 +10,11 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.NameNotFoundException +import androidx.core.content.pm.PackageInfoCompat import android.content.pm.Signature import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable -import androidx.core.content.pm.PackageInfoCompat import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.service.InstallService import app.revanced.manager.service.UninstallService @@ -121,7 +121,7 @@ class PM( val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null // This is needed in order to load label and icon. - pkgInfo.applicationInfo.apply { + pkgInfo.applicationInfo!!.apply { sourceDir = path publicSourceDir = path } @@ -129,6 +129,10 @@ class PM( return pkgInfo } + fun PackageInfo.label() = this.applicationInfo!!.loadLabel(app.packageManager).toString() + + fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) + fun getSignature(packageName: String): Signature = // Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used. PackageInfoCompat.getSignatures(app.packageManager, packageName).last() @@ -141,8 +145,6 @@ class PM( false ) - fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() - suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { val packageInstaller = app.packageManager.packageInstaller packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> @@ -198,4 +200,4 @@ class PM( Intent(this, UninstallService::class.java), intentFlags ).intentSender -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt index 8d7b7ec3..67dce3d4 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt @@ -2,6 +2,7 @@ package app.revanced.manager.util import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings @@ -10,7 +11,7 @@ import androidx.annotation.RequiresApi @RequiresApi(Build.VERSION_CODES.R) class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract() { - override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null)) override fun getSynchronousResult(context: Context, input: String): SynchronousResult? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null 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 415bdb46..27329fed 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf @@ -27,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -262,4 +264,24 @@ fun ScrollState.isScrollingUp(): State { val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value +@Composable +@ReadOnlyComposable +fun (() -> R).withHapticFeedback(constant: Int): () -> R { + val view = LocalView.current + return { + view.performHapticFeedback(constant) + this() + } +} + +@Composable +@ReadOnlyComposable +fun ((T) -> R).withHapticFeedback(constant: Int): (T) -> R { + val view = LocalView.current + return { + view.performHapticFeedback(constant) + this(it) + } +} + fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f) \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 9bcfcc08..d0178073 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -11,8 +11,4 @@ %d selected - - %d patch available - %d patches available - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5980f3e..51ea7ec2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,13 +2,15 @@ ReVanced Manager Patcher Patches - Integrations CLI Manager ReVanced Manager plugin host Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this. + Copied! + Copy to clipboard + Dashboard Settings Select an app @@ -21,7 +23,6 @@ Import patch bundle Bundle patches Patch bundle - Integrations Selected Not selected @@ -56,19 +57,18 @@ These settings can be changed later. General - General settings - Advanced - Advanced settings + Theme, dynamic color Updates - Updates for ReVanced Manager + Check for updates and view changelogs + Downloads + Downloader plugins and downloaded apps + Import & export + Keystore, patch options and selection + Advanced + API URL, memory limit, debugging + About Open source licenses View all the libraries used to make this application - Downloads - Manage downloaded content - Import & export - Import and export settings - About - About ReVanced Contributors View the contributors of ReVanced @@ -76,8 +76,6 @@ Adapt colors to the wallpaper Theme Choose between light or dark theme - Multi-threaded DEX file writer - Use multiple cores to write DEX files. This is faster, but uses more memory Safeguards Disable version compatibility check The check restricts patches to supported app versions @@ -132,7 +130,6 @@ Search apps… Loading… Downloading patch bundle… - Downloading Integrations… Options OK @@ -154,7 +151,6 @@ Close System Light - Information Dark Appearance Downloaded apps @@ -180,7 +176,7 @@ Memory limits %1$dMB (Normal) - %2$dMB (Large) Patch bundles - Redownload all patch bundles + Force download all patch bundles Reset patch bundles Patching Signing @@ -190,15 +186,12 @@ Patch bundles Delete Refresh - Remote - Local Continue anyways Download another version Download app Download APK file Failed to download patch bundle: %s Failed to load updated patch bundle: %s - Failed to update integrations: %s No patched apps found Tap on the patches to get more information about them %s selected @@ -235,7 +228,7 @@ Applied patches View applied patches Default - Root + Mount Mounted Not mounted Mount @@ -275,6 +268,7 @@ Install App installed Failed to install app: %s + Failed to reinstall app: %s Failed to uninstall app: %s Open Save APK @@ -305,6 +299,7 @@ reorder More + Less Continue Dismiss Do not show this again @@ -317,14 +312,15 @@ Help us improve this application Developer options Options for debugging issues - Name Source URL Successfully updated %s No update available for %s Auto update Automatically update this bundle when ReVanced starts - Bundle type - Choose the type of bundle you want + View patches + Any version + Any package + 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. An update is available @@ -376,6 +372,24 @@ Import local files from your storage, does not automatically update Import remote files from a URL, can automatically update Recommended + + Installation failed + Installation cancelled + Installation blocked + Installation conflict + Installation incompatible + Installation invalid + Not enough storage + Installation timed out + The installation failed due to an unknown reason. Try again? + The installation was cancelled manually. Try again? + The installation was blocked. Review your device security settings and try again. + The installation was prevented by an existing installation of the app. Uninstall the installed app and try again? + The app is incompatible with this device. Use an APK that is supported by this device and try again. + The app is invalid. Uninstall the app and try again? + The app could not be installed due to insufficient storage. Free up some space and try again. + The installation took too long. Try again? + Reinstall Show Debugging About device diff --git a/build.gradle.kts b/build.gradle.kts index 11126717..8ed32e02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,11 @@ plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.devtools) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.about.libraries) apply false - alias(libs.plugins.android.library) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.binary.compatibility.validator) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8504018a..532fc54b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,30 +1,30 @@ [versions] -kotlin = "2.0.10" -ktx = "1.13.1" -material3 = "1.3.0-beta05" -ui-tooling = "1.6.8" -viewmodel-lifecycle = "2.8.4" +ktx = "1.15.0" +material3 = "1.3.1" +ui-tooling = "1.7.5" +viewmodel-lifecycle = "2.8.7" splash-screen = "1.0.1" -compose-activity = "1.9.1" +compose-activity = "1.9.3" preferences-datastore = "1.1.1" -work-runtime = "2.9.1" -compose-bom = "2024.06.00" +work-runtime = "2.10.0" +compose-bom = "2024.11.00" accompanist = "0.34.0" placeholder = "1.1.2" reorderable = "1.5.2" -serialization = "1.7.1" -collection = "0.3.7" +serialization = "1.7.3" +collection = "0.3.8" room-version = "2.6.1" -revanced-patcher = "19.3.1" -revanced-library = "2.2.1" +revanced-patcher = "21.0.0" +revanced-library = "3.0.2" koin-version = "3.5.3" koin-version-compose = "3.5.3" reimagined-navigation = "1.5.0" ktor = "2.3.9" markdown-renderer = "0.22.0" fading-edges = "1.0.4" -android-gradle-plugin = "8.3.2" -dev-tools-ksp-gradle-plugin = "2.0.10-1.0.24" +kotlin = "2.0.21" +android-gradle-plugin = "8.7.2" +dev-tools-gradle-plugin = "2.0.21-1.0.27" about-libraries-gradle-plugin = "11.1.1" binary-compatibility-validator = "0.15.1" coil = "2.6.0" @@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0" skrapeit = "1.2.2" libsu = "5.2.2" scrollbars = "1.0.4" +enumutil = "1.1.0" compose-icons = "1.2.4" kotlin-process = "1.4.1" hidden-api-stub = "4.3.3" @@ -126,6 +127,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = # Scrollbars scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" } +# EnumUtil +enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" } +enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" } + # Reorderable lists reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } @@ -146,6 +151,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-ksp-gradle-plugin" } +devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..dfe2d1c1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ +#Tue Nov 12 21:36:50 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME