From a0b92554e9b8dadd63cf92b7357be3fb8a343091 Mon Sep 17 00:00:00 2001 From: Robert <72943079+CnC-Robert@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:42:10 +0200 Subject: [PATCH] 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 --- .../1.json | 109 +++++++++++- .../java/app/revanced/manager/MainActivity.kt | 41 ++--- .../revanced/manager/ManagerApplication.kt | 14 ++ .../revanced/manager/data/room/AppDatabase.kt | 12 +- .../apps/{ => downloaded}/DownloadedApp.kt | 2 +- .../DownloadedAppDao.kt} | 4 +- .../data/room/apps/installed/AppliedPatch.kt | 34 ++++ .../data/room/apps/installed/InstalledApp.kt | 23 +++ .../room/apps/installed/InstalledAppDao.kt | 40 +++++ .../revanced/manager/di/RepositoryModule.kt | 1 + .../revanced/manager/di/ViewModelModule.kt | 2 + .../repository/DownloadedAppRepository.kt | 4 +- .../repository/InstalledAppRepository.kt | 51 ++++++ .../manager/service/UninstallService.kt | 11 +- .../revanced/manager/ui/component/AppLabel.kt | 2 +- .../manager/ui/component/LoadingIndicator.kt | 24 ++- .../manager/ui/component/SegmentedButton.kt | 68 ++++++++ .../manager/ui/destination/Destination.kt | 9 +- .../manager/ui/screen/AppInfoScreen.kt | 158 ++++++++++++++++++ .../manager/ui/screen/DashboardScreen.kt | 17 +- .../manager/ui/screen/InstalledAppsScreen.kt | 66 +++++++- .../ui/screen/PatchesSelectorScreen.kt | 2 +- .../ui/screen/VersionSelectorScreen.kt | 18 +- .../manager/ui/viewmodel/AppInfoViewModel.kt | 96 +++++++++++ .../ui/viewmodel/DownloadsViewModel.kt | 2 +- .../ui/viewmodel/InstalledAppsViewModel.kt | 33 ++++ .../ui/viewmodel/InstallerViewModel.kt | 14 +- .../ui/viewmodel/PatchesSelectorViewModel.kt | 15 +- .../ui/viewmodel/VersionSelectorViewModel.kt | 18 +- .../main/java/app/revanced/manager/util/PM.kt | 15 +- .../java/app/revanced/manager/util/Util.kt | 10 +- app/src/main/res/values/plurals.xml | 4 + app/src/main/res/values/strings.xml | 12 ++ 33 files changed, 842 insertions(+), 89 deletions(-) rename app/src/main/java/app/revanced/manager/data/room/apps/{ => downloaded}/DownloadedApp.kt (86%) rename app/src/main/java/app/revanced/manager/data/room/apps/{AppDao.kt => downloaded/DownloadedAppDao.kt} (87%) create mode 100644 app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt create mode 100644 app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt create mode 100644 app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt create mode 100644 app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json index 47ff71df..543d7a70 100644 --- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json @@ -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')" ] } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 41275864..8fba9f6d 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -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( diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 07856a59..8f9393d6 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -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() } diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt index 00b770bf..e1a0f81b 100644 --- a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt +++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt @@ -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() diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/DownloadedApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt similarity index 86% rename from app/src/main/java/app/revanced/manager/data/room/apps/DownloadedApp.kt rename to app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt index d0f50bb4..a30063ff 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/DownloadedApp.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/AppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt similarity index 87% rename from app/src/main/java/app/revanced/manager/data/room/apps/AppDao.kt rename to app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt index 868172ee..4f4d9623 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/AppDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt @@ -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> diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt new file mode 100644 index 00000000..6feb04ed --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt new file mode 100644 index 00000000..ad7033dd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt new file mode 100644 index 00000000..71172493 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt @@ -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> + + @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> + + @Transaction + suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List) { + insertApp(installedApp) + insertAppliedPatches(appliedPatches) + } + + @Insert + suspend fun insertApp(installedApp: InstalledApp) + + @Insert + suspend fun insertAppliedPatches(appliedPatches: List) + + @Delete + suspend fun delete(installedApp: InstalledApp) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt index 2265d0b3..37c152e5 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -18,4 +18,5 @@ val repositoryModule = module { singleOf(::PatchBundleRepository) singleOf(::WorkerRepository) singleOf(::DownloadedAppRepository) + singleOf(::InstalledAppRepository) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index b0545375..00ba56cf 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -19,4 +19,6 @@ val viewModelModule = module { viewModelOf(::ImportExportViewModel) viewModelOf(::ContributorViewModel) viewModelOf(::DownloadsViewModel) + viewModelOf(::InstalledAppsViewModel) + viewModelOf(::AppInfoViewModel) } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index 1cb37cbe..7035c6c4 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -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() diff --git a/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt new file mode 100644 index 00000000..99a37ed9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/service/UninstallService.kt b/app/src/main/java/app/revanced/manager/service/UninstallService.kt index 2b954f9b..cefd3528 100644 --- a/app/src/main/java/app/revanced/manager/service/UninstallService.kt +++ b/app/src/main/java/app/revanced/manager/service/UninstallService.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt index 238f3673..9723e58d 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt index e5772789..61e94888 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt @@ -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) + ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt b/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt new file mode 100644 index 00000000..c2dc9463 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt @@ -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() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt index f9b7b9c0..e737ed8c 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt @@ -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 + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt new file mode 100644 index 00000000..74b5c185 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt @@ -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)) } + ) + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index d43a3c1c..33dee138 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -12,16 +12,7 @@ import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.HelpOutline 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.getValue import androidx.compose.runtime.mutableStateOf @@ -35,6 +26,7 @@ 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.AppTopBar import app.revanced.manager.ui.component.bundle.ImportBundleDialog import app.revanced.manager.ui.viewmodel.DashboardViewModel @@ -56,6 +48,7 @@ fun DashboardScreen( vm: DashboardViewModel = getViewModel(), onAppSelectorClick: () -> Unit, onSettingsClick: () -> Unit, + onAppClick: (InstalledApp) -> Unit ) { var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } val pages: Array = DashboardPage.values() @@ -150,7 +143,9 @@ fun DashboardScreen( pageContent = { index -> when (pages[index]) { DashboardPage.DASHBOARD -> { - InstalledAppsScreen() + InstalledAppsScreen( + onAppClick = onAppClick + ) } DashboardPage.BUNDLES -> { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt index ab1a5e4d..82bd6f6b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt @@ -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() } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index ee758b14..722e65e1 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -70,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 ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt index f2b8169a..374cfad6 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -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 + } ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt new file mode 100644 index 00000000..73a0c6b0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt index 24cd51df..4688cf16 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt new file mode 100644 index 00000000..6ed37b55 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt @@ -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() + + init { + viewModelScope.launch { + apps.collectEach { installedApp -> + packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) { + pm.getPackageInfo(installedApp.currentPackageName) + .also { if (it == null) installedAppsRepository.delete(installedApp) } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt index c98128ec..82174d9a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt @@ -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)) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 8090c307..b5c6b670 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -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() val universal = mutableListOf() - 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()) { @@ -158,7 +159,7 @@ class PatchesSelectorViewModel( val set = HashSet() unsupportedVersions.forEach { patch -> - patch.compatiblePackages?.find { it.packageName == selectedApp.packageName } + patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName } ?.let { compatiblePackage -> set.addAll(compatiblePackage.versions) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt index 504f471e..87ca563b 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -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? 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) { diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index f8ea2dad..96710e36 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -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 + ) } } diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index a6b478ac..e8f38056 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -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) - } \ No newline at end of file + } + +suspend fun Flow>.collectEach(block: suspend (T) -> Unit) { + this.collect { iterable -> + iterable.forEach { + block(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 7a5bdd94..697bce97 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -4,4 +4,8 @@ %d Patch %d Patches + + %d applied patch + %d applied patches + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eaf954d7..5517915f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,6 +154,17 @@ Loading… Not installed + App info + Uninstall + Repatch + Installation type + Package name + Original package name + Applied patches + View applied patches + Default + Root + An error occurred Already downloaded @@ -173,6 +184,7 @@ Install App installed Failed to install app: %s + Failed to uninstall app: %s Open Export Apk exported