Compare commits

...

14 Commits

Author SHA1 Message Date
Ax333l f0ab415a6f
Merge branch 'dev' into feat/make-patched-apps-selectable 2023-09-03 13:49:31 +02:00
Patryk Miś ecae587491
build: updates (#85) 2023-08-26 14:03:44 +00:00
Ax333l 4edec8afb4
fix(deps): use correct work-runtime version string 2023-08-26 15:52:40 +02:00
Tyff 0e6b4ed350
feat: more info for the select from application screen (#81) 2023-08-23 20:05:21 +02:00
Pun Butrach 12dc8a2585
ci(release): migrate from `node12` to `node16`
This bump `actions/upload-artifact`@v2 to `actions/upload-artifact`@v3
2023-08-19 16:00:01 +07:00
Robert fe95afca6c
feat: store patched apps (#79)
* feat: store patched apps

* fix: missing string

* feat: save patch selection

* feat: things

* fix: fix broken query

* fix: remove redundant `withContext`

* fix: fix
2023-08-17 17:42:10 +02:00
Palm 0ba1ac6880
ci(release): use correct `vars` context object
why am i so stupid
2023-08-17 19:30:31 +07:00
Palm e37cbf19f2
ci(release): no longer store keystore alias in secrets
fixes an issue where GitHub Actions logs would be censored
2023-08-17 19:13:18 +07:00
Ax333l cd75ac90d9
fix: patches not being reloaded 2023-08-14 18:29:56 +02:00
Ax333l 0520ff23c7
fix: permission error when using installed app 2023-08-12 14:52:34 +02:00
Ax333l 0dc3bed349
feat: patch options UI (#80) 2023-08-12 08:41:22 +00:00
Ax333l 1630668360
feat: switch to the new api (#75) 2023-08-07 09:03:50 +00:00
Ax333l d666ece4a8
chore: bump patcher 2023-08-04 12:55:14 +02:00
Ax333l 4eca2a3b5d
feat: improve bundle dialog UI 2023-08-04 12:46:07 +02:00
66 changed files with 1558 additions and 517 deletions

View File

@ -34,11 +34,11 @@ jobs:
releaseDirectory: ./app/build/outputs/apk/release/
signingKeyBase64: ${{ secrets.TEMP_SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.TEMP_SIGNING_KEYSTORE_PASSWORD }}
alias: ${{ secrets.TEMP_SIGNING_KEY_ALIAS }}
alias: ${{ vars.TEMP_SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.TEMP_SIGNING_KEY_PASSWORD }}
- name: Upload APK
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: manager
path: ${{steps.sign_apk.outputs.signedReleaseFile}}

View File

@ -67,7 +67,7 @@ android {
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = "1.4.8"
composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
}
kotlin {

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "7142188e25ce489eb233aed8fb76e4cc",
"identityHash": "5515d164bc8f713201506d42a02d337f",
"entities": [
{
"tableName": "patch_bundles",
@ -190,12 +190,117 @@
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
}
],
"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, '7142188e25ce489eb233aed8fb76e4cc')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5515d164bc8f713201506d42a02d337f')"
]
}
}

View File

@ -26,7 +26,6 @@
android:name=".ManagerApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:extractNativeLibs="true"
android:largeHeap="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.screen.AppInfoScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen
@ -18,19 +19,14 @@ import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import coil.Coil
import coil.ImageLoader
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.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
@ -42,17 +38,6 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
val scale = this.resources.displayMetrics.density
val pixels = (36 * scale).roundToInt()
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@MainActivity))
}
.build()
)
setContent {
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
@ -77,7 +62,16 @@ class MainActivity : ComponentActivity() {
when (destination) {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) }
)
is Destination.ApplicationInfo -> AppInfoScreen(
onPatchClick = { packageName, patchesSelection ->
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
},
onBackClick = { navController.pop() },
viewModel = getViewModel { parametersOf(destination.installedApp) }
)
is Destination.Settings -> SettingsScreen(
@ -92,8 +86,15 @@ class MainActivity : ComponentActivity() {
is Destination.VersionSelector -> VersionSelectorScreen(
onBackClick = { navController.pop() },
onAppClick = { navController.navigate(Destination.PatchesSelector(it)) },
viewModel = getViewModel { parametersOf(destination.packageName) }
onAppClick = { selectedApp ->
navController.navigate(
Destination.PatchesSelector(
selectedApp,
destination.patchesSelection
)
)
},
viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) }
)
is Destination.PatchesSelector -> PatchesSelectorScreen(
@ -107,7 +108,7 @@ class MainActivity : ComponentActivity() {
)
)
},
vm = getViewModel { parametersOf(destination.selectedApp) }
vm = getViewModel { parametersOf(destination) }
)
is Destination.Installer -> InstallerScreen(

View File

@ -5,8 +5,12 @@ import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@ -36,6 +40,16 @@ class ManagerApplication : Application() {
)
}
val pixels = 512
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
}
.build()
)
scope.launch {
prefs.preload()
}

View File

@ -3,8 +3,11 @@ package app.revanced.manager.data.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.AppDao
import app.revanced.manager.data.room.apps.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
@ -12,12 +15,13 @@ import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlin.random.Random
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1)
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao
abstract fun appDao(): AppDao
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
companion object {
fun generateUid() = Random.Default.nextInt()

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps
package app.revanced.manager.data.room.apps.downloaded
import androidx.room.ColumnInfo
import androidx.room.Entity

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps
package app.revanced.manager.data.room.apps.downloaded
import androidx.room.Dao
import androidx.room.Delete
@ -7,7 +7,7 @@ import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface AppDao {
interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>>

View File

@ -0,0 +1,34 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "applied_patch",
primaryKeys = ["package_name", "bundle", "patch_name"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["bundle"]
)
],
indices = [Index(value = ["bundle"], unique = false)]
)
data class AppliedPatch(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle") val bundle: Int,
@ColumnInfo(name = "patch_name") val patchName: String
) : Parcelable

View File

@ -0,0 +1,23 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
ROOT(R.string.root_install)
}
@Parcelize
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable

View File

@ -0,0 +1,40 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.MapInfo
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
interface InstalledAppDao {
@Query("SELECT * FROM installed_app")
fun getAll(): Flow<List<InstalledApp>>
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
suspend fun get(packageName: String): InstalledApp?
@MapInfo(keyColumn = "bundle", valueColumn = "patch_name")
@Query(
"SELECT bundle, patch_name FROM applied_patch" +
" WHERE package_name = :packageName"
)
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
@Transaction
suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
insertApp(installedApp)
insertAppliedPatches(appliedPatches)
}
@Insert
suspend fun insertApp(installedApp: InstalledApp)
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@ -4,14 +4,13 @@ import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.*
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.network.api.ReVancedAPI
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepository)
singleOf(::ReVancedAPI)
singleOf(::GithubRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem)
singleOf(::NetworkInfo)
singleOf(::PatchBundlePersistenceRepository)
@ -19,4 +18,5 @@ val repositoryModule = module {
singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
}

View File

@ -18,4 +18,6 @@ val viewModelModule = module {
viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
viewModelOf(::AppInfoViewModel)
}

View File

@ -2,20 +2,18 @@ package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.domain.bundles.APIPatchBundle.Companion.toBundleAsset
import app.revanced.manager.domain.repository.Assets
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.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.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
import io.ktor.client.request.url
import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -57,7 +55,11 @@ 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() && VersionInfo(
info.patches.version,
info.integrations.version
) == currentVersion()
) {
return@withContext false
}
@ -94,18 +96,24 @@ class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String)
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedRepository by inject()
private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = api.getAssets().toBundleInfo()
private companion object {
fun Assets.toBundleInfo(): BundleInfo {
val patches = find(ghPatches, ".jar")
val integrations = find(ghIntegrations, ".apk")
return BundleInfo(patches.toBundleAsset(), integrations.toBundleAsset())
override suspend fun getLatestInfo() = coroutineScope {
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
api
.getRelease(repo)
.getOrThrow()
.let {
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
}
}
fun Asset.toBundleAsset() = BundleAsset(version, downloadUrl)
val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
BundleInfo(
patches.await(),
integrations.await()
)
}
}

View File

@ -10,7 +10,7 @@ class PreferencesManager(
val dynamicColor = booleanPreference("dynamic_color", true)
val theme = enumPreference("theme", Theme.SYSTEM)
val api = stringPreference("api_url", "https://releases.revanced.app")
val api = stringPreference("api_url", "https://api.revanced.app")
val allowExperimental = booleanPreference("allow_experimental", false)

View File

@ -1,14 +1,14 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File
class DownloadedAppRepository(
db: AppDatabase
) {
private val dao = db.appDao()
private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()

View File

@ -2,6 +2,7 @@ package app.revanced.manager.domain.repository
import app.revanced.manager.network.service.GithubService
// TODO: delete this when the revanced api adds download count.
class GithubRepository(private val service: GithubService) {
suspend fun getChangelog(repo: String) = service.getChangelog(repo)
}

View File

@ -0,0 +1,51 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.flow.distinctUntilChanged
class InstalledAppRepository(
db: AppDatabase
) {
private val dao = db.installedAppDao()
fun getAll() = dao.getAll().distinctUntilChanged()
suspend fun get(packageName: String) = dao.get(packageName)
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun add(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchesSelection: PatchesSelection
) {
dao.insertApp(
InstalledApp(
currentPackageName = currentPackageName,
originalPackageName = originalPackageName,
version = version,
installType = installType
),
patchesSelection.flatMap { (uid, patches) ->
patches.map { patch ->
AppliedPatch(
packageName = currentPackageName,
bundle = uid,
patchName = patch
)
}
}
)
}
suspend fun delete(installedApp: InstalledApp) {
dao.delete(installedApp)
}
}

View File

@ -1,25 +0,0 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.service.ReVancedService
import app.revanced.manager.network.utils.getOrThrow
class ReVancedRepository(
private val service: ReVancedService,
private val prefs: PreferencesManager
) {
private suspend fun apiUrl() = prefs.api.get()
suspend fun getContributors() = service.getContributors(apiUrl())
suspend fun getAssets() = Assets(service.getAssets(apiUrl()).getOrThrow())
}
class Assets(private val releases: ReVancedReleases): List<Asset> by releases.tools {
fun find(repo: String, file: String) = find { asset ->
asset.name.contains(file) && asset.repository.contains(repo)
} ?: throw MissingAssetException()
}

View File

@ -1,43 +0,0 @@
package app.revanced.manager.network.api
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.revanced.manager.domain.repository.Assets
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.util.*
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import java.io.File
// TODO: merge ReVancedRepository into this class
class ManagerAPI(
private val http: HttpService,
private val revancedRepository: ReVancedRepository
) {
var downloadProgress: Float? by mutableStateOf(null)
var downloadedSize: Long? by mutableStateOf(null)
var totalSize: Long? by mutableStateOf(null)
private suspend fun downloadAsset(asset: Asset, saveLocation: File) {
http.download(saveLocation) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
downloadProgress = null
}
suspend fun downloadManager(location: File) {
val managerAsset = revancedRepository.getAssets().find(ghManager, ".apk")
downloadAsset(managerAsset, location)
}
}
class MissingAssetException : Exception()

View File

@ -0,0 +1,26 @@
package app.revanced.manager.network.api
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedLatestRelease
import app.revanced.manager.network.dto.ReVancedRelease
import app.revanced.manager.network.service.ReVancedService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.network.utils.transform
class ReVancedAPI(
private val service: ReVancedService,
private val prefs: PreferencesManager
) {
private suspend fun apiUrl() = prefs.api.get()
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
suspend fun getRelease(name: String) = service.getRelease(apiUrl(), name).transform { it.release }
companion object Extensions {
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
}
}
class MissingAssetException(type: String) : Exception("No asset with type $type")

View File

@ -4,18 +4,18 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ReVancedRepositories(
@SerialName("repositories") val repositories: List<ReVancedRepository>,
data class ReVancedGitRepositories(
val repositories: List<ReVancedGitRepository>,
)
@Serializable
class ReVancedRepository(
@SerialName("name") val name: String,
@SerialName("contributors") val contributors: List<ReVancedContributor>,
data class ReVancedGitRepository(
val name: String,
val contributors: List<ReVancedContributor>,
)
@Serializable
class ReVancedContributor(
@SerialName("login") val username: String,
data class ReVancedContributor(
val username: String,
@SerialName("avatar_url") val avatarUrl: String,
)

View File

@ -0,0 +1,32 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReVancedLatestRelease(
val release: ReVancedRelease,
)
@Serializable
data class ReVancedRelease(
val metadata: ReVancedReleaseMeta,
val assets: List<Asset>
)
@Serializable
data class ReVancedReleaseMeta(
@SerialName("tag_name") val tag: String,
val name: String,
val draft: Boolean,
val prerelease: Boolean,
@SerialName("created_at") val createdAt: String,
@SerialName("published_at") val publishedAt: String
)
@Serializable
data class Asset(
val name: String,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val contentType: String
)

View File

@ -1,20 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ReVancedReleases(
@SerialName("tools") val tools: List<Asset>,
)
@Serializable
class Asset(
@SerialName("repository") val repository: String,
@SerialName("version") val version: String,
@SerialName("timestamp") val timestamp: String,
@SerialName("name") val name: String,
@SerialName("size") val size: String?,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val content_type: String
)

View File

@ -1,11 +1,8 @@
package app.revanced.manager.network.service
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.dto.ReVancedRepositories
import app.revanced.manager.network.dto.ReVancedLatestRelease
import app.revanced.manager.network.dto.ReVancedGitRepositories
import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.network.utils.getOrThrow
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -13,20 +10,17 @@ import kotlinx.coroutines.withContext
class ReVancedService(
private val client: HttpService,
) {
suspend fun getAssets(api: String): APIResponse<ReVancedReleases> {
return withContext(Dispatchers.IO) {
suspend fun getRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
withContext(Dispatchers.IO) {
client.request {
url("$api/tools")
url("$api/v2/$repo/releases/latest")
}
}
}
suspend fun getContributors(api: String): APIResponse<ReVancedRepositories> {
return withContext(Dispatchers.IO) {
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
withContext(Dispatchers.IO) {
client.request {
url("$api/contributors")
}
}
}
}

View File

@ -30,7 +30,7 @@ object Aligning {
}
file.copyEntriesFromFileAligned(
ZipFile(inputFile),
ZipFile(inputFile, readonly = true),
ZipAligner::getEntryAlignment
)
}

View File

@ -28,7 +28,7 @@ class Session(
PatcherOptions(
inputFile = input,
resourceCacheDirectory = temporary.resolve("aapt-resources").path,
frameworkFolderLocation = frameworkDir,
frameworkDirectory = frameworkDir,
aaptPath = aaptPath,
logger = logger,
)

View File

@ -5,16 +5,17 @@ import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.zip.CRC32
import java.util.zip.Deflater
class ZipFile(file: File) : Closeable {
class ZipFile(file: File, private val readonly: Boolean = false) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
private val filePointer: RandomAccessFile = RandomAccessFile(file, if (readonly) "r" else "rw")
private var CDNeedsRewrite = false
private val compressionLevel = 5
@ -34,6 +35,10 @@ class ZipFile(file: File) : Closeable {
filePointer.seek(0)
}
private fun assertWritable() {
if (readonly) throw IOException("Archive is read-only")
}
private fun findEndRecord(): ZipEndRecord {
//look from end to start since end record is at the end
for (i in filePointer.length() - 1 downTo 0) {
@ -110,6 +115,8 @@ class ZipFile(file: File) : Closeable {
}
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
assertWritable()
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
@ -136,6 +143,8 @@ class ZipFile(file: File) : Closeable {
}
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
assertWritable()
alignment?.let {
//calculate where data would end up
val dataOffset = filePointer.filePointer + entry.LFHSize
@ -162,6 +171,8 @@ class ZipFile(file: File) : Closeable {
}
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
assertWritable()
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates

View File

@ -12,14 +12,15 @@ import java.io.File
class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) {
constructor(bundleJar: File, integrations: File?) : this(
object : Iterable<PatchClass> {
private val bundle = bundleJar.absolutePath.let {
PatchBundle.Dex(
it,
PathClassLoader(it, Patcher::class.java.classLoader)
)
private fun load(): List<PatchClass> {
val path = bundleJar.absolutePath
return PatchBundle.Dex(
path,
PathClassLoader(path, Patcher::class.java.classLoader)
).loadPatches()
}
override fun iterator() = bundle.loadPatches().iterator()
override fun iterator() = load().iterator()
},
integrations
) {

View File

@ -14,7 +14,10 @@ class UninstallService : Service() {
flags: Int,
startId: Int
): Int {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
@ -28,6 +31,9 @@ class UninstallService : Service() {
else -> {
sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
})
}
}
@ -39,6 +45,9 @@ class UninstallService : Service() {
companion object {
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
const val EXTRA_UNINSTALL_STATUS = "EXTRA_UNINSTALL_STATUS"
const val EXTRA_UNINSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
}
}

View File

@ -25,7 +25,7 @@ fun AppLabel(
packageInfo: PackageInfo?,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
defaultText: String = stringResource(R.string.not_installed)
defaultText: String? = stringResource(R.string.not_installed)
) {
val context = LocalContext.current

View File

@ -36,6 +36,13 @@ fun AppScaffold(
fun AppTopBar(
title: String,
onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.Default.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
@ -47,10 +54,7 @@ fun AppTopBar(
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back)
)
backIcon()
}
}
},

View File

@ -12,18 +12,26 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingIndicator(progress: Float? = null, text: String? = null) {
fun LoadingIndicator(
modifier: Modifier = Modifier,
progress: Float? = null,
text: String? = null
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (text != null)
Text(text)
if (progress == null) {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
} else {
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp))
}
text?.let { Text(text) }
progress?.let {
CircularProgressIndicator(
progress = progress,
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
)
} ?:
CircularProgressIndicator(
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
)
}
}

View File

@ -0,0 +1,68 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
/**
* Credits to [Vendetta](https://github.com/vendetta-mod)
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SegmentedButton(
icon: Any,
iconDescription: String? = null,
text: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
modifier = Modifier
.clickable(onClick = onClick)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.weight(1f)
.padding(vertical = 20.dp)
) {
when (icon) {
is ImageVector -> {
Icon(
imageVector = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
is Painter -> {
Icon(
painter = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
}
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
modifier = Modifier.basicMarquee()
)
}
}

View File

@ -0,0 +1,51 @@
package app.revanced.manager.ui.component
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
fun TextInputDialog(
initial: String,
title: String,
onDismissRequest: () -> Unit,
onConfirm: (String) -> Unit,
validator: (String) -> Boolean = String::isNotEmpty,
) {
val (value, setValue) = rememberSaveable(initial) {
mutableStateOf(initial)
}
val valid = remember(value, validator) {
validator(value)
}
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = { onConfirm(value) },
enabled = valid
) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
title = {
Text(title)
},
text = {
TextField(value = value, onValueChange = setValue)
}
)
}

View File

@ -1,5 +1,7 @@
package app.revanced.manager.ui.component.bundle
import android.webkit.URLUtil
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
@ -12,23 +14,27 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.TextInputDialog
@Composable
fun BaseBundleDialog(
modifier: Modifier = Modifier,
isDefault: Boolean,
name: String,
onNameChange: (String) -> Unit = {},
onNameChange: ((String) -> Unit)? = null,
remoteUrl: String?,
onRemoteUrlChange: (String) -> Unit = {},
onRemoteUrlChange: ((String) -> Unit)? = null,
patchCount: Int,
version: String?,
autoUpdate: Boolean,
@ -40,79 +46,108 @@ fun BaseBundleDialog(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.then(modifier)
) {
Column(
modifier = Modifier.padding(
start = 24.dp,
top = 16.dp,
end = 24.dp,
)
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = name,
onValueChange = onNameChange,
label = {
Text(stringResource(R.string.bundle_input_name))
}
)
remoteUrl?.takeUnless { isDefault }?.let {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = it,
onValueChange = onRemoteUrlChange,
label = {
Text(stringResource(R.string.bundle_input_source_url))
}
)
}
extraFields()
}
Column(
Modifier.padding(
.padding(
start = 8.dp,
top = 8.dp,
end = 4.dp,
)
) Info@{
if (remoteUrl != null) {
BundleListItem(
headlineText = stringResource(R.string.automatically_update),
supportingText = stringResource(R.string.automatically_update_description),
trailingContent = {
Switch(
checked = autoUpdate,
onCheckedChange = onAutoUpdateChange
)
.then(modifier)
) {
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
}
)
}
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
}
)
remoteUrl?.takeUnless { isDefault }?.let { url ->
var showUrlInputDialog by rememberSaveable {
mutableStateOf(false)
}
if (showUrlInputDialog) {
TextInputDialog(
initial = url,
title = stringResource(R.string.bundle_input_source_url),
onDismissRequest = { showUrlInputDialog = false },
onConfirm = {
showUrlInputDialog = false
onRemoteUrlChange?.invoke(it)
},
validator = {
if (it.isEmpty()) return@TextInputDialog false
URLUtil.isValidUrl(it)
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_type),
supportingText = stringResource(R.string.bundle_type_description)
) {
FilledTonalButton(
onClick = onBundleTypeClick,
content = {
if (remoteUrl == null) {
Text(stringResource(R.string.local))
} else {
Text(stringResource(R.string.remote))
}
}
)
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.automatically_update),
supportingText = stringResource(R.string.automatically_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))
}
}
)
}
if (version == null && patchCount < 1) return@Info
if (version != null || patchCount > 0) {
Text(
text = stringResource(R.string.information),
modifier = Modifier.padding(
@ -122,7 +157,9 @@ fun BaseBundleDialog(
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
if (patchCount > 0) {
BundleListItem(
headlineText = stringResource(R.string.patches),
supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
@ -138,12 +175,12 @@ fun BaseBundleDialog(
}
}
)
}
if (version == null) return@Info
version?.let {
BundleListItem(
headlineText = stringResource(R.string.version),
supportingText = version,
supportingText = it,
)
}
}

View File

@ -67,7 +67,7 @@ fun BundleInformationDialog(
Scaffold(
topBar = {
BundleTopBar(
title = stringResource(R.string.bundle_information),
title = bundle.name,
onBackClick = onDismissRequest,
onBackIcon = {
Icon(

View File

@ -4,9 +4,11 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun BundleListItem(
modifier: Modifier = Modifier,
headlineText: String,
supportingText: String = "",
trailingContent: @Composable (() -> Unit)? = null,
@ -26,5 +28,6 @@ fun BundleListItem(
)
},
trailingContent = trailingContent,
modifier = modifier
)
}

View File

@ -1,10 +1,9 @@
package app.revanced.manager.ui.component.bundle
import android.net.Uri
import android.webkit.URLUtil
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
@ -12,7 +11,6 @@ import androidx.compose.material.icons.filled.Topic
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -46,17 +44,9 @@ fun ImportBundleDialog(
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
val patchBundleText = patchBundle?.toString().orEmpty()
val integrationText = integrations?.toString().orEmpty()
val inputsAreValid by remember {
derivedStateOf {
val nameSize = name.length
when {
nameSize !in 1..19 -> false
isLocal -> patchBundle != null
else -> remoteUrl.isNotEmpty() && URLUtil.isValidUrl(remoteUrl)
}
name.isNotEmpty() && if (isLocal) patchBundle != null else remoteUrl.isNotEmpty()
}
}
@ -64,10 +54,17 @@ fun ImportBundleDialog(
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { patchBundle = it }
}
fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE)
}
val integrationsActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it }
}
fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE)
}
Dialog(
onDismissRequest = onDismissRequest,
@ -123,53 +120,43 @@ fun ImportBundleDialog(
onPatchesClick = {},
onBundleTypeClick = { isLocal = !isLocal },
) {
if (isLocal) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = patchBundleText,
onValueChange = {},
label = {
Text("Patches Source File")
},
trailingIcon = {
IconButton(
onClick = {
patchActivityLauncher.launch(JAR_MIMETYPE)
}
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
}
if (!isLocal) return@BaseBundleDialog
BundleListItem(
headlineText = stringResource(R.string.patch_bundle_field),
supportingText = stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set),
trailingContent = {
IconButton(
onClick = ::launchPatchActivity
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
}
)
},
modifier = Modifier.clickable {
launchPatchActivity()
}
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = integrationText,
onValueChange = {},
label = {
Text("Integrations Source File")
},
trailingIcon = {
IconButton(
onClick = {
integrationsActivityLauncher.launch(APK_MIMETYPE)
}
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
}
BundleListItem(
headlineText = stringResource(R.string.integrations_field),
supportingText = 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()
}
)
}
}
}

View File

@ -1,38 +1,71 @@
package app.revanced.manager.ui.component.patches
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.Button
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.util.toast
import app.revanced.patcher.patch.PatchOption
import org.koin.compose.rememberKoinInject
/**
* [Composable] functions do not support function references, so we have to use composable lambdas instead.
*/
private typealias OptionField = @Composable (Any?, (Any?) -> Unit) -> Unit
// Composable functions do not support function references, so we have to use composable lambdas instead.
private typealias OptionImpl = @Composable (Option, Any?, (Any?) -> Unit) -> Unit
private val StringField: OptionField = { value, setValue ->
val fs: FileSystem = rememberKoinInject()
@Composable
private fun OptionListItem(
option: Option,
onClick: () -> Unit,
trailingContent: @Composable () -> Unit
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
headlineContent = { Text(option.title) },
supportingContent = { Text(option.description) },
trailingContent = trailingContent
)
}
@Composable
private fun StringOptionDialog(
name: String,
value: String?,
onSubmit: (String) -> Unit,
onDismissRequest: () -> Unit
) {
var showFileDialog by rememberSaveable { mutableStateOf(false) }
var fieldValue by rememberSaveable(value) {
mutableStateOf(value.orEmpty())
}
val fs: FileSystem = rememberKoinInject()
val (contract, permissionName) = fs.permissionContract()
val permissionLauncher = rememberLauncherForActivityResult(contract = contract) {
showFileDialog = it
}
val current = value as? String
if (showFileDialog) {
PathSelectorDialog(
@ -40,45 +73,133 @@ private val StringField: OptionField = { value, setValue ->
) {
showFileDialog = false
it?.let { path ->
setValue(path.toString())
fieldValue = path.toString()
}
}
}
Column {
TextField(value = current ?: "", onValueChange = setValue)
Button(onClick = {
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(name) },
text = {
OutlinedTextField(
value = fieldValue,
onValueChange = { fieldValue = it },
placeholder = {
Text(stringResource(R.string.string_option_placeholder))
},
trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
IconButton(
onClick = { showDropdownMenu = true }
) {
Icon(
Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.string_option_menu_description)
)
}
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Outlined.Folder, null)
},
text = {
Text(stringResource(R.string.path_selector))
},
onClick = {
showDropdownMenu = false
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
}
}
)
}
}
)
},
confirmButton = {
TextButton(onClick = { onSubmit(fieldValue) }) {
Text(stringResource(R.string.save))
}
}) {
Icon(Icons.Filled.FileOpen, null)
Text("Select file or folder")
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
)
}
private val StringOption: OptionImpl = { option, value, setValue ->
var showInputDialog by rememberSaveable { mutableStateOf(false) }
fun showInputDialog() {
showInputDialog = true
}
fun dismissInputDialog() {
showInputDialog = false
}
if (showInputDialog) {
StringOptionDialog(
name = option.title,
value = value as? String,
onSubmit = {
dismissInputDialog()
setValue(it)
},
onDismissRequest = ::dismissInputDialog
)
}
OptionListItem(
option = option,
onClick = ::showInputDialog
) {
IconButton(onClick = ::showInputDialog) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.string_option_icon_description)
)
}
}
}
private val BooleanField: OptionField = { value, setValue ->
val current = value as? Boolean
Switch(checked = current ?: false, onCheckedChange = setValue)
private val BooleanOption: OptionImpl = { option, value, setValue ->
val current = (value as? Boolean) ?: false
OptionListItem(
option = option,
onClick = { setValue(!current) }
) {
Switch(checked = current, onCheckedChange = setValue)
}
}
private val UnknownField: OptionField = { _, _ ->
Text("This type has not been implemented")
private val UnknownOption: OptionImpl = { option, _, _ ->
val context = LocalContext.current
OptionListItem(
option = option,
onClick = { context.toast("Unknown type: ${option.type.name}") },
trailingContent = {})
}
@Composable
fun OptionField(option: Option, value: Any?, setValue: (Any?) -> Unit) {
fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) {
when (option.type) {
// These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringField
PatchOption.BooleanOption::class.java -> BooleanField
else -> UnknownField
PatchOption.StringOption::class.java -> StringOption
PatchOption.BooleanOption::class.java -> BooleanOption
else -> UnknownOption
}
}
implementation(value, setValue)
implementation(option, value, setValue)
}

View File

@ -2,18 +2,20 @@ package app.revanced.manager.ui.component.patches
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.DocumentScanner
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.InsertDriveFile
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.ListItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -21,15 +23,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.util.saver.PathSaver
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.isReadable
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
@ -40,14 +45,8 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
val notAtRootDir = remember(currentDirectory) {
currentDirectory != root
}
val everything = remember(currentDirectory) {
currentDirectory.listDirectoryEntries()
}
val directories = remember(everything) {
everything.filter { it.isDirectory() }
}
val files = remember(everything) {
everything.filter { it.isRegularFile() }
val (directories, files) = remember(currentDirectory) {
currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
}
Dialog(
@ -60,51 +59,78 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.select_file),
onBackClick = { onSelect(null) }
title = stringResource(R.string.path_selector),
onBackClick = { onSelect(null) },
backIcon = {
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.close))
}
)
}
},
) { paddingValues ->
BackHandler(enabled = notAtRootDir) {
currentDirectory = currentDirectory.parent
}
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
Text(text = currentDirectory.toString())
Row(
modifier = Modifier.clickable { onSelect(currentDirectory) }
) {
Text("(Use this directory)")
item(key = "current") {
PathItem(
onClick = { onSelect(currentDirectory) },
icon = Icons.Outlined.Folder,
name = currentDirectory.toString()
)
}
if (notAtRootDir) {
Row(
modifier = Modifier.clickable { currentDirectory = currentDirectory.parent }
) {
Text("Previous directory")
item(key = "parent") {
PathItem(
onClick = { currentDirectory = currentDirectory.parent },
icon = Icons.Outlined.ArrowBack,
name = stringResource(R.string.path_selector_parent_dir)
)
}
}
directories.forEach {
Row(
modifier = Modifier.clickable { currentDirectory = it }
) {
Icon(Icons.Filled.Folder, null)
Text(text = it.name)
if (directories.isNotEmpty()) {
item(key = "dirs_header") {
GroupHeader(title = stringResource(R.string.path_selector_dirs))
}
}
files.forEach {
Row(
modifier = Modifier.clickable { onSelect(it) }
) {
Icon(Icons.Filled.FileOpen, null)
Text(text = it.name)
items(directories, key = { it.absolutePathString() }) {
PathItem(
onClick = { currentDirectory = it },
icon = Icons.Outlined.Folder,
name = it.name
)
}
if (files.isNotEmpty()) {
item(key = "files_header") {
GroupHeader(title = stringResource(R.string.path_selector_files))
}
}
items(files, key = { it.absolutePathString() }) {
PathItem(
onClick = { onSelect(it) },
icon = Icons.Outlined.InsertDriveFile,
name = it.name
)
}
}
}
}
}
@Composable
private fun PathItem(
onClick: () -> Unit,
icon: ImageVector,
name: String
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
headlineContent = { Text(name) },
leadingContent = { Icon(icon, contentDescription = null) }
)
}

View File

@ -1,6 +1,7 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
@ -12,6 +13,9 @@ sealed interface Destination : Parcelable {
@Parcelize
object Dashboard : Destination
@Parcelize
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize
object AppSelector : Destination
@ -19,11 +23,12 @@ sealed interface Destination : Parcelable {
object Settings : Destination
@Parcelize
data class VersionSelector(val packageName: String) : Destination
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize
data class PatchesSelector(val selectedApp: SelectedApp) : Destination
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
}

View File

@ -0,0 +1,158 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowRight
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api
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.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.viewmodel.AppInfoViewModel
import app.revanced.manager.util.PatchesSelection
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppInfoScreen(
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
onBackClick: () -> Unit,
viewModel: AppInfoViewModel
) {
SideEffect {
viewModel.onBackClick = onBackClick
}
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppIcon(
viewModel.appInfo,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.padding(bottom = 5.dp)
)
AppLabel(
viewModel.appInfo,
style = MaterialTheme.typography.titleLarge,
defaultText = null
)
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(24.dp))
) {
SegmentedButton(
icon = Icons.Outlined.OpenInNew,
text = stringResource(R.string.open_app),
onClick = viewModel::launch
)
SegmentedButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall),
onClick = viewModel::uninstall
)
SegmentedButton(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
viewModel.appliedPatches?.let {
onPatchClick(viewModel.installedApp.originalPackageName, it)
}
}
)
}
Column(
modifier = Modifier.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.applied_patches)) },
supportingContent = {
Text(
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
pluralStringResource(
id = R.plurals.applied_patches,
it,
it
)
}
)
},
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
)
ListItem(
headlineContent = { Text(stringResource(R.string.package_name)) },
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
)
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
ListItem(
headlineContent = { Text(stringResource(R.string.original_package_name)) },
supportingContent = { Text(viewModel.installedApp.originalPackageName) }
)
}
ListItem(
headlineContent = { Text(stringResource(R.string.install_type)) },
supportingContent = { Text(stringResource(viewModel.installedApp.installType.stringResource)) }
)
}
}
}
}

View File

@ -49,7 +49,11 @@ fun AppSelectorScreen(
val pickApkLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { apkUri ->
vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(context.getString(R.string.failed_to_load_apk))
vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(
context.getString(
R.string.failed_to_load_apk
)
)
}
}
@ -85,28 +89,62 @@ fun AppSelectorScreen(
}
},
content = {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
if (appList.isNotEmpty()) {
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(
items = filteredAppList,
key = { it.packageName }
) { app ->
ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) },
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
leadingContent = {
AppIcon(
app.packageInfo,
null,
Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) },
trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } }
trailingContent = app.patches?.let {
{
Text(
pluralStringResource(
R.plurals.patches_count,
it,
it
)
)
}
}
)
}
} else {
item { LoadingIndicator() }
}
} else {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = stringResource(R.string.search),
modifier = Modifier.size(64.dp)
)
Text(
text = stringResource(R.string.type_anything),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
)
}
@ -146,7 +184,10 @@ fun AppSelectorScreen(
)
}
},
headlineContent = { Text(stringResource(R.string.select_from_storage)) }
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
supportingContent = {
Text(stringResource(R.string.select_from_storage_description))
}
)
Divider()
}
@ -162,7 +203,17 @@ fun AppSelectorScreen(
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) },
trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } }
trailingContent = app.patches?.let {
{
Text(
pluralStringResource(
R.plurals.patches_count,
it,
it
)
)
}
}
)
}

View File

@ -15,16 +15,7 @@ import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -41,6 +32,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar
@ -64,6 +56,7 @@ fun DashboardScreen(
vm: DashboardViewModel = getViewModel(),
onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
) {
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
@ -195,7 +188,9 @@ fun DashboardScreen(
pageContent = { index ->
when (pages[index]) {
DashboardPage.DASHBOARD -> {
InstalledAppsScreen()
InstalledAppsScreen(
onAppClick = onAppClick
)
}
DashboardPage.BUNDLES -> {

View File

@ -1,21 +1,73 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.InstalledApp
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
import org.koin.androidx.compose.getViewModel
@Composable
fun InstalledAppsScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
fun InstalledAppsScreen(
onAppClick: (InstalledApp) -> Unit,
viewModel: InstalledAppsViewModel = getViewModel()
) {
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
) {
installedApps?.let { installedApps ->
if (installedApps.isNotEmpty()) {
items(
installedApps,
key = { it.currentPackageName }
) { installedApp ->
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
ListItem(
modifier = Modifier.clickable { onAppClick(installedApp) },
leadingContent = {
AppIcon(
packageInfo,
contentDescription = null,
Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(packageInfo, defaultText = null) },
supportingContent = { Text(installedApp.currentPackageName) }
)
}
}
} else {
item {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
}
} ?: item { LoadingIndicator() }
}
}

View File

@ -13,15 +13,13 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
@ -49,7 +47,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.patches.OptionField
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
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
@ -72,7 +70,7 @@ fun PatchesSelectorScreen(
if (vm.compatibleVersions.isNotEmpty())
UnsupportedDialog(
appVersion = vm.selectedApp.version,
appVersion = vm.input.selectedApp.version,
supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs
)
@ -82,8 +80,8 @@ fun PatchesSelectorScreen(
onDismissRequest = vm::dismissDialogs,
patch = patch,
values = vm.getOptions(bundle, patch),
set = { key, value -> vm.setOption(bundle, patch, key, value) },
unset = { vm.unsetOption(bundle, patch, it) }
reset = { vm.resetOptions(bundle, patch) },
set = { key, value -> vm.setOption(bundle, patch, key, value) }
)
}
@ -336,7 +334,7 @@ fun UnsupportedDialog(
fun OptionsDialog(
patch: PatchInfo,
values: Map<String, Any?>?,
unset: (String) -> Unit,
reset: () -> Unit,
set: (String, Any?) -> Unit,
onDismissRequest: () -> Unit,
) = Dialog(
@ -350,36 +348,26 @@ fun OptionsDialog(
topBar = {
AppTopBar(
title = patch.name,
onBackClick = onDismissRequest
onBackClick = onDismissRequest,
actions = {
IconButton(onClick = reset) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
patch.options?.forEach {
ListItem(
headlineContent = { Text(it.title) },
supportingContent = { Text(it.description) },
overlineContent = {
Button(onClick = { unset(it.key) }) {
Text("reset")
}
},
trailingContent = {
val key = it.key
val value =
if (values == null || !values.contains(key)) it.defaultValue else values[key]
if (patch.options == null) return@LazyColumn
OptionField(option = it, value = value, setValue = { set(key, it) })
}
)
}
items(patch.options, key = { it.key }) { option ->
val key = option.key
val value =
if (values == null || !values.contains(key)) option.defaultValue else values[key]
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.apply))
OptionItem(option = option, value = value, setValue = { set(key, it) })
}
}
}

View File

@ -27,6 +27,7 @@ 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.alpha
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -89,7 +90,7 @@ fun VersionSelectorScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
viewModel.installedApp?.let { packageInfo ->
viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
SelectedApp.Installed(
packageName = viewModel.packageName,
version = packageInfo.versionName
@ -98,7 +99,8 @@ fun VersionSelectorScreen(
selectedApp = it,
selected = selectedVersion == it,
onClick = { selectedVersion = it },
patchCount = supportedVersions[it.version]
patchCount = supportedVersions[it.version],
alreadyPatched = alreadyPatched
)
}
}
@ -132,14 +134,13 @@ fun VersionSelectorScreen(
}
}
const val alreadyPatched = false
@Composable
fun SelectedAppItem(
selectedApp: SelectedApp,
selected: Boolean,
onClick: () -> Unit,
patchCount: Int?
patchCount: Int?,
alreadyPatched: Boolean = false
) {
ListItem(
leadingContent = { RadioButton(selected, null) },
@ -161,6 +162,11 @@ fun SelectedAppItem(
trailingContent = patchCount?.let { {
Text(pluralStringResource(R.plurals.patches_count, it, it))
} },
modifier = Modifier.clickable(onClick = onClick)
modifier = Modifier
.clickable(enabled = !alreadyPatched, onClick = onClick)
.run {
if (alreadyPatched) alpha(0.5f)
else this
}
)
}

View File

@ -56,7 +56,7 @@ fun UpdateProgressScreen(
), style = MaterialTheme.typography.headlineMedium
)
LinearProgressIndicator(
progress = vm.downloadProgress / 100f,
progress = vm.downloadProgress,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
@ -66,7 +66,7 @@ fun UpdateProgressScreen(
vm.totalSize.div(
1000000
)
} MB (${vm.downloadProgress.toInt()}%)" else stringResource(R.string.installing_message),
} MB (${vm.downloadProgress.times(100).toInt()}%)" else stringResource(R.string.installing_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally),

View File

@ -0,0 +1,96 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AppInfoViewModel(
val installedApp: InstalledApp
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
lateinit var onBackClick: () -> Unit
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchesSelection? by mutableStateOf(null)
fun launch() = pm.launch(installedApp.currentPackageName)
fun uninstall() {
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
InstallType.ROOT -> TODO()
}
}
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UninstallService.APP_UNINSTALL_ACTION -> {
val extraStatus = intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedAppRepository.delete(installedApp)
withContext(Dispatchers.Main) { onBackClick() }
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
app.registerReceiver(
uninstallBroadcastReceiver,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION)
)
}
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(uninstallBroadcastReceiver)
}
}

View File

@ -3,22 +3,21 @@ package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedGitRepository
import app.revanced.manager.network.utils.getOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ContributorViewModel(private val repository: ReVancedRepository): ViewModel() {
val repositories = mutableStateListOf<app.revanced.manager.network.dto.ReVancedRepository>()
class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
val repositories = mutableStateListOf<ReVancedGitRepository>()
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val repos = repository.getContributors().getOrNull()?.repositories
withContext(Dispatchers.Main) {
if (repos != null) { repositories.addAll(repos) }
}
}
withContext(Dispatchers.IO) { reVancedAPI.getContributors().getOrNull() }?.let(
repositories::addAll
)
}
}
}

View File

@ -2,7 +2,7 @@ package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.util.mutableStateSetOf

View File

@ -0,0 +1,33 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
import app.revanced.manager.util.collectEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class InstalledAppsViewModel(
private val installedAppsRepository: InstalledAppRepository,
private val pm: PM
) : ViewModel() {
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
val packageInfoMap = mutableStateMapOf<String, PackageInfo?>()
init {
viewModelScope.launch {
apps.collectEach { installedApp ->
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
.also { if (it == null) installedAppsRepository.delete(installedApp) }
}
}
}
}
}

View File

@ -18,8 +18,10 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker
@ -50,6 +52,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
private val app: Application by inject()
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
private val installedAppReceiver: InstalledAppRepository by inject()
val packageName: String = input.selectedApp.packageName
private val outputFile = File(app.cacheDir, "output.apk")
@ -113,6 +116,15 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
app.toast(app.getString(R.string.install_app_success))
installedPackageName =
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
viewModelScope.launch {
installedAppReceiver.add(
installedPackageName!!,
packageName,
input.selectedApp.version,
InstallType.DEFAULT,
input.selectedPatches
)
}
} else {
app.toast(app.getString(R.string.install_app_fail, extra))
}

View File

@ -17,7 +17,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet
@ -37,7 +37,7 @@ import org.koin.core.component.get
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(
val selectedApp: SelectedApp
val input: Destination.PatchesSelector
) : ViewModel(), KoinComponent {
private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = get()
@ -54,10 +54,10 @@ class PatchesSelectorViewModel(
val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach {
bundle.patches.filter { it.compatibleWith(input.selectedApp.packageName) }.forEach {
val targetList = when {
it.compatiblePackages == null -> universal
it.supportsVersion(selectedApp.version) -> supported
it.supportsVersion(input.selectedApp.version) -> supported
else -> unsupported
}
@ -75,7 +75,8 @@ class PatchesSelectorViewModel(
viewModelScope.launch(Dispatchers.Default) {
val bundles = bundlesFlow.first()
val filteredSelection =
selectionRepository.getSelection(selectedApp.packageName)
(input.patchesSelection
?: selectionRepository.getSelection(input.selectedApp.packageName))
.mapValues { (uid, patches) ->
// Filter out patches that don't exist.
val filteredPatches = bundles.singleOrNull { it.uid == uid }
@ -125,7 +126,7 @@ class PatchesSelectorViewModel(
suspend fun getAndSaveSelection(): PatchesSelection =
selectedPatches.also {
withContext(Dispatchers.Default) {
selectionRepository.updateSelection(selectedApp.packageName, it)
selectionRepository.updateSelection(input.selectedApp.packageName, it)
}
}.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental.get()) {
@ -145,8 +146,8 @@ class PatchesSelectorViewModel(
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
}
fun unsetOption(bundle: Int, patch: PatchInfo, key: String) {
patchOptions[bundle]?.get(patch.name)?.remove(key)
fun resetOptions(bundle: Int, patch: PatchInfo) {
patchOptions[bundle]?.remove(patch.name)
}
fun dismissDialogs() {
@ -158,7 +159,7 @@ class PatchesSelectorViewModel(
val set = HashSet<String>()
unsupportedVersions.forEach { patch ->
patch.compatiblePackages?.find { it.packageName == selectedApp.packageName }
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
?.let { compatiblePackage ->
set.addAll(compatiblePackage.versions)
}

View File

@ -8,24 +8,36 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import app.revanced.manager.util.PM
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import kotlinx.coroutines.withContext
import java.io.File
class UpdateProgressViewModel(
app: Application,
private val managerAPI: ManagerAPI,
private val reVancedAPI: ReVancedAPI,
private val http: HttpService,
private val pm: PM
) : ViewModel() {
var downloadedSize by mutableStateOf(0L)
private set
var totalSize by mutableStateOf(0L)
private set
val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
val downloadProgress by derivedStateOf { managerAPI.downloadProgress?.times(100) ?: 0f }
val downloadedSize by derivedStateOf { managerAPI.downloadedSize ?: 0L }
val totalSize by derivedStateOf { managerAPI.totalSize ?: 0L }
val isInstalling by derivedStateOf { downloadProgress >= 100 }
downloadedSize.toFloat() / totalSize.toFloat()
}
val isInstalling by derivedStateOf { downloadProgress >= 1 }
var finished by mutableStateOf(false)
private set
@ -33,7 +45,18 @@ class UpdateProgressViewModel(
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download manager") {
withContext(Dispatchers.IO) {
managerAPI.downloadManager(location)
val asset = reVancedAPI
.getRelease("revanced-manager")
.getOrThrow()
.findAssetByType(APK_MIMETYPE)
http.download(location) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
}
finished = true
}

View File

@ -7,7 +7,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.downloader.APKMirror
import app.revanced.manager.network.downloader.AppDownloader
@ -17,6 +19,7 @@ import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -28,11 +31,12 @@ class VersionSelectorViewModel(
val packageName: String
) : ViewModel(), KoinComponent {
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val pm: PM by inject()
private val appDownloader: AppDownloader = APKMirror()
var installedApp: PackageInfo? by mutableStateOf(null)
var installedApp: Pair<PackageInfo, Boolean>? by mutableStateOf(null)
private set
var isLoading by mutableStateOf(true)
private set
@ -67,7 +71,17 @@ class VersionSelectorViewModel(
init {
viewModelScope.launch(Dispatchers.Main) {
installedApp = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val alreadyPatched = async(Dispatchers.IO) {
installedAppRepository.get(packageName)
?.let { it.installType == InstallType.DEFAULT }
?: false
}
installedApp =
packageInfo.await()?.let {
it to alreadyPatched.await()
}
}
viewModelScope.launch(Dispatchers.IO) {

View File

@ -53,21 +53,18 @@ class PM(
.eachCount()
compatiblePackages.keys.map { pkg ->
try {
val packageInfo = app.packageManager.getPackageInfo(pkg, 0)
getPackageInfo(pkg)?.let { packageInfo ->
AppInfo(
pkg,
compatiblePackages[pkg],
packageInfo,
File(packageInfo.applicationInfo.sourceDir)
)
} catch (e: NameNotFoundException) {
AppInfo(
pkg,
compatiblePackages[pkg],
null
)
}
} ?: AppInfo(
pkg,
compatiblePackages[pkg],
null
)
}
}

View File

@ -89,4 +89,12 @@ val Color.hexCode: String
val g: Int = (green * 255).toInt()
val b: Int = (blue * 255).toInt()
return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a)
}
}
suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
this.collect { iterable ->
iterable.forEach {
block(it)
}
}
}

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="patches_count">
<item quantity="one">%d Patch</item>
<item quantity="other">%d Patches</item>
<item quantity="one">%d patch</item>
<item quantity="other">%d patches</item>
</plurals>
<plurals name="applied_patches">
<item quantity="one">%d applied patch</item>
<item quantity="other">%d applied patches</item>
</plurals>
</resources>

View File

@ -13,8 +13,13 @@
<string name="import_">Import</string>
<string name="import_bundle">Import patch bundle</string>
<string name="bundle_information">Bundle information</string>
<string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string>
<string name="integrations_field">Integrations</string>
<string name="file_field_set">Provided</string>
<string name="file_field_not_set">Not provided</string>
<string name="field_not_set">Not set</string>
<string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string>
@ -85,8 +90,11 @@
<string name="options">Options</string>
<string name="ok">OK</string>
<string name="reset">Reset</string>
<string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</string>
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
<string name="type_anything">Type anything to continue</string>
<string name="search">Search</string>
<string name="apply">Apply</string>
<string name="help">Help</string>
@ -149,10 +157,28 @@
<string name="loading">Loading…</string>
<string name="not_installed">Not installed</string>
<string name="app_info">App info</string>
<string name="uninstall">Uninstall</string>
<string name="repatch">Repatch</string>
<string name="install_type">Installation type</string>
<string name="package_name">Package name</string>
<string name="original_package_name">Original package name</string>
<string name="applied_patches">Applied patches</string>
<string name="view_applied_patches">View applied patches</string>
<string name="default_install">Default</string>
<string name="root_install">Root</string>
<string name="error_occurred">An error occurred</string>
<string name="already_downloaded">Already downloaded</string>
<string name="select_file">Select file</string>
<string name="string_option_icon_description">Edit</string>
<string name="string_option_menu_description">More options</string>
<string name="string_option_placeholder">Value</string>
<string name="path_selector">Select from storage</string>
<string name="path_selector_parent_dir">Previous directory</string>
<string name="path_selector_dirs">Directories</string>
<string name="path_selector_files">Files</string>
<string name="show_password_field">Show password</string>
<string name="hide_password_field">Hide password</string>
@ -161,6 +187,7 @@
<string name="install_app">Install</string>
<string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string>
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
<string name="open_app">Open</string>
<string name="export_app">Export</string>
<string name="export_app_success">Apk exported</string>

View File

@ -5,23 +5,23 @@ splash-screen = "1.0.1"
compose-activity = "1.7.2"
paging = "3.1.1"
preferences-datastore = "1.0.0"
work-runtime = "2.8.1ō"
work-runtime = "2.8.1"
compose-bom = "2023.06.01"
accompanist = "0.30.1"
serialization = "1.5.1"
serialization = "1.6.0"
collection = "0.3.5"
room-version = "2.5.2"
patcher = "11.0.4"
apksign = "8.0.2"
bcpkix-jdk18on = "1.75"
patcher = "12.1.1"
apksign = "8.1.1"
bcpkix-jdk18on = "1.76"
koin-version = "3.4.2"
koin-version-compose = "3.4.5"
reimagined-navigation = "1.4.0"
ktor = "2.3.2"
markdown = "0.4.1"
androidGradlePlugin = "8.0.2"
kotlinGradlePlugin = "1.8.22"
devToolsGradlePlugin = "1.8.22-1.0.11"
androidGradlePlugin = "8.1.1"
kotlinGradlePlugin = "1.9.0"
devToolsGradlePlugin = "1.9.0-1.0.12"
aboutLibrariesGradlePlugin = "10.8.2"
coil = "2.4.0"
app-icon-loader-coil = "1.5.0"
@ -102,4 +102,4 @@ markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "devToolsGradlePlugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibrariesGradlePlugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibrariesGradlePlugin" }

Binary file not shown.

View File

@ -1,8 +1,7 @@
#Wed Jul 12 20:30:33 ICT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum