diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 051de336bf..32e79fb7d1 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -3,12 +3,14 @@ name: Build pull request on: workflow_dispatch: pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - dev jobs: release: name: Build + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Checkout diff --git a/app/build.gradle.kts b/app/build.gradle.kts index def49f0312..2ab1d82c48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,6 @@ import com.mikepenz.aboutlibraries.plugin.DuplicateMode import com.mikepenz.aboutlibraries.plugin.DuplicateRule import io.github.z4kn4fein.semver.toVersion import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import kotlin.random.Random plugins { alias(libs.plugins.android.application) @@ -182,6 +181,7 @@ android { release { // Causes patching to not work properly, if enabled. isMinifyEnabled = false + //noinspection NotShrinkingResources isShrinkResources = false val keystoreFile = file("keystore.jks") @@ -228,6 +228,7 @@ android { } androidResources { + @Suppress("UnstableApiUsage") generateLocaleConfig = true } @@ -318,6 +319,7 @@ aboutLibraries { tasks { // Needed by gradle-semantic-release-plugin. // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. + @Suppress("UNUSED") val publish by registering { group = "publishing" description = "Build the release APK" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f9ae36f712..502e5eb791 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - @@ -11,8 +12,11 @@ - - + @@ -43,6 +47,7 @@ + @@ -50,7 +55,10 @@ - + + android:value="patching" /> + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + android:name="androidx.work.WorkManagerInitializer" + android:value="androidx.startup" + tools:node="remove" /> diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 64793f8fe5..54df9a4a6e 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -26,13 +26,13 @@ project("prop_override") # for GameActivity/NativeActivity derived applications, the same library name must be # used in the AndroidManifest.xml file. add_library(${CMAKE_PROJECT_NAME} SHARED - # List C/C++ source files with relative paths to this CMakeLists.txt. + # List C/C++ source files with relative paths to this CMakeLists.txt. prop_override.cpp) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this # build script, prebuilt third-party libraries, or Android system libraries. target_link_libraries(${CMAKE_PROJECT_NAME} - # List libraries link to the target library - android - log) + # List libraries link to the target library + android + log) diff --git a/app/src/main/cpp/prop_override.cpp b/app/src/main/cpp/prop_override.cpp index b314ccd117..bf2e952fd8 100644 --- a/app/src/main/cpp/prop_override.cpp +++ b/app/src/main/cpp/prop_override.cpp @@ -13,6 +13,7 @@ #define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_" extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *); + typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &); char *GetPropOverride(const std::string &key) { diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 588d528b9e..5ee50e8b45 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -111,7 +111,9 @@ class MainActivity : AppCompatActivity() { // fixes issue #2857 by preventing an empty locale override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) { if (overrideConfiguration != null && overrideConfiguration.locales.isEmpty) { - overrideConfiguration.setLocale(SupportedLocales.getCurrentLocale() ?: Locale.getDefault()) + overrideConfiguration.setLocale( + SupportedLocales.getCurrentLocale() ?: Locale.getDefault() + ) } super.applyOverrideConfiguration(overrideConfiguration) } @@ -363,7 +365,12 @@ private fun ReVancedManager(vm: MainViewModel) { deepLinkedComposable("settings/updates") { UpdatesSettingsScreen( onBackClick = navController::popBackStackSafe, - onChangelogClick = { navController.navigateComplex(Settings.Changelogs, ChangelogSource.Manager) }, + onChangelogClick = { + navController.navigateComplex( + Settings.Changelogs, + ChangelogSource.Manager + ) + }, onUpdateClick = { navController.navigateSafe(Update()) } ) } @@ -398,7 +405,10 @@ private fun ReVancedManager(vm: MainViewModel) { composable { val source = it.getComplexArg() - ChangelogsSettingsScreen(source = source, onBackClick = navController::popBackStackSafe) + ChangelogsSettingsScreen( + source = source, + onBackClick = navController::popBackStackSafe + ) } composable { diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 4be3d96171..a501b4e2ae 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -5,17 +5,26 @@ import android.app.Application import android.os.Bundle import android.util.Log import app.revanced.manager.data.platform.Filesystem -import app.revanced.manager.di.* +import app.revanced.manager.di.ackpineModule +import app.revanced.manager.di.databaseModule +import app.revanced.manager.di.httpModule +import app.revanced.manager.di.managerModule +import app.revanced.manager.di.preferencesModule +import app.revanced.manager.di.repositoryModule +import app.revanced.manager.di.rootModule +import app.revanced.manager.di.serviceModule +import app.revanced.manager.di.viewModelModule +import app.revanced.manager.di.workerModule import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.util.tag -import kotlinx.coroutines.Dispatchers import coil.Coil import coil.ImageLoader import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.BuilderImpl +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import me.zhanghai.android.appiconloader.coil.AppIconFetcher 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 093646f01b..ba597cf15b 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 @@ -12,9 +12,6 @@ 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.apps.installed.InstalledPatchBundle -import app.revanced.manager.data.room.selection.PatchSelection -import app.revanced.manager.data.room.selection.SelectedPatch -import app.revanced.manager.data.room.selection.SelectionDao import app.revanced.manager.data.room.bundles.PatchBundleDao import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.downloader.DownloaderDao @@ -22,6 +19,9 @@ import app.revanced.manager.data.room.downloader.DownloaderEntity import app.revanced.manager.data.room.options.Option import app.revanced.manager.data.room.options.OptionDao import app.revanced.manager.data.room.options.OptionGroup +import app.revanced.manager.data.room.selection.PatchSelection +import app.revanced.manager.data.room.selection.SelectedPatch +import app.revanced.manager.data.room.selection.SelectionDao import kotlin.random.Random @Database( diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt index d37a120f09..630c710318 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt @@ -24,7 +24,11 @@ interface DownloadedAppDao { suspend fun upsert(downloadedApp: DownloadedApp) @Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version") - suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis()) + suspend fun markUsed( + packageName: String, + version: String, + newValue: Long = System.currentTimeMillis() + ) @Delete suspend fun delete(downloadedApps: Collection) diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt index 85dd7ffc3f..2a579777b0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -1,6 +1,9 @@ package app.revanced.manager.data.room.bundles -import androidx.room.* +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert import app.revanced.manager.data.room.sources.SourceProperties @Dao diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index 7c4b01177e..dff4339e3f 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -1,6 +1,8 @@ package app.revanced.manager.data.room.bundles -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey import app.revanced.manager.data.room.sources.Source import app.revanced.manager.domain.manager.SourceManager diff --git a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt index a0d558002d..c2f1a398a0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt @@ -1,6 +1,8 @@ package app.revanced.manager.data.room.downloader -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey import app.revanced.manager.data.room.sources.Source import app.revanced.manager.domain.manager.SourceManager diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt index 13c90fa3ab..5b014820d0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt @@ -82,7 +82,8 @@ data class Option( } fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value)) - fun fromValue(value: Any?) = SerializedValue(when (value) { + fun fromValue(value: Any?) = SerializedValue( + when (value) { null -> JsonNull is Number -> JsonPrimitive(value) is Boolean -> JsonPrimitive(value) diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt index fe8f0f4e40..789e1e6a2e 100644 --- a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt @@ -3,7 +3,6 @@ package app.revanced.manager.data.room.selection import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import app.revanced.manager.data.room.AppDatabase diff --git a/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt index 37d8c05dd6..016a69c878 100644 --- a/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt +++ b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt @@ -7,7 +7,8 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val databaseModule = module { - fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build() + fun provideAppDatabase(context: Context) = + Room.databaseBuilder(context, AppDatabase::class.java, "manager").build() single { provideAppDatabase(androidContext()) diff --git a/app/src/main/java/app/revanced/manager/di/HttpModule.kt b/app/src/main/java/app/revanced/manager/di/HttpModule.kt index 1d827ce633..1963c0f9bb 100644 --- a/app/src/main/java/app/revanced/manager/di/HttpModule.kt +++ b/app/src/main/java/app/revanced/manager/di/HttpModule.kt @@ -2,12 +2,12 @@ package app.revanced.manager.di import android.content.Context import app.revanced.manager.BuildConfig -import io.ktor.client.* -import io.ktor.client.engine.okhttp.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.UserAgent -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import okhttp3.Cache import okhttp3.Dns @@ -31,7 +31,12 @@ val httpModule = module { } } }) - cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100)) + cache( + Cache( + context.cacheDir.resolve("cache").also { it.mkdirs() }, + 1024 * 1024 * 100 + ) + ) followRedirects(true) followSslRedirects(true) } 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 952513cf9d..d9c0c673f0 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -2,7 +2,15 @@ package app.revanced.manager.di import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.NetworkInfo -import app.revanced.manager.domain.repository.* +import app.revanced.manager.domain.repository.AnnouncementRepository +import app.revanced.manager.domain.repository.ChangelogsRepository +import app.revanced.manager.domain.repository.DownloadedAppRepository +import app.revanced.manager.domain.repository.DownloaderRepository +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.domain.repository.ManagerUpdateRepository +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.repository.PatchOptionsRepository +import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.network.api.ReVancedAPI import org.koin.core.module.dsl.createdAtStart 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 150c290af7..17589f3d4c 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -1,7 +1,27 @@ package app.revanced.manager.di -import app.revanced.manager.ui.viewmodel.* -import org.koin.core.module.dsl.* +import app.revanced.manager.ui.viewmodel.AboutViewModel +import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel +import app.revanced.manager.ui.viewmodel.AnnouncementsViewModel +import app.revanced.manager.ui.viewmodel.AppsViewModel +import app.revanced.manager.ui.viewmodel.BundleInformationViewModel +import app.revanced.manager.ui.viewmodel.BundleListViewModel +import app.revanced.manager.ui.viewmodel.ChangelogsViewModel +import app.revanced.manager.ui.viewmodel.ContributorViewModel +import app.revanced.manager.ui.viewmodel.DashboardViewModel +import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel +import app.revanced.manager.ui.viewmodel.DownloadsViewModel +import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel +import app.revanced.manager.ui.viewmodel.ImportExportViewModel +import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel +import app.revanced.manager.ui.viewmodel.MainViewModel +import app.revanced.manager.ui.viewmodel.OnboardingViewModel +import app.revanced.manager.ui.viewmodel.PatcherViewModel +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel +import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel +import app.revanced.manager.ui.viewmodel.UpdateViewModel +import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val viewModelModule = module { diff --git a/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt index 6ea6dc0c26..2407774672 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt @@ -8,7 +8,6 @@ import app.revanced.manager.data.redux.Action import app.revanced.manager.data.redux.ActionContext import app.revanced.manager.data.redux.Store import app.revanced.manager.data.room.AppDatabase.Companion.generateUid -import app.revanced.manager.data.room.sources.Source as SourceInfo import app.revanced.manager.data.room.sources.SourceProperties import app.revanced.manager.domain.sources.APISource import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull @@ -33,9 +32,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File import java.io.InputStream -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set +import app.revanced.manager.data.room.sources.Source as SourceInfo /** * Abstraction for managing a source system. Used by [app.revanced.manager.domain.repository.PatchBundleRepository] and [app.revanced.manager.domain.repository.DownloaderRepository]. @@ -321,7 +318,13 @@ abstract class SourceManager( val updateResult = it.runCatching { when { redownload -> downloadLatest() - checkOnly -> getUpdateInfo()?.let { info -> RemoteSource.UpdateResult(info.version, info.createdAt) } + checkOnly -> getUpdateInfo()?.let { info -> + RemoteSource.UpdateResult( + info.version, + info.createdAt + ) + } + else -> update() } ?: return@update null } diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt index 09b34a17a9..dd24d48f34 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -4,7 +4,14 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor diff --git a/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt index 77d59e665b..7a6f93f4fc 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt @@ -7,6 +7,7 @@ import androidx.paging.PagingState import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedAssetHistory import app.revanced.manager.network.utils.getOrThrow +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -15,6 +16,7 @@ import kotlinx.serialization.Serializable sealed interface ChangelogSource : Parcelable { data object Manager : ChangelogSource data class Patches(val url: String, val prerelease: Boolean) : ChangelogSource { + @IgnoredOnParcel val baseUrl by lazy { url.toUri().let { "${it.scheme}://${it.host}" } } } } 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 43d202829d..5a85eb75b2 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 @@ -7,9 +7,9 @@ import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.apps.downloaded.DownloadedApp import app.revanced.manager.downloader.DownloaderHostApi -import app.revanced.manager.network.downloader.LoadedDownloader import app.revanced.manager.downloader.OutputDownloadScope import app.revanced.manager.downloader.Scope +import app.revanced.manager.network.downloader.LoadedDownloader import app.revanced.manager.util.PM import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.channelFlow @@ -51,6 +51,8 @@ class DownloadedAppRepository( delete(getAll().first().filter { it.packageName == packageName }) } + suspend fun deleteAll() = delete(getAll().first()) + @OptIn(DownloaderHostApi::class) suspend fun download( downloader: LoadedDownloader, diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt index 1c39aee3f2..b164d65038 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt @@ -21,12 +21,12 @@ import app.revanced.manager.domain.sources.JsonSource import app.revanced.manager.domain.sources.Loader import app.revanced.manager.domain.sources.LocalSource import app.revanced.manager.domain.sources.Source -import app.revanced.manager.network.downloader.LoadedDownloader -import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.downloader.DownloaderBuilder import app.revanced.manager.downloader.DownloaderHostApi import app.revanced.manager.downloader.Scope import app.revanced.manager.network.downloader.DownloaderPackage +import app.revanced.manager.network.downloader.LoadedDownloader +import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.util.PM import dalvik.system.PathClassLoader import kotlinx.coroutines.flow.map diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 3b6ad8a7bb..5e7f779cb6 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -8,20 +8,21 @@ import app.revanced.manager.R import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.sources.SourceProperties -import app.revanced.manager.data.room.sources.Source as SourceInfo +import app.revanced.manager.domain.manager.SourceManager import app.revanced.manager.domain.sources.APIPatchBundle import app.revanced.manager.domain.sources.JsonPatchBundle +import app.revanced.manager.domain.sources.Loader import app.revanced.manager.domain.sources.LocalPatchBundle import app.revanced.manager.domain.sources.PatchBundleSource -import app.revanced.manager.domain.manager.SourceManager -import app.revanced.manager.domain.sources.Loader import app.revanced.manager.domain.sources.RemotePatchBundle import app.revanced.manager.domain.sources.Source -import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.util.tag -import kotlinx.collections.immutable.* +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -31,9 +32,8 @@ import kotlinx.coroutines.withContext import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import java.io.File -import kotlin.collections.map -import kotlin.text.ifEmpty import kotlin.time.Instant +import app.revanced.manager.data.room.sources.Source as SourceInfo private typealias Info = PersistentMap @@ -108,7 +108,8 @@ class PatchBundleRepository( ) override fun realNameOf(loaded: PatchBundle) = loaded.manifestAttributes?.name - override suspend fun loadDataFromSources(sources: MutableMap>) = loadMetadata(sources).toPersistentMap() + override suspend fun loadDataFromSources(sources: MutableMap>) = + loadMetadata(sources).toPersistentMap() val sources = store.state.map { it.sources.values.toList() } val bundles = store.state.map { diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt index cad6d776b2..2092fc6d9d 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt @@ -68,7 +68,9 @@ class PatchOptionsRepository(db: AppDatabase) { fun getPackagesWithSavedOptions() = dao.getPackagesWithOptions().map(Iterable::toSet).distinctUntilChanged() - suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName) + suspend fun resetOptionsForPackage(packageName: String) = + dao.resetOptionsForPackage(packageName) + suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid) suspend fun reset() = dao.reset() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt index e38a25f7d8..e84f7fb195 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/LocalSource.kt @@ -18,7 +18,7 @@ class LocalSource( ) : Source(name, uid, error, file, loader) { suspend fun ActionContext.replace(inputStream: InputStream) { withContext(Dispatchers.IO) { - outputStream().use { outputStream -> + writeFile { outputStream -> inputStream.copyTo(outputStream) } } diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index cc3246712e..0fc6367e98 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -10,10 +10,10 @@ import app.revanced.manager.patcher.patch.PatchBundle import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDateTime import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File -import kotlinx.datetime.LocalDateTime typealias RemotePatchBundle = RemoteSource typealias JsonPatchBundle = JsonSource @@ -47,8 +47,8 @@ sealed class RemoteSource( copy(error, name, this.autoUpdate, this.versionHash, this.releasedAt) private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) { - outputStream().use { - http.streamTo(it) { + writeFile { stream -> + http.streamTo(stream) { url(info.downloadUrl) } } diff --git a/app/src/main/java/app/revanced/manager/domain/sources/Source.kt b/app/src/main/java/app/revanced/manager/domain/sources/Source.kt index a25a22921a..bd705cd4b0 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/Source.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/Source.kt @@ -47,13 +47,13 @@ sealed class Source( protected fun hasInstalled() = file.exists() - protected fun outputStream(): OutputStream = with(file) { + protected suspend fun writeFile(block: suspend (OutputStream) -> Unit) { // Android 14+ requires dex containers to be readonly. + file.setWritable(true, true) try { - setWritable(true, true) - outputStream() + file.outputStream().use { block(it) } } finally { - setReadOnly() + file.setReadOnly() } } diff --git a/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt index 85dc3df355..38fe851710 100644 --- a/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt +++ b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt @@ -4,4 +4,5 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -abstract class Worker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) \ No newline at end of file +abstract class Worker(context: Context, parameters: WorkerParameters) : + CoroutineWorker(context, parameters) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt index b90e4729f0..953b9321f9 100644 --- a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt @@ -19,7 +19,8 @@ class WorkerRepository(app: Application) { @Suppress("UNCHECKED_CAST") fun > claimInput(worker: W): A { - val data = workerInputs[worker.id] ?: throw IllegalStateException("Worker was not launched via WorkerRepository") + val data = workerInputs[worker.id] + ?: throw IllegalStateException("Worker was not launched via WorkerRepository") workerInputs.remove(worker.id) return data as A diff --git a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt index adcddcc3b8..a2e72a2ce0 100644 --- a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt +++ b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt @@ -10,9 +10,9 @@ import app.revanced.manager.network.dto.ReVancedGitRepository import app.revanced.manager.network.dto.ReVancedInfo import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.utils.APIResponse +import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import io.ktor.client.request.url class ReVancedAPI( private val client: HttpService, @@ -21,7 +21,11 @@ class ReVancedAPI( private suspend fun apiUrl() = prefs.api.get() private val defaultApiVersion = "v5" - private suspend inline fun request(api: String, apiVersion: String, route: String): APIResponse = + private suspend inline fun request( + api: String, + apiVersion: String, + route: String + ): APIResponse = withContext(Dispatchers.IO) { val fullUrl = "$api/$apiVersion/$route" try { @@ -37,21 +41,31 @@ class ReVancedAPI( } } - private suspend inline fun request(route: String, apiVersion: String = defaultApiVersion) = request(apiUrl(), apiVersion, route) + private suspend inline fun request( + route: String, + apiVersion: String = defaultApiVersion + ) = request(apiUrl(), apiVersion, route) suspend fun getAnnouncements() = request>("announcements") suspend fun getLatestAppInfo() = request("manager${prefs.useManagerPrereleases.prereleaseString()}") - suspend fun getAppHistory() = request>("manager/history${prefs.useManagerPrereleases.prereleaseString()}") + suspend fun getAppHistory() = + request>("manager/history${prefs.useManagerPrereleases.prereleaseString()}") - suspend fun getPatchesUpdate() = request("patches${prefs.usePatchesPrereleases.prereleaseString()}") + suspend fun getPatchesUpdate() = + request("patches${prefs.usePatchesPrereleases.prereleaseString()}") suspend fun getPatchesHistory(apiUrl: String, prerelease: Boolean) = - request>(apiUrl, defaultApiVersion, "patches/history${prerelease.prereleaseString()}") + request>( + apiUrl, + defaultApiVersion, + "patches/history${prerelease.prereleaseString()}" + ) - suspend fun getDownloaderUpdate() = request("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}") + suspend fun getDownloaderUpdate() = + request("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}") suspend fun getContributors() = request>("contributors") diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAnnouncement.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAnnouncement.kt index 8dccc45073..73eeb58773 100644 --- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAnnouncement.kt +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAnnouncement.kt @@ -1,11 +1,11 @@ package app.revanced.manager.network.dto import android.os.Parcelable -import kotlin.time.Clock -import kotlin.time.Instant import kotlinx.parcelize.Parcelize import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.time.Clock +import kotlin.time.Instant @Parcelize @Serializable diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt index 27ea5b4bca..01da7ef8b2 100644 --- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ReVancedAsset ( +data class ReVancedAsset( @SerialName("download_url") val downloadUrl: String, @SerialName("created_at") diff --git a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt index 6a4a87b25b..ac7c831a3a 100644 --- a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt +++ b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt @@ -19,6 +19,7 @@ import io.ktor.utils.io.readRemaining import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.io.asSink +import kotlinx.io.buffered import kotlinx.serialization.json.Json import java.io.File import java.io.OutputStream @@ -69,18 +70,18 @@ class HttpService( builder: HttpRequestBuilder.() -> Unit ) { http.prepareGet(builder).execute { httpResponse -> - if (httpResponse.status.isSuccess()) { - withContext(Dispatchers.IO) { - val channel: ByteReadChannel = httpResponse.body() - val sink = outputStream.asSink() + if (!httpResponse.status.isSuccess()) { + throw HttpException(httpResponse.status) + } + withContext(Dispatchers.IO) { + val channel: ByteReadChannel = httpResponse.body() + outputStream.asSink().buffered().use { sink -> while (!channel.exhausted()) { - val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) - packet.transferTo(sink) + channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()).use { packet -> + packet.transferTo(sink) + } } } - - } else { - throw HttpException(httpResponse.status) } } } diff --git a/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt b/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt index 04765f888f..2af9c95059 100644 --- a/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt +++ b/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt @@ -2,7 +2,7 @@ package app.revanced.manager.network.utils -import io.ktor.http.* +import io.ktor.http.HttpStatusCode /** * @author Aliucord Authors, DiamondMiner88 diff --git a/app/src/main/java/app/revanced/manager/patcher/Heartbeat.kt b/app/src/main/java/app/revanced/manager/patcher/Heartbeat.kt new file mode 100644 index 0000000000..17570798dd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/Heartbeat.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.patcher + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds + +class Heartbeat( + scope: CoroutineScope, + initialDelayMs: Long = 3_000L, + private val intervalMs: Long = 1_000L, + private val onTick: (elapsedSeconds: Long) -> Unit, +) { + private val startNs = System.nanoTime() + private val finished = AtomicBoolean(false) + + val elapsedSeconds get() = (System.nanoTime() - startNs) / 1_000_000_000L + + private val job: Job = scope.launch { + delay(initialDelayMs.milliseconds) + while (!finished.get()) { + onTick(elapsedSeconds) + delay(intervalMs.milliseconds) + } + } + + fun complete(onComplete: (elapsedSeconds: Long) -> Unit) { + if (!finished.compareAndSet(false, true)) return + onComplete(elapsedSeconds) + job.cancel() + } + + fun stop() { + finished.set(true) + job.cancel() + } +} diff --git a/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt index e0fe293f9a..5295c098f2 100644 --- a/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt +++ b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt @@ -4,7 +4,9 @@ import android.content.Context import java.io.File abstract class LibraryResolver { - protected fun findLibrary(context: Context, searchTerm: String): File? = File(context.applicationInfo.nativeLibraryDir).run { - list { _, f -> !File(f).isDirectory && f.contains(searchTerm) }?.firstOrNull()?.let { resolve(it) } - } + protected fun findLibrary(context: Context, searchTerm: String): File? = + File(context.applicationInfo.nativeLibraryDir).run { + list { _, f -> !File(f).isDirectory && f.contains(searchTerm) }?.firstOrNull() + ?.let { resolve(it) } + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt index 20b7d8e128..2997883119 100644 --- a/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt +++ b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt @@ -16,12 +16,16 @@ sealed class ProgressEvent : Parcelable { val current: Long? = null, val total: Long? = null, val message: String? = null, + val title: String? = null, + val trailingText: String? = null, ) : ProgressEvent() data class Log( override val stepId: StepId, val level: LogLevel, val message: String, + val replaceLast: Boolean = false, + val trailing: String? = null, ) : ProgressEvent() data class Completed( diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index 6a8b7ff63b..fe0caf6e9b 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -44,29 +44,65 @@ class Session( Dispatchers.Default ) { val context = currentCoroutineContext() - patcher { (patch, exception) -> - // Make the patching process cancelable. - context.ensureActive() + fun emitLog(message: String, trailing: String? = null) = onEvent( + ProgressEvent.Log( + StepId.ExecutePatches, LogLevel.INFO, message, + replaceLast = true, trailing = trailing, + ) + ) - val index = indices[patch] ?: return@patcher + val heartbeat = Heartbeat(this) { elapsed -> + emitLog("Decoding resources", "${elapsed}s") + } + try { + patcher { (patch, exception) -> + heartbeat.complete { elapsed -> + val message = "Decoded resources in ${elapsed}s" + logger.info(message) + emitLog(message) + } + // Make the patching process cancelable. + context.ensureActive() + + val index = indices[patch] ?: return@patcher + + if (exception != null) { + onEvent( + ProgressEvent.Failed( + StepId.ExecutePatch(index), + exception.toRemoteError(), + ) + ) + + // PatchException: Soft-skip missing fingerprint + // Causes can be unsupported version or code obfuscation, + // so skip this one and keep applying the rest instead of aborting the run. + val rootCause = generateSequence(exception as Throwable) { it.cause }.last() + val isFingerprintMissing = + (rootCause is IllegalArgumentException || rootCause is NullPointerException) + && rootCause.stackTrace.any { it.className.contains("CompositeMatch") } + + if (isFingerprintMissing) { + phaseLogger.warn( + "${patch.name} skipped: fingerprint did not match in this app version " + + "(${rootCause::class.java.simpleName}: ${rootCause.message})" + ) + return@patcher + } + + phaseLogger.error("${patch.name} failed:") + phaseLogger.error(exception.stackTraceToString()) + throw exception + } - if (exception != null) { onEvent( - ProgressEvent.Failed( + ProgressEvent.Completed( StepId.ExecutePatch(index), - exception.toRemoteError(), ) ) - phaseLogger.error("${patch.name} failed:") - phaseLogger.error(exception.stackTraceToString()) - throw exception } - - onEvent( - ProgressEvent.Completed( - StepId.ExecutePatch(index), - ) - ) + } finally { + heartbeat.stop() } } @@ -104,14 +140,22 @@ class Session( val patched = tempDir.resolve("result.apk") withContext(Dispatchers.IO) { - Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.copy( + input.toPath(), + patched.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) } result.applyTo(patched) writingLogger.info("Patched apk saved to $patched") withContext(Dispatchers.IO) { - Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.move( + patched.toPath(), + output.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) } } } diff --git a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt index 406c9e9d1f..cda70094e7 100644 --- a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt +++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt @@ -3,6 +3,7 @@ package app.revanced.manager.patcher.aapt import android.content.Context import app.revanced.manager.patcher.LibraryResolver import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS + object Aapt : LibraryResolver() { private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64", "armeabi-v7a") diff --git a/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt index 7eea6f4b01..cd10d2e05b 100644 --- a/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt +++ b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt @@ -37,29 +37,31 @@ abstract class Logger { } } -fun Logger.forStep(stepId: StepId, minLogLevel: LogLevel, onEvent: (ProgressEvent) -> Unit) = object : Logger() { - override fun log(level: LogLevel, message: String) { - this@forStep.log(level, message) - if (level.ordinal >= minLogLevel.ordinal) { - onEvent(ProgressEvent.Log(stepId, level, message)) +fun Logger.forStep(stepId: StepId, minLogLevel: LogLevel, onEvent: (ProgressEvent) -> Unit) = + object : Logger() { + override fun log(level: LogLevel, message: String) { + this@forStep.log(level, message) + if (level.ordinal >= minLogLevel.ordinal) { + onEvent(ProgressEvent.Log(stepId, level, message)) + } } - } - override fun log(level: LogLevel, message: String, loggerName: String?) { - this@forStep.log(level, message, loggerName) + override fun log(level: LogLevel, message: String, loggerName: String?) { + this@forStep.log(level, message, loggerName) - // App loggers should use empty or "app.revanced" prefix; - // filter out logs from libraries to avoid cluttering the step view. - if (level.ordinal >= minLogLevel.ordinal && - (loggerName.isNullOrEmpty() || loggerName.startsWith("app.revanced"))) { - onEvent(ProgressEvent.Log(stepId, level, message)) + // App loggers should use empty or "app.revanced" prefix; + // filter out logs from libraries to avoid cluttering the step view. + if (level.ordinal >= minLogLevel.ordinal && + (loggerName.isNullOrEmpty() || loggerName.startsWith("app.revanced")) + ) { + onEvent(ProgressEvent.Log(stepId, level, message)) + } } } -} inline fun Logger.withJavaLogging(block: () -> T): T { val rootLogger = java.util.logging.Logger.getLogger("") - + // Save the previous level and force INFO to prevent the library from // eagerly allocating millions of string/LogRecord objects for TRACE logs. val previousLevel = rootLogger.level diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index 1e39ae5472..e495369029 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -1,9 +1,9 @@ package app.revanced.manager.patcher.patch -import kotlinx.parcelize.IgnoredOnParcel import android.os.Parcelable import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.loadPatches +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException @@ -77,7 +77,10 @@ data class PatchBundle(val patchesJar: String) : Parcelable { } } - fun patches(bundles: Iterable, packageName: String): Map> = + fun patches( + bundles: Iterable, + packageName: String + ): Map> = patches(bundles).mapValues { (_, result) -> val patches = result.getOrDefault(emptySet()) diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt index 884541fdc0..5b0070f321 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -2,13 +2,13 @@ package app.revanced.manager.patcher.patch import androidx.compose.runtime.Immutable import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.Option as PatchOption import app.revanced.patcher.patch.resourcePatch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlin.reflect.KType +import app.revanced.patcher.patch.Option as PatchOption data class PatchInfo( val name: String, diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 94995eafd4..c01647ddd0 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -8,12 +8,12 @@ import android.os.Build import android.util.Log import androidx.core.content.ContextCompat import app.revanced.manager.BuildConfig -import app.revanced.manager.patcher.runtime.process.IPatcherEvents -import app.revanced.manager.patcher.runtime.process.IPatcherProcess import app.revanced.manager.patcher.LibraryResolver import app.revanced.manager.patcher.ProgressEvent import app.revanced.manager.patcher.ProgressEventParcel import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.runtime.process.IPatcherEvents +import app.revanced.manager.patcher.runtime.process.IPatcherProcess import app.revanced.manager.patcher.runtime.process.Parameters import app.revanced.manager.patcher.runtime.process.PatchConfiguration import app.revanced.manager.patcher.runtime.process.PatcherProcess @@ -106,9 +106,19 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { logger.warn("[STDIO]: $line") } - Log.d(tag, "Process finished with exit code ${result.resultCode}") - - if (result.resultCode != 0) throw Exception("Process exited with nonzero exit code ${result.resultCode}") + val code = result.resultCode + Log.d(tag, "Process finished with exit code $code") + + when (code) { + // :) + 0 -> Unit + // Killed by system - almost always Android LowMemoryKiller targeting the patcher + in setOf(137, 143, 9, 15) -> throw PatcherKilledException(code) + // Native crash in the subprocess - ART, libaapt2, dexlib, etc + in setOf(134, 139, 135, 132, 136, 6, 11) -> throw PatcherCrashedException(code) + // :( + else -> throw Exception("Process exited with nonzero exit code $code") + } } val patching = CompletableDeferred() @@ -181,5 +191,35 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { * @param originalStackTrace The stack trace of the original [Exception]. */ class RemoteFailureException(val originalStackTrace: String) : Exception() + + /** + * The patcher subprocess was terminated by the system (typically SIGKILL + * from Android's LowMemoryKiller or SIGTERM from a service-revoke). + * Distinct from [RemoteFailureException]: no stack trace is available + * because the process was killed externally. + * + * @param exitCode The process exit code that signaled the crash. + * SIGKILL: 137 / 9 + * SIGTERM: 143 / 15 + */ + class PatcherKilledException(exitCode: Int) : + Exception("Patcher subprocess killed by system (exit code $exitCode)") + + /** + * The patcher subprocess crashed natively, the cause is usually a + * bug in a native dependency (ART, libaapt2, dexlib) or a malformed + * APK that tripped one of them. + * Distinct from [RemoteFailureException]: no stack trace is available + * because the process was killed externally. + * + * @param exitCode The process exit code that signaled the crash. + * SIGABRT: 134 / 6 + * SIGSEGV: 139 / 11 + * SIGBUS: 135 + * SIGILL: 132 + * SIGFPE: 136 + */ + class PatcherCrashedException(exitCode: Int) : + Exception("Patcher subprocess crashed natively (exit code $exitCode)") } diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt index 7e4e499620..100d999d73 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -124,9 +124,16 @@ class PatcherProcess() : IPatcherProcess.Stub() { // Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388 Class.forName("android.app.AppCompatCallbacks").apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray) + getDeclaredMethod( + "install", + longArrayClass, + longArrayClass + ).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray) + getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }( + null, + emptyLongArray + ) } } diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 0c9066b1c2..c68b5890ab 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -29,7 +29,12 @@ import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.WorkerRepository +import app.revanced.manager.downloader.DownloaderHostApi +import app.revanced.manager.downloader.GetScope +import app.revanced.manager.downloader.Scope +import app.revanced.manager.downloader.UserInteractionException import app.revanced.manager.network.downloader.LoadedDownloader +import app.revanced.manager.patcher.Heartbeat import app.revanced.manager.patcher.ProgressEvent import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.logger.Logger @@ -37,10 +42,6 @@ import app.revanced.manager.patcher.runStep import app.revanced.manager.patcher.runtime.CoroutineRuntime import app.revanced.manager.patcher.runtime.ProcessRuntime import app.revanced.manager.patcher.toRemoteError -import app.revanced.manager.downloader.GetScope -import app.revanced.manager.downloader.DownloaderHostApi -import app.revanced.manager.downloader.Scope -import app.revanced.manager.downloader.UserInteractionException import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PM @@ -48,6 +49,7 @@ import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.tag import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent @@ -160,24 +162,51 @@ class PatcherWorker( } } - suspend fun download(downloader: LoadedDownloader, data: Parcelable) = - downloadedAppRepository.download( - downloader, - data, - args.packageName, - args.input.version, - prefs.suggestedVersionSafeguard.get(), - !prefs.disablePatchVersionCompatCheck.get(), - onDownload = { progress -> + suspend fun download(downloader: LoadedDownloader, data: Parcelable): File = + coroutineScope { + args.logger.info("Downloading APK file") + val heartbeat = Heartbeat(this) { elapsed -> args.onEvent( ProgressEvent.Progress( stepId = StepId.DownloadAPK, - current = progress.first, - total = progress.second + trailingText = "${elapsed}s", ) ) } - ).also { args.setInputFile(it) } + try { + val result = downloadedAppRepository.download( + downloader, + data, + args.packageName, + args.input.version, + prefs.suggestedVersionSafeguard.get(), + !prefs.disablePatchVersionCompatCheck.get(), + onDownload = { progress -> + args.onEvent( + ProgressEvent.Progress( + stepId = StepId.DownloadAPK, + current = progress.first, + total = progress.second, + ) + ) + } + ).also { args.setInputFile(it) } + heartbeat.complete { elapsed -> + val title = "Downloaded APK file in ${elapsed}s" + args.logger.info(title) + args.onEvent( + ProgressEvent.Progress( + stepId = StepId.DownloadAPK, + title = title, + trailingText = "", + ) + ) + } + result + } finally { + heartbeat.stop() + } + } val inputFile = when (val selectedApp = args.input) { is SelectedApp.Download -> { @@ -197,20 +226,24 @@ class PatcherWorker( .ifEmpty { error("No downloaders are loaded") } .firstNotNullOfOrNull { downloader -> try { - val getScope = object : GetScope, Scope by downloader.scopeImpl { - override suspend fun requestStartActivity(intent: Intent): Intent? { - val result = - args.handleStartActivityRequest(downloader, intent) - return when (result.resultCode) { - Activity.RESULT_OK -> result.data - Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() - else -> throw UserInteractionException.Activity.NotCompleted( - result.resultCode, - result.data - ) + val getScope = + object : GetScope, Scope by downloader.scopeImpl { + override suspend fun requestStartActivity(intent: Intent): Intent? { + val result = + args.handleStartActivityRequest( + downloader, + intent + ) + return when (result.resultCode) { + Activity.RESULT_OK -> result.data + Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() + else -> throw UserInteractionException.Activity.NotCompleted( + result.resultCode, + result.data + ) + } } } - } withContext(Dispatchers.IO) { downloader.impl.get( getScope, 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 6bb4d4fdfe..cf4b5a96aa 100644 --- a/app/src/main/java/app/revanced/manager/service/UninstallService.kt +++ b/app/src/main/java/app/revanced/manager/service/UninstallService.kt @@ -19,13 +19,14 @@ class UninstallService : Service() { when (extraStatus) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - startActivity(if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_INTENT) - }.apply { - this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) + startActivity( + if (Build.VERSION.SDK_INT >= 33) { + intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + } else { + intent.getParcelableExtra(Intent.EXTRA_INTENT) + }.apply { + this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) } else -> { diff --git a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt index c2089d5811..1361c9a6a6 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt @@ -55,7 +55,9 @@ fun AlertDialogExtended( ) { Column(modifier = Modifier.padding(vertical = 24.dp)) { Column( - modifier = Modifier.padding(horizontal = 24.dp).fillMaxWidth() + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() ) { icon?.let { ContentStyle(color = iconContentColor) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/AnimateVisibility.kt b/app/src/main/java/app/revanced/manager/ui/component/AnimateVisibility.kt new file mode 100644 index 0000000000..6880b78332 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AnimateVisibility.kt @@ -0,0 +1,27 @@ +package app.revanced.manager.ui.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.IntSize + +@Composable +fun AnimateVisibility( + visible: Boolean, + content: @Composable AnimatedVisibilityScope.() -> Unit, +) { + val motionSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastSpatialSpec() + + AnimatedVisibility( + visible = visible, + enter = expandVertically(motionSpec) + fadeIn(), + exit = shrinkVertically(motionSpec) + fadeOut(), + content = content, + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt index 6d45b20bee..d4a775161c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt @@ -12,7 +12,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun AppInfo(appInfo: PackageInfo?, placeholderLabel: String? = null, extraContent: @Composable () -> Unit = {}) { +fun AppInfo( + appInfo: PackageInfo?, + placeholderLabel: String? = null, + extraContent: @Composable () -> Unit = {} +) { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt index e7362f08c8..89d3b56c25 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt @@ -9,9 +9,19 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Update -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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 @@ -79,7 +89,9 @@ fun AvailableUpdateDialog( }, leadingContent = { CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { - HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + HapticCheckbox( + checked = dontShowAgain, + onCheckedChange = { dontShowAgain = it }) } }, colors = transparentListItemColors diff --git a/app/src/main/java/app/revanced/manager/ui/component/BottomContentBar.kt b/app/src/main/java/app/revanced/manager/ui/component/BottomContentBar.kt index 48072a9a9b..eede68e36d 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/BottomContentBar.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/BottomContentBar.kt @@ -13,7 +13,12 @@ import androidx.compose.ui.unit.dp @Composable fun BottomContentBar( modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp), + contentPadding: PaddingValues = PaddingValues( + start = 16.dp, + end = 16.dp, + top = 8.dp, + bottom = 16.dp + ), content: @Composable ColumnScope.() -> Unit ) { Column( diff --git a/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt b/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt index bc35877966..af77276f41 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt @@ -25,5 +25,8 @@ fun ColumnWithScrollbar( horizontalAlignment = horizontalAlignment, content = content ) - Scrollbar(state, Modifier.then(modifier.padding())) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues + Scrollbar( + state, + Modifier.then(modifier.padding()) + ) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt index 5476c01053..957a2f9da8 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt @@ -11,9 +11,10 @@ import androidx.compose.runtime.Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ContentSelector(mime: String, onSelect: (Uri) -> Unit, content: @Composable () -> Unit) { - val activityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let(onSelect) - } + val activityLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let(onSelect) + } Button( onClick = { activityLauncher.launch(mime) diff --git a/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt index 13f05d9af4..bb2a6afe05 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt @@ -27,13 +27,14 @@ fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Un val prefs: PreferencesManager = koinInject() val theme by prefs.theme.getAsState() val isDarkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK - + Dialog( onDismissRequest = onDismissRequest, properties = properties ) { val view = LocalView.current - + + @Suppress("DEPRECATION") SideEffect { val window = (view.parent as DialogWindowProvider).window WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index 82fddad0a6..2eb30c5b30 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -20,6 +20,9 @@ import com.github.materiiapps.enumutil.FromValue private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit +const val INTERNAL_STATUS_PATCHER_KILLED = -1000 +const val INTERNAL_STATUS_PATCHER_CRASHED = -1001 + @Composable fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) { val dialogKind = remember { @@ -116,6 +119,20 @@ enum class DialogKind( contentStringResId = R.string.installation_timeout_description, confirmButton = installerStatusDialogButton(R.string.try_again) { it.install() }, dismissButton = installerStatusDialogButton(R.string.cancel), + ), + PATCHER_KILLED( + flag = INTERNAL_STATUS_PATCHER_KILLED, + title = R.string.installation_patcher_killed_dialog_title, + contentStringResId = R.string.installation_patcher_killed_description, + confirmButton = installerStatusDialogButton(R.string.try_again) { it.retry() }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + PATCHER_CRASHED( + flag = INTERNAL_STATUS_PATCHER_CRASHED, + title = R.string.installation_patcher_crashed_dialog_title, + contentStringResId = R.string.installation_patcher_crashed_description, + confirmButton = installerStatusDialogButton(R.string.try_again) { it.retry() }, + dismissButton = installerStatusDialogButton(R.string.cancel), ); // Needed due to the @FromValue annotation. diff --git a/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt b/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt index e1f984c546..8c801c45f4 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt @@ -38,5 +38,8 @@ fun LazyColumnWithScrollbar( userScrollEnabled = userScrollEnabled, content = content ) - Scrollbar(state, Modifier.then(modifier.padding())) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues + Scrollbar( + state, + Modifier.then(modifier.padding()) + ) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ListSection.kt b/app/src/main/java/app/revanced/manager/ui/component/ListSection.kt index 0d06d78d2f..3d2f586338 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ListSection.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ListSection.kt @@ -11,8 +11,8 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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 e6d8f24379..f1d2d14c0c 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 @@ -26,12 +26,11 @@ fun LoadingIndicator( trackColor = trackColor, strokeCap = strokeCap ) - } ?: - CircularProgressIndicator( - modifier = modifier, - color = color, - strokeWidth = strokeWidth, - trackColor = trackColor, - strokeCap = strokeCap - ) + } ?: CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt index 5e9f36f1f8..004d6f8958 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.util.formatStyled enum class NotificationCardType { NORMAL, WARNING, ERROR @@ -79,12 +80,14 @@ fun NotificationCard( title?.let { Text( text = it, - style = MaterialTheme.typography.bodyMediumEmphasized.merge(fontWeight = FontWeight.SemiBold), + style = MaterialTheme.typography.bodyMediumEmphasized.merge( + fontWeight = FontWeight.SemiBold + ), color = LocalContentColor.current, ) } Text( - text = text, + text = formatStyled(text), style = MaterialTheme.typography.bodyMedium, color = LocalContentColor.current, ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt b/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt index aeaa169dac..246a06b0ed 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt @@ -22,7 +22,13 @@ import app.revanced.manager.R @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) { +fun PasswordField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null +) { var visible by rememberSaveable { mutableStateOf(false) } diff --git a/app/src/main/java/app/revanced/manager/ui/component/PillTabBar.kt b/app/src/main/java/app/revanced/manager/ui/component/PillTabBar.kt index c01ed734bb..9682d3d186 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/PillTabBar.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/PillTabBar.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -48,6 +47,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat import kotlin.math.roundToInt @Composable @@ -69,7 +69,8 @@ fun PillTabBar( .padding(PillTabBarDefaults.ContainerPadding) ) { val indicatorWidthPx = with(LocalDensity.current) { maxWidth.toPx() } / tabCount - val offsetX = (pagerState.currentPage + pagerState.currentPageOffsetFraction) * indicatorWidthPx + val offsetX = + (pagerState.currentPage + pagerState.currentPageOffsetFraction) * indicatorWidthPx PillTabIndicator( offsetX = offsetX, @@ -185,7 +186,8 @@ private fun PillTabContent( icon: (@Composable () -> Unit)?, text: @Composable () -> Unit ) { - val contentColor = if (isSelected) colors.selectedContentColor else colors.unselectedContentColor + val contentColor = + if (isSelected) colors.selectedContentColor else colors.unselectedContentColor val fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal CompositionLocalProvider(LocalContentColor provides contentColor) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/ShareSheet.kt b/app/src/main/java/app/revanced/manager/ui/component/ShareSheet.kt index 0345652d87..991faa3152 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ShareSheet.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ShareSheet.kt @@ -67,14 +67,15 @@ private fun resolveShareTargets(context: Context, uri: Uri): List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.queryIntentActivities( - sendIntent, - PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) - ) - } else { - packageManager.queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY) - } + val resolvedActivities: List = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.queryIntentActivities( + sendIntent, + PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + ) + } else { + packageManager.queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY) + } return resolvedActivities.mapNotNull { resolved -> val info = resolved.activityInfo ?: return@mapNotNull null diff --git a/app/src/main/java/app/revanced/manager/ui/component/Tooltip.kt b/app/src/main/java/app/revanced/manager/ui/component/Tooltip.kt index d4b8d4ad8e..42f1979905 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/Tooltip.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/Tooltip.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.IconButton @@ -16,6 +15,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView +import androidx.core.view.HapticFeedbackConstantsCompat @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index eb1f874895..3a45c8a82f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -3,14 +3,32 @@ package app.revanced.manager.ui.component.bundle import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.Search -import androidx.compose.material3.* +import androidx.compose.material3.CardColors +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,8 +45,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R -import app.revanced.manager.domain.sources.PatchBundleSource import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.sources.PatchBundleSource import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.FullscreenDialog @@ -56,13 +74,13 @@ fun BundlePatchesDialog( } else { patches.filter { patch -> patch.name.contains(query, ignoreCase = true) || - patch.description?.contains(query, ignoreCase = true) == true || - patch.compatiblePackages?.any { compatiblePackage -> - compatiblePackage.packageName.contains(query, ignoreCase = true) || - compatiblePackage.versions?.any { version -> - version.contains(query, ignoreCase = true) - } == true - } == true + patch.description?.contains(query, ignoreCase = true) == true || + patch.compatiblePackages?.any { compatiblePackage -> + compatiblePackage.packageName.contains(query, ignoreCase = true) || + compatiblePackage.versions?.any { version -> + version.contains(query, ignoreCase = true) + } == true + } == true } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt index fb26e904bd..1dc14a7804 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt @@ -65,7 +65,10 @@ fun BundleSelector( colors = ListItemDefaults.colors( containerColor = MaterialTheme.colorScheme.surfaceContainer ), - shapes = ListItemDefaults.segmentedShapes(index = index, count = sources.size) + shapes = ListItemDefaults.segmentedShapes( + index = index, + count = sources.size + ) ) { Text( "${source.name} ${source.version}", diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt index 6d7d6cdec6..ada03b5cae 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt @@ -10,10 +10,10 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.TooltipIconButton -import androidx.compose.ui.res.stringResource @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt index 4cfb340057..72e3352b43 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxColors @@ -8,6 +7,7 @@ import androidx.compose.material3.CheckboxDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.util.withHapticFeedback @Composable diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt index 8f9829d81a..48e8bea951 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults @@ -11,11 +10,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.ui.component.TooltipHost import app.revanced.manager.util.withHapticFeedback @Composable -fun HapticExtendedFloatingActionButton ( +fun HapticExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt index 93a97e2d0f..e58fd25a99 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults @@ -11,11 +10,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.ui.component.TooltipHost import app.revanced.manager.util.withHapticFeedback @Composable -fun HapticFloatingActionButton ( +fun HapticFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, tooltip: String? = null, diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt index af37a2f8ac..c786fc3d7e 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButtonColors @@ -9,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView +import androidx.core.view.HapticFeedbackConstantsCompat @Composable fun HapticRadioButton( diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt index 5615fa5c50..8e9357b9c3 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.Switch import androidx.compose.material3.SwitchColors @@ -9,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView +import androidx.core.view.HapticFeedbackConstantsCompat @Composable fun HapticSwitch( diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt index 4ede041eb0..48de0453fe 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Tab @@ -8,10 +7,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.util.withHapticFeedback @Composable -fun HapticTab ( +fun HapticTab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTriStateCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTriStateCheckbox.kt index 58dfdc10b4..56c5fa4a98 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTriStateCheckbox.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTriStateCheckbox.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.haptics -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.CheckboxColors import androidx.compose.material3.CheckboxDefaults @@ -9,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.state.ToggleableState +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.util.withHapticFeedback @Composable diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt index aa5c1b748c..212be286bc 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -4,7 +4,13 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt index b449e146f4..82c8db13fc 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -40,8 +41,8 @@ import app.revanced.manager.R import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.model.State -import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.Step +import app.revanced.manager.ui.model.StepCategory import java.util.Locale import kotlin.math.floor @@ -114,20 +115,29 @@ fun Steps( .padding(top = 10.dp) ) { filteredSteps.forEachIndexed { index, step -> - val (progress, progressText) = step.progress?.let { (current, total) -> - if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB" - else null to "${current.megaBytes} MB" - } ?: (null to null) - - SubStep( - name = step.title, - state = step.state, - message = step.message, - progress = progress, - progressText = progressText, - isFirst = index == 0, - isLast = index == filteredSteps.lastIndex, - ) + key(step.id) { + val derived = remember(step.progress, step.trailingText) { + val (progress, sizeText) = step.progress?.let { (current, total) -> + if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB" + else null to "${current.megaBytes} MB" + } ?: (null to null) + val trailing = step.trailingText?.takeIf { it.isNotEmpty() } + val progressText = listOfNotNull(sizeText, trailing) + .takeIf { it.isNotEmpty() } + ?.joinToString(" - ") + progress to progressText + } + SubStep( + name = step.title, + state = step.state, + message = step.message, + trailing = step.messageTrailing, + progress = derived.first, + progressText = derived.second, + isFirst = index == 0, + isLast = index == filteredSteps.lastIndex, + ) + } } } } @@ -139,6 +149,7 @@ fun SubStep( name: String, state: State, message: String? = null, + trailing: String? = null, progress: Float? = null, progressText: String? = null, isFirst: Boolean = false, @@ -154,7 +165,7 @@ fun SubStep( else this } .padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp) - .padding(horizontal = 20.dp) + .padding(horizontal = 23.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -194,12 +205,40 @@ fun SubStep( } AnimatedVisibility(visible = messageExpanded && message != null) { - Text( - text = message.orEmpty(), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp) - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 36.dp, vertical = 8.dp) + ) { + val lines = remember(message) { message.orEmpty().split('\n') } + lines.forEachIndexed { i, line -> + val isLastLine = i == lines.lastIndex + if (isLastLine && trailing != null) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = line, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.weight(1f, fill = true), + ) + Text( + text = trailing, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + ) + } + } else { + Text( + text = line, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + ) + } + } + } } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt index 6d4ba8b5aa..983d98f12c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -193,7 +193,9 @@ fun OptionItem( Box { content() if (safeguardActive) { - Box(modifier = Modifier.matchParentSize().clickable { showWarningDialog = true }) + Box(modifier = Modifier + .matchParentSize() + .clickable { showWarningDialog = true }) } } } @@ -362,7 +364,8 @@ private fun OptionDropdownSection( }, ) { OutlinedTextField( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .menuAnchor( if (!readOnly) ExposedDropdownMenuAnchorType.PrimaryEditable else ExposedDropdownMenuAnchorType.PrimaryNotEditable diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/PatchItem.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/PatchItem.kt index 418040a6fd..2d54396728 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/PatchItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/PatchItem.kt @@ -42,28 +42,30 @@ fun PatchItem( val anyVersionLabel = stringResource(R.string.patches_view_any_version) val anyAppLabel = stringResource(R.string.universal) - val chipLabels = remember(patch.compatiblePackages, scopedPackageName, anyVersionLabel, anyAppLabel) { - val pkgs = patch.compatiblePackages - when { - pkgs == null -> if (scopedPackageName == null) listOf(anyAppLabel) else emptyList() - scopedPackageName != null -> { - val pkg = pkgs.firstOrNull { it.packageName == scopedPackageName } - ?: return@remember emptyList() - val versions = pkg.versions - if (versions.isNullOrEmpty()) listOf(anyVersionLabel) else versions.toList() - } - else -> { - pkgs.map { pkg -> + val chipLabels = + remember(patch.compatiblePackages, scopedPackageName, anyVersionLabel, anyAppLabel) { + val pkgs = patch.compatiblePackages + when { + pkgs == null -> if (scopedPackageName == null) listOf(anyAppLabel) else emptyList() + scopedPackageName != null -> { + val pkg = pkgs.firstOrNull { it.packageName == scopedPackageName } + ?: return@remember emptyList() val versions = pkg.versions - if (versions.isNullOrEmpty()) { - "${pkg.packageName} ($anyVersionLabel)" - } else { - "${pkg.packageName} (${versions.joinToString(", ")})" + if (versions.isNullOrEmpty()) listOf(anyVersionLabel) else versions.toList() + } + + else -> { + pkgs.map { pkg -> + val versions = pkg.versions + if (versions.isNullOrEmpty()) { + "${pkg.packageName} ($anyVersionLabel)" + } else { + "${pkg.packageName} (${versions.joinToString(", ")})" + } } } } } - } ListItem( modifier = Modifier diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt index a2b5ba5996..b289f8e197 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt @@ -44,7 +44,8 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) { currentDirectory != root } val (directories, files) = remember(currentDirectory) { - currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory) + currentDirectory.listDirectoryEntries().filter(Path::isReadable) + .partition(Path::isDirectory) } FullscreenDialog( @@ -56,7 +57,10 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) { title = stringResource(R.string.path_selector), onBackClick = { onSelect(null) }, backIcon = { - Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.close)) + Icon( + Icons.Filled.Close, + contentDescription = stringResource(R.string.close) + ) } ) }, diff --git a/app/src/main/java/app/revanced/manager/ui/component/scaffold/BannerScaffold.kt b/app/src/main/java/app/revanced/manager/ui/component/scaffold/BannerScaffold.kt index 5f4800fcdd..3b239d508b 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/scaffold/BannerScaffold.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/scaffold/BannerScaffold.kt @@ -182,7 +182,7 @@ fun rememberBannerScrollBehavior( state = state, canCollapse = { canCollapse() && - (sheetLazyListState.canScrollForward || sheetLazyListState.canScrollBackward) + (sheetLazyListState.canScrollForward || sheetLazyListState.canScrollBackward) }, dispatchSheetRawDelta = sheetLazyListState::dispatchRawDelta, ) @@ -236,13 +236,15 @@ fun BannerScaffold( val totalMainAxisPx = with(density) { if (isLandscape) maxWidth.roundToPx() else maxHeight.roundToPx() } - val availableMainAxisPx = (totalMainAxisPx - if (isLandscape) 0 else topBarPx).coerceAtLeast(0) + val availableMainAxisPx = + (totalMainAxisPx - if (isLandscape) 0 else topBarPx).coerceAtLeast(0) val expandedBannerPx = (availableMainAxisPx * axisFraction).roundToInt() val collapsedBannerPx = (availableMainAxisPx * collapsedAxisFraction) .roundToInt() .coerceAtMost(expandedBannerPx) - val collapseFraction = if (isLandscape) 0f else scrollBehavior?.state?.collapsedFraction ?: 0f + val collapseFraction = + if (isLandscape) 0f else scrollBehavior?.state?.collapsedFraction ?: 0f SideEffect { scrollBehavior?.state?.offsetLimitPx = @@ -250,7 +252,11 @@ fun BannerScaffold( } val bannerPx = if (isLandscape) expandedBannerPx else { - lerp(expandedBannerPx.toFloat(), collapsedBannerPx.toFloat(), collapseFraction).roundToInt() + lerp( + expandedBannerPx.toFloat(), + collapsedBannerPx.toFloat(), + collapseFraction + ).roundToInt() } val bannerSize = with(density) { @@ -280,14 +286,22 @@ fun BannerScaffold( Modifier } - Box(Modifier.fillMaxSize().then(scaffoldScrollModifier)) { - val bannerBackgroundModifier = Modifier.align(Alignment.TopStart).then( - if (isLandscape) { - Modifier.width(backgroundExtent).fillMaxHeight() - } else { - Modifier.fillMaxWidth().height(backgroundExtent) - } - ) + Box(Modifier + .fillMaxSize() + .then(scaffoldScrollModifier)) { + val bannerBackgroundModifier = Modifier + .align(Alignment.TopStart) + .then( + if (isLandscape) { + Modifier + .width(backgroundExtent) + .fillMaxHeight() + } else { + Modifier + .fillMaxWidth() + .height(backgroundExtent) + } + ) CompositionLocalProvider(LocalContentColor provides colors.bannerContentColor) { Box(modifier = bannerBackgroundModifier) { @@ -295,13 +309,20 @@ fun BannerScaffold( } } - val bannerContentModifier = Modifier.align(Alignment.TopStart).then( - if (isLandscape) { - Modifier.width(bannerSize).fillMaxHeight() - } else { - Modifier.padding(top = topBarHeight).fillMaxWidth().height(bannerSize) - } - ) + val bannerContentModifier = Modifier + .align(Alignment.TopStart) + .then( + if (isLandscape) { + Modifier + .width(bannerSize) + .fillMaxHeight() + } else { + Modifier + .padding(top = topBarHeight) + .fillMaxWidth() + .height(bannerSize) + } + ) CompositionLocalProvider(LocalContentColor provides colors.bannerContentColor) { Box(modifier = bannerContentModifier.clipToBounds()) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt index a82e02fe5a..cf04d9a085 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt @@ -1,6 +1,7 @@ package app.revanced.manager.ui.component.settings import androidx.annotation.StringRes +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close @@ -11,7 +12,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.foundation.layout.size import app.revanced.manager.domain.manager.base.Preference import app.revanced.manager.ui.component.haptics.HapticSwitch import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt index 4f495ea011..5a6179f491 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt @@ -51,7 +51,8 @@ fun SettingsListItem( ) SegmentedListItem( - onClick = onClick?.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK) ?: { }.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK), + onClick = onClick?.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK) + ?: { }.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK), onLongClick = onLongClick, onLongClickLabel = onLongClickLabel, shapes = shapes, @@ -100,7 +101,8 @@ fun SettingsListItem( ) SegmentedListItem( - onClick = onClick?.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK) ?: { }.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK), + onClick = onClick?.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK) + ?: { }.withHapticFeedback(HapticFeedbackConstantsCompat.CLOCK_TICK), onLongClick = onLongClick, onLongClickLabel = onLongClickLabel, shapes = shapes, diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/ThemeSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/ThemeSelector.kt index 96c7290a36..85ed89d293 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/ThemeSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/ThemeSelector.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.component.settings -import androidx.core.view.HapticFeedbackConstantsCompat import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -35,6 +34,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat import app.revanced.manager.R import app.revanced.manager.ui.theme.Theme import app.revanced.manager.util.withHapticFeedback @@ -49,7 +49,11 @@ fun ThemeSelector( Surface( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(4.dp), - color = animateColorAsState(MaterialTheme.colorScheme.surfaceContainerLow, MaterialTheme.motionScheme.defaultEffectsSpec(), "surfaceContainerLow").value, + color = animateColorAsState( + MaterialTheme.colorScheme.surfaceContainerLow, + MaterialTheme.motionScheme.defaultEffectsSpec(), + "surfaceContainerLow" + ).value, ) { Row( modifier = Modifier diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 9dac923a4c..ad201ecb19 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -12,9 +12,25 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Topic -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -78,7 +94,7 @@ fun ImportSourceDialog( } fun launchFileActivity() { - when(strings) { + when (strings) { ImportSourceDialogStrings.PATCHES -> fileActivityLauncher.launch(BIN_MIMETYPE) ImportSourceDialogStrings.DOWNLOADERS -> fileActivityLauncher.launch(APK_MIMETYPE) } diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt index 410b64c1bd..f6312d2088 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -3,4 +3,5 @@ package app.revanced.manager.ui.model interface InstallerModel { fun reinstall() fun install() + fun retry() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index db5205cd38..64155e4143 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -23,7 +23,10 @@ data class Step( val category: StepCategory, val state: State = State.WAITING, val message: String? = null, + val index: Int? = null, val progress: Pair? = null, + val trailingText: String? = null, + val messageTrailing: String? = null, val hide: Boolean = false, ) : Parcelable @@ -31,5 +34,23 @@ data class Step( fun Step.withState( state: State = this.state, message: String? = this.message, - progress: Pair? = this.progress -) = copy(state = state, message = message, progress = progress) \ No newline at end of file + progress: Pair? = this.progress, + index: Int? = this.index, +) = copy(state = state, message = message, progress = progress, index = index) + +internal fun replaceLineAtIndex(current: String?, index: Int, replacement: String): String { + if (current.isNullOrEmpty()) return replacement + val lastNewline = current.lastIndexOf('\n') + val lastLineIndex = if (lastNewline == -1) 0 else current.count { it == '\n' } + if (index == lastLineIndex) { + return if (lastNewline == -1) replacement + else current.substring(0, lastNewline + 1) + replacement + } + if (index == lastLineIndex + 1) return current + "\n" + replacement + val lines = current.split("\n").toMutableList() + when (index) { + in lines.indices -> lines[index] = replacement + lines.size -> lines.add(replacement) + } + return lines.joinToString("\n") +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementScreen.kt index 6e69d7c744..edcb17188f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementScreen.kt @@ -63,7 +63,8 @@ fun AnnouncementScreen( ) }, subtitle = { - val createDate = announcement.createdAt.toLocalDateTime(TimeZone.UTC).relativeTime(LocalContext.current) + val createDate = announcement.createdAt.toLocalDateTime(TimeZone.UTC) + .relativeTime(LocalContext.current) Text("$createDate\u2002\u2022\u2002${announcement.author}") }, navigationIcon = { @@ -119,6 +120,7 @@ fun AnnouncementScreen( }, update = { val webView = it.children.first() as WebView + @Language("HTML") val style = """ diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementsScreen.kt index f17f33ec1a..18a9e86951 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AnnouncementsScreen.kt @@ -298,7 +298,7 @@ private fun AnnouncementListItem( author: String, tags: List, unread: Boolean, - archived: Boolean + @Suppress("unused") archived: Boolean ) { SettingsListItem( onClick = onClick, @@ -324,7 +324,8 @@ private fun AnnouncementListItem( val dot = "\u2022" // • val emSpace = "\u2002" // en space, roughly half character width val separator = "$emSpace$dot$emSpace" - Text("$date$separator$author", + Text( + text = "$date$separator$author", style = MaterialTheme.typography.labelSmall.copy( fontWeight = if (unread) FontWeight.Bold else FontWeight.Normal ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt index 1de685a6b2..17203d79eb 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt @@ -72,7 +72,6 @@ import app.revanced.manager.ui.component.settings.SafeguardBooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.BundleInformationViewModel import app.revanced.manager.util.relativeTime -import java.util.Locale.getDefault @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable 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 c8c5d0b8b2..9fe44a6cf6 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 @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -61,6 +62,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -79,6 +81,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.network.dto.ReVancedAnnouncement import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.AnimateVisibility import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.NotificationCard @@ -107,6 +110,7 @@ enum class DashboardPage( BUNDLES(R.string.tab_patches, Icons.Outlined.Source), } +@Suppress("AssignedValueIsNeverRead") @SuppressLint("BatteryLife") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -169,10 +173,13 @@ fun DashboardScreen( browseAllBundles = true ) } - val dashboardPatchesViewModel = koinViewModel(key = "dashboard-patches") { - parametersOf(dashboardPatchesParams) - } - val dashboardPatchesBundles by dashboardPatchesViewModel.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) + val dashboardPatchesViewModel = + koinViewModel(key = "dashboard-patches") { + parametersOf(dashboardPatchesParams) + } + val dashboardPatchesBundles by dashboardPatchesViewModel.bundlesFlow.collectAsStateWithLifecycle( + initialValue = emptyList() + ) var patchesSourceEditMode by rememberSaveable { mutableStateOf(false) } var sourceDeleteUid by rememberSaveable { mutableStateOf(null) } @@ -190,7 +197,10 @@ fun DashboardScreen( onDismiss = { sourceDeleteUid = null }, onConfirm = { vm.deleteSource(uid) }, title = stringResource(R.string.delete), - description = stringResource(R.string.patches_delete_single_dialog_description, sourceName), + description = stringResource( + R.string.patches_delete_single_dialog_description, + sourceName + ), icon = Icons.Outlined.Delete ) } @@ -309,7 +319,8 @@ fun DashboardScreen( ) ) - val navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val navBarHeight = + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() if (navBarHeight > 0.dp) { Box( modifier = Modifier @@ -428,7 +439,13 @@ fun DashboardScreen( DashboardPage.entries.forEachIndexed { index, page -> PillTab( index = index, - onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, + onClick = { + composableScope.launch { + pagerState.animateScrollToPage( + index + ) + } + }, text = { Text(stringResource(page.titleResId)) }, icon = { Icon(page.icon, null) } ) @@ -436,38 +453,33 @@ fun DashboardScreen( } Notifications( - if (bundleDownloadError != null) { - { - NotificationCard( - type = NotificationCardType.ERROR, - icon = Icons.Outlined.WarningAmber, - title = stringResource(R.string.api_not_working_title), - text = stringResource(R.string.api_not_working_description), - onClick = onSettingsClick - ) - } - } else null, - if (sourceUpdatesAvailable) { - { - NotificationCard( - type = NotificationCardType.WARNING, - icon = Icons.Outlined.Refresh, - text = stringResource(R.string.banner_sources_not_updated_description), - onClick = vm::downloadSources - ) - } - } else if (sourcesNotDownloaded && bundleDownloadError == null) { - { - NotificationCard( - type = NotificationCardType.WARNING, - icon = Icons.Outlined.Refresh, - text = stringResource(R.string.banner_sources_not_downloaded_description), - onClick = vm::downloadSources - ) - } - } else null, - vm.unreadAnnouncement?.let { announcement -> - { + (bundleDownloadError != null) to { + NotificationCard( + type = NotificationCardType.ERROR, + icon = Icons.Outlined.WarningAmber, + title = stringResource(R.string.api_not_working_title), + text = stringResource(R.string.api_not_working_description), + onClick = onSettingsClick + ) + }, + (sourceUpdatesAvailable) to { + NotificationCard( + type = NotificationCardType.WARNING, + icon = Icons.Outlined.Refresh, + text = stringResource(R.string.banner_sources_not_updated_description), + onClick = vm::downloadSources + ) + }, + (!sourceUpdatesAvailable && sourcesNotDownloaded && bundleDownloadError == null) to { + NotificationCard( + type = NotificationCardType.WARNING, + icon = Icons.Outlined.Refresh, + text = stringResource(R.string.banner_sources_not_downloaded_description), + onClick = vm::downloadSources + ) + }, + (vm.unreadAnnouncement != null) to { + vm.unreadAnnouncement?.let { announcement -> NotificationCard( title = stringResource(R.string.new_announcement), text = announcement.title, @@ -493,7 +505,11 @@ fun DashboardScreen( AppsScreen( onAppClick = { onAppClick(it.currentPackageName) }, onPatchableAppClick = ::onPatchableSelection, - onStorageSelect = { selectedApp -> onStorageSelection(selectedApp)}, + onStorageSelect = { selectedApp -> + onStorageSelection( + selectedApp + ) + }, lazyListState = appsLazyListState, searchLazyListState = appsSearchLazyListState, onSearchExpandedChange = { appsSearchExpanded = it } @@ -558,7 +574,9 @@ private fun DashboardFab( } AnimatedVisibility( - visible = fabState != DashboardFabState.Hidden, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut() + visible = fabState != DashboardFabState.Hidden, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut() ) { HapticExtendedFloatingActionButton( onClick = { @@ -624,19 +642,22 @@ private enum class DashboardFabState { @Composable fun Notifications( - vararg notifications: (@Composable () -> Unit)?, + vararg notifications: Pair Unit> ) { - val activeNotifications = notifications.filterNotNull() - - if (activeNotifications.isNotEmpty()) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - activeNotifications.forEach { notification -> - notification() + Column(Modifier.fillMaxWidth()) { + notifications.forEachIndexed { index, (isVisible, notification) -> + key(index) { + AnimateVisibility(visible = isVisible) { + Box(Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)) { + notification() + } + } } } + + AnimateVisibility(visible = notifications.any { it.first }) { + Spacer(Modifier.height(16.dp)) + } } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 49257b5c47..a15307dc80 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -92,8 +92,12 @@ fun InstalledAppInfoScreen( ) { val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar - AppInfo(viewModel.appInfo) { - Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) + AppInfo(viewModel.appInfo) { + Text( + installedApp.version, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) if (installedApp.installType == InstallType.MOUNT) { Text( @@ -136,7 +140,9 @@ fun InstalledAppInfoScreen( SegmentedButton( icon = Icons.Outlined.Circle, - text = if (viewModel.isMounted) stringResource(R.string.unmount) else stringResource(R.string.mount), + text = if (viewModel.isMounted) stringResource(R.string.unmount) else stringResource( + R.string.mount + ), onClick = viewModel::mountOrUnmount, enabled = viewModel.rootInstaller.hasRootAccess() ) @@ -159,17 +165,22 @@ fun InstalledAppInfoScreen( ) { SettingsListItem( headlineContent = stringResource(R.string.applied_patches), - supportingContent = - (viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let { - pluralStringResource( - id = R.plurals.patch_count, - it, - it - ) - }, - trailingContent = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }, + supportingContent = + (viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let { + pluralStringResource( + id = R.plurals.patch_count, + it, + it + ) + }, + trailingContent = { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(R.string.view_applied_patches) + ) + }, onClick = { showAppliedPatchesDialog = true }, - ) + ) SettingsListItem( headlineContent = stringResource(R.string.package_name), diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index ca3926052e..bbb9b9aa0e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -82,12 +82,13 @@ fun PatcherScreen( val context = LocalContext.current val resources = LocalResources.current var showLogExportSheet by rememberSaveable { mutableStateOf(false) } - val exportApkLauncher = rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export) + val exportApkLauncher = + rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export) val saveLogsLauncher = rememberLauncherForActivityResult(CreateDocument("text/plain")) { uri -> - viewModel.saveLogs(uri) - showLogExportSheet = false - viewModel.clearPreparedLogExport() - } + viewModel.saveLogs(uri) + showLogExportSheet = false + viewModel.clearPreparedLogExport() + } val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null) val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } } 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 99e00bc8c2..434337382b 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 @@ -358,7 +358,10 @@ fun PatchesSelectorScreen( expanded = searchExpanded, onExpandedChange = setSearchExpanded, placeholder = { Text(stringResource(R.string.search_patches)) }, - windowInsets = if (readOnly) WindowInsets(top = 0, bottom = 0) else WindowInsets.systemBars, + windowInsets = if (readOnly) WindowInsets( + top = 0, + bottom = 0 + ) else WindowInsets.systemBars, leadingIcon = { TooltipIconButton( onClick = { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index 3f321d396e..b18c5272cf 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -1,12 +1,14 @@ package app.revanced.manager.ui.screen import android.annotation.SuppressLint -import androidx.annotation.StringRes -import androidx.appcompat.content.res.AppCompatResources +import android.content.Intent +import android.net.Uri +import android.os.PowerManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.Image -import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -14,8 +16,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import android.content.Intent -import android.os.PowerManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.BatteryAlert @@ -33,11 +33,11 @@ import androidx.compose.material3.MediumFlexibleTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,6 +51,7 @@ import androidx.core.content.getSystemService import app.revanced.manager.BuildConfig import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.ui.component.AnimateVisibility import app.revanced.manager.ui.component.BottomContentBar import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ListSection @@ -88,9 +89,9 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> ) { derivedStateOf { disablePatchVersionCompatCheck || - disableSelectionWarning || - disableUniversalPatchCheck || - !suggestedVersionSafeguard + disableSelectionWarning || + disableUniversalPatchCheck || + !suggestedVersionSafeguard } } val scrollState = rememberScrollState() @@ -101,16 +102,17 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> ) val context = LocalContext.current val powerManager = remember(context) { context.getSystemService()!! } - + var showBatteryOptimizationsWarning by remember { mutableStateOf(!powerManager.isIgnoringBatteryOptimizations(context.packageName)) } - + val batteryOptimizationsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(context.packageName) + showBatteryOptimizationsWarning = + !powerManager.isIgnoringBatteryOptimizations(context.packageName) } - + val appIcon = rememberDrawablePainter( drawable = remember(context) { AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring) @@ -215,7 +217,7 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> state = scrollState ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - if (showBatteryOptimizationsWarning) { + AnimateVisibility(visible = showBatteryOptimizationsWarning) { NotificationCard( modifier = Modifier.padding(horizontal = 16.dp), type = NotificationCardType.WARNING, @@ -231,7 +233,7 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> } ) } - + ListSection { generalSections.forEach { (name, description, icon, destination) -> SettingsListItem( @@ -245,7 +247,8 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> ListSection { advancedSections.forEach { (name, description, icon, destination) -> - val hasSafeguardWarning = destination == Settings.Advanced && safeguardsToggled + val hasSafeguardWarning = + destination == Settings.Advanced && safeguardsToggled val supportingText = if (hasSafeguardWarning) { "Safeguards have been toggled" } else { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt index 9e3417feae..46c7d90d9a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt @@ -1,5 +1,6 @@ package app.revanced.manager.ui.screen +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -27,7 +28,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/app/src/main/java/app/revanced/manager/ui/screen/onboarding/PermissionsStepContent.kt b/app/src/main/java/app/revanced/manager/ui/screen/onboarding/PermissionsStepContent.kt index a86592cd14..2bb3fc02f4 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/onboarding/PermissionsStepContent.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/onboarding/PermissionsStepContent.kt @@ -1,9 +1,16 @@ package app.revanced.manager.ui.screen.onboarding import android.os.Build +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -18,14 +25,19 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import app.revanced.manager.R import app.revanced.manager.ui.component.ListSection import app.revanced.manager.ui.component.settings.SettingsListItem @@ -39,12 +51,23 @@ fun PermissionsStepContent( onRequestNotifications: () -> Unit, onRequestBatteryOptimization: () -> Unit ) { + val textMeasurer = rememberTextMeasurer() + val buttonText = stringResource(R.string.permission_grant) + val textStyle = MaterialTheme.typography.labelMedium + val density = LocalDensity.current + + val minButtonWidth = remember(buttonText, textStyle, density) { + val textWidthPx = textMeasurer.measure(buttonText, textStyle).size.width + max(64.dp, with(density) { textWidthPx.toDp() } + 24.dp) + } + ListSection(contentPadding = PaddingValues(0.dp)) { PermissionItem( icon = Icons.Outlined.Security, title = stringResource(R.string.permission_install_apps), description = stringResource(R.string.permission_install_apps_description), isGranted = canInstallUnknownApps, + minTrailingWidth = minButtonWidth, onRequest = onRequestInstallApps ) @@ -54,6 +77,7 @@ fun PermissionsStepContent( title = stringResource(R.string.permission_notifications), description = stringResource(R.string.permission_notifications_description), isGranted = isNotificationsEnabled, + minTrailingWidth = minButtonWidth, onRequest = onRequestNotifications ) } @@ -63,6 +87,7 @@ fun PermissionsStepContent( title = stringResource(R.string.permission_battery), description = stringResource(R.string.permission_battery_description), isGranted = isBatteryOptimizationExempt, + minTrailingWidth = minButtonWidth, onRequest = onRequestBatteryOptimization ) } @@ -75,6 +100,7 @@ private fun PermissionItem( title: String, description: String, isGranted: Boolean, + minTrailingWidth: Dp, onRequest: () -> Unit ) { SettingsListItem( @@ -97,24 +123,36 @@ private fun PermissionItem( ) }, trailingContent = { - if (isGranted) { - OnboardingLeadingIcon( - icon = Icons.Default.Check, - containerColor = MaterialTheme.colorScheme.primaryContainer, - iconColor = MaterialTheme.colorScheme.onPrimaryContainer, - size = 32.dp, - iconSize = 16.dp - ) - } else { - FilledTonalButton( - onClick = onRequest, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), - shapes = ButtonDefaults.shapes() - ) { - Text( - text = stringResource(R.string.permission_grant), - style = MaterialTheme.typography.labelMedium - ) + Box( + modifier = Modifier.defaultMinSize(minWidth = minTrailingWidth), + contentAlignment = Alignment.CenterEnd + ) { + AnimatedContent( + targetState = isGranted, + transitionSpec = { fadeIn() togetherWith fadeOut() }, + contentAlignment = Alignment.Center, + label = "onboarding_trailing_button" + ) { isGranted -> + if (isGranted) { + OnboardingLeadingIcon( + icon = Icons.Default.Check, + containerColor = MaterialTheme.colorScheme.primaryContainer, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer, + size = 32.dp, + iconSize = 16.dp + ) + } else { + FilledTonalButton( + onClick = onRequest, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + shapes = ButtonDefaults.shapes() + ) { + Text( + text = stringResource(R.string.permission_grant), + style = MaterialTheme.typography.labelMedium + ) + } + } } } } @@ -129,18 +167,29 @@ internal fun OnboardingLeadingIcon( size: Dp = 40.dp, iconSize: Dp = 22.dp ) { + val animatedContainerColor by animateColorAsState( + containerColor, + tween(), + "containerColor", + ) + val animatedIconColor by animateColorAsState( + iconColor, + tween(), + "iconColor", + ) + Box( modifier = Modifier .size(size) .clip(CircleShape) - .background(containerColor), + .background(animatedContainerColor), contentAlignment = Alignment.Center ) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.size(iconSize), - tint = iconColor + tint = animatedIconColor ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt index 1d21fdf6b5..118fef7a4d 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt @@ -62,7 +62,8 @@ import app.revanced.manager.util.openUrl import com.google.accompanist.drawablepainter.rememberDrawablePainter import org.koin.androidx.compose.koinViewModel -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class ) @Composable @@ -82,36 +83,37 @@ fun AboutSettingsScreen( viewModel.socials.partition(ReVancedSocial::preferred) } - val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) { - preferredSocials.map { - Triple( - getSocialIcon(it.name), - it.name, - third = { - context.openUrl(it.url) - } - ) - } + listOfNotNull( - viewModel.donate?.let { - Triple( - Icons.Outlined.FavoriteBorder, - resources.getString(R.string.donate), - third = { - context.openUrl(it) - } - ) - }, - viewModel.contact?.let { + val preferredSocialButtons = + remember(resources, preferredSocials, viewModel.donate, viewModel.contact) { + preferredSocials.map { Triple( - Icons.Outlined.MailOutline, - resources.getString(R.string.contact), + getSocialIcon(it.name), + it.name, third = { - context.openUrl("mailto:$it") + context.openUrl(it.url) } ) - } - ) - } + } + listOfNotNull( + viewModel.donate?.let { + Triple( + Icons.Outlined.FavoriteBorder, + resources.getString(R.string.donate), + third = { + context.openUrl(it) + } + ) + }, + viewModel.contact?.let { + Triple( + Icons.Outlined.MailOutline, + resources.getString(R.string.contact), + third = { + context.openUrl("mailto:$it") + } + ) + } + ) + } val socialButtons = remember(socials) { socials.map { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 2f696492f0..3ab3d85a8b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MediumFlexibleTopAppBar -import app.revanced.manager.ui.component.haptics.HapticRadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -49,13 +48,13 @@ import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch import app.revanced.manager.BuildConfig import app.revanced.manager.R import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ListSection import app.revanced.manager.ui.component.TooltipIconButton +import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.SafeguardBooleanItem @@ -64,6 +63,7 @@ import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel import app.revanced.manager.util.toast import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.withHapticFeedback +import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -147,7 +147,7 @@ fun AdvancedSettingsScreen( coroutineScope = viewModel.viewModelScope, headline = R.string.patch_compat_check, description = R.string.patch_compat_check_description, - dialogTitle = R.string.patch_compat_check_title, + dialogTitle = R.string.patch_compat_check_title, confirmationText = R.string.patch_compat_check_confirmation ) SafeguardBooleanItem( @@ -155,7 +155,7 @@ fun AdvancedSettingsScreen( coroutineScope = viewModel.viewModelScope, headline = R.string.suggested_version_safeguard, description = R.string.suggested_version_safeguard_description, - dialogTitle = R.string.suggested_version_safeguard_title, + dialogTitle = R.string.suggested_version_safeguard_title, confirmationText = R.string.suggested_version_safeguard_confirmation ) SafeguardBooleanItem( @@ -163,7 +163,7 @@ fun AdvancedSettingsScreen( coroutineScope = viewModel.viewModelScope, headline = R.string.patch_selection_safeguard, description = R.string.patch_selection_safeguard_description, - dialogTitle = R.string.patch_selection_safeguard_title, + dialogTitle = R.string.patch_selection_safeguard_title, confirmationText = R.string.patch_selection_safeguard_confirmation ) SafeguardBooleanItem( @@ -220,7 +220,10 @@ fun AdvancedSettingsScreen( ListItem( modifier = Modifier.clickable { selected = level }, leadingContent = { - HapticRadioButton(selected = selected == level, onClick = null) + HapticRadioButton( + selected = selected == level, + onClick = null + ) }, headlineContent = { Text(stringResource(level.displayName)) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt index 54a57b412e..2dec385976 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt @@ -19,8 +19,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon - - import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumFlexibleTopAppBar import androidx.compose.material3.OutlinedTextField @@ -91,7 +89,13 @@ fun DeveloperSettingsScreen( ) { ListSection( title = stringResource(R.string.manager), - leadingContent = { Icon(Icons.Outlined.WorkOutline, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.WorkOutline, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { val apiUrl by vm.prefs.api.getAsState() var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } @@ -131,7 +135,13 @@ fun DeveloperSettingsScreen( ListSection( title = stringResource(R.string.patches), - leadingContent = { Icon(Icons.Outlined.PostAdd, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.PostAdd, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { SettingsListItem( headlineContent = stringResource(R.string.patches_force_download), @@ -145,12 +155,22 @@ fun DeveloperSettingsScreen( ListSection( title = stringResource(R.string.downloaders), - leadingContent = { Icon(Icons.Outlined.Download, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Download, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { SettingsListItem( headlineContent = stringResource(R.string.downloaders_reset), onClick = vm::resetDownloaders ) + SettingsListItem( + headlineContent = stringResource(R.string.downloaded_apps_clear), + onClick = vm::clearDownloadedApps + ) } } } @@ -208,7 +228,10 @@ private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (Stri onClick = { url = defaultUrl }, tooltip = stringResource(R.string.api_url_dialog_reset) ) { - Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset)) + Icon( + Icons.Outlined.Restore, + stringResource(R.string.api_url_dialog_reset) + ) } } ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt index 1c9efbe4d9..55af7f89aa 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt @@ -1,10 +1,10 @@ package app.revanced.manager.ui.screen.settings import android.webkit.URLUtil.isValidUrl +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt index 823197992a..f1e0d4612a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt @@ -126,14 +126,20 @@ fun GeneralSettingsScreen( ) { ListSection( title = stringResource(R.string.appearance), - leadingContent = { Icon(Icons.Outlined.Palette, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Palette, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { val currentLocale = viewModel.getCurrentLocale() val currentLanguageDisplay = remember(currentLocale) { currentLocale?.let { viewModel.getLocaleDisplayName(it) } } val theme by prefs.theme.getAsState() - + ThemeSelector( currentTheme = theme, onThemeSelected = { viewModel.setTheme(it) } @@ -183,7 +189,13 @@ fun GeneralSettingsScreen( ListSection( title = stringResource(R.string.networking), - leadingContent = { Icon(Icons.Outlined.Public, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Public, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { BooleanItem( preference = prefs.allowMeteredNetworks, @@ -224,7 +236,7 @@ private fun LanguagePicker( val nativeName = locale.getDisplayName(locale) localizedName.contains(searchQuery, ignoreCase = true) || - nativeName.contains(searchQuery, ignoreCase = true) + nativeName.contains(searchQuery, ignoreCase = true) } } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 5901d6e5cb..b9469bbf55 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -153,7 +153,11 @@ fun ImportExportSettingsScreen( onDismissRequest = vm::cancelKeystoreImport, onSubmit = { alias, pass -> vm.viewModelScope.launch { - uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") { + uiSafe( + context, + R.string.failed_to_import_keystore, + "Failed to import keystore" + ) { val result = vm.tryKeystoreImport(alias, pass) if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials)) } @@ -202,7 +206,13 @@ fun ImportExportSettingsScreen( ) { ListSection( title = stringResource(R.string.keystore), - leadingContent = { Icon(Icons.Outlined.Key, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Key, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { Surface( modifier = Modifier @@ -214,77 +224,96 @@ fun ImportExportSettingsScreen( "surfaceContainerLow" ).value, ) { - Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 8.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - ImpoxportDetailColumn( - title = stringResource(R.string.import_keystore_dialog_alias_field), - value = keystoreAlias, - leadingContent = { - TooltipIconButton( - onClick = { - clipboard.setPrimaryClip( - ClipData.newPlainText( - resources.getString(R.string.import_keystore_dialog_alias_field), - keystoreAlias - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ImpoxportDetailColumn( + title = stringResource(R.string.import_keystore_dialog_alias_field), + value = keystoreAlias, + leadingContent = { + TooltipIconButton( + onClick = { + clipboard.setPrimaryClip( + ClipData.newPlainText( + resources.getString(R.string.import_keystore_dialog_alias_field), + keystoreAlias ) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) context.toast(resources.getString(R.string.toast_copied_to_clipboard)) - }, - tooltip = stringResource(R.string.copy_to_clipboard) - ) { - Icon( - imageVector = Icons.Outlined.ContentCopy, - contentDescription = stringResource(R.string.copy_to_clipboard) ) - } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) context.toast( + resources.getString(R.string.toast_copied_to_clipboard) + ) + }, + tooltip = stringResource(R.string.copy_to_clipboard) + ) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.copy_to_clipboard) + ) } - ) - ImpoxportDetailColumn( - title = stringResource(R.string.import_keystore_dialog_password_field), - value = if (showKeystorePassword) keystorePass else "•".repeat(keystorePass.length), - leadingContent = { - val hidePassword = showKeystorePassword - TooltipIconButton( - onClick = { showKeystorePassword = !showKeystorePassword }, - tooltip = if (hidePassword) { + } + ) + ImpoxportDetailColumn( + title = stringResource(R.string.import_keystore_dialog_password_field), + value = if (showKeystorePassword) keystorePass else "•".repeat( + keystorePass.length + ), + leadingContent = { + val hidePassword = showKeystorePassword + TooltipIconButton( + onClick = { showKeystorePassword = !showKeystorePassword }, + tooltip = if (hidePassword) { + stringResource(R.string.hide_password_field) + } else { + stringResource(R.string.show_password_field) + } + ) { + Icon( + imageVector = if (hidePassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = if (hidePassword) { stringResource(R.string.hide_password_field) } else { stringResource(R.string.show_password_field) } - ) { - Icon( - imageVector = if (hidePassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - contentDescription = if (hidePassword) { - stringResource(R.string.hide_password_field) - } else { - stringResource(R.string.show_password_field) - } - ) - } - TooltipIconButton( - onClick = { - clipboard.setPrimaryClip( - ClipData.newPlainText( - resources.getString(R.string.import_keystore_dialog_password_field), - keystorePass - ).apply { description.extras = PersistableBundle().apply { + ) + } + TooltipIconButton( + onClick = { + clipboard.setPrimaryClip( + ClipData.newPlainText( + resources.getString(R.string.import_keystore_dialog_password_field), + keystorePass + ).apply { + description.extras = PersistableBundle().apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + putBoolean( + ClipDescription.EXTRA_IS_SENSITIVE, + true + ) } else { - putBoolean("android.content.extra.IS_SENSITIVE", true) + putBoolean( + "android.content.extra.IS_SENSITIVE", + true + ) } - } } - ) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) context.toast(resources.getString(R.string.toast_copied_to_clipboard)) - }, - tooltip = stringResource(R.string.copy_to_clipboard) - ) { - Icon( - imageVector = Icons.Outlined.ContentCopy, - contentDescription = stringResource(R.string.copy_to_clipboard) + } + } ) - } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) context.toast( + resources.getString(R.string.toast_copied_to_clipboard) + ) + }, + tooltip = stringResource(R.string.copy_to_clipboard) + ) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.copy_to_clipboard) + ) } - ) + } + ) } } Surface( @@ -338,7 +367,13 @@ fun ImportExportSettingsScreen( ListSection( title = stringResource(R.string.patches_selections), - leadingContent = { Icon(Icons.Outlined.Source, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Source, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -409,7 +444,13 @@ fun ImportExportSettingsScreen( } ListSection( title = stringResource(R.string.reset), - leadingContent = { Icon(Icons.Outlined.Restore, contentDescription = null, modifier = Modifier.size(18.dp)) } + leadingContent = { + Icon( + Icons.Outlined.Restore, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) { SettingsListItem( onClick = { @@ -420,32 +461,32 @@ fun ImportExportSettingsScreen( headlineContent = stringResource(R.string.regenerate_keystore), supportingContent = stringResource(R.string.regenerate_keystore_description) ) - Surface( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - color = animateColorAsState( - MaterialTheme.colorScheme.surfaceContainerLow, - MaterialTheme.motionScheme.defaultEffectsSpec(), - "surfaceContainerLow" - ).value, - ) { - FilledTonalButton( - onClick = { showResetSheet = true }, + Surface( modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - shapes = ButtonDefaults.shapes() + .fillMaxWidth(), + shape = RoundedCornerShape(4.dp), + color = animateColorAsState( + MaterialTheme.colorScheme.surfaceContainerLow, + MaterialTheme.motionScheme.defaultEffectsSpec(), + "surfaceContainerLow" + ).value, ) { - Icon( - imageVector = Icons.Outlined.Restore, - contentDescription = null - ) - Spacer(modifier = Modifier.size(8.dp)) - Text(stringResource(R.string.reset)) + FilledTonalButton( + onClick = { showResetSheet = true }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shapes = ButtonDefaults.shapes() + ) { + Icon( + imageVector = Icons.Outlined.Restore, + contentDescription = null + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.reset)) + } } } - } if (showResetSheet) { ResetBottomSheet( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt index e91bf1519f..c69c722b49 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt @@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size diff --git a/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt index 9a5b160a8c..ec4bab879c 100644 --- a/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt +++ b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt @@ -2,7 +2,13 @@ package app.revanced.manager.ui.theme import android.app.Activity import android.os.Build -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color @@ -141,6 +147,7 @@ fun ReVancedManagerTheme( val view = LocalView.current if (!view.isInEditMode) { + @Suppress("DEPRECATION") SideEffect { val activity = view.context as Activity @@ -149,8 +156,12 @@ fun ReVancedManagerTheme( activity.window.statusBarColor = Color.Transparent.toArgb() activity.window.navigationBarColor = Color.Transparent.toArgb() - WindowCompat.getInsetsController(activity.window, view).isAppearanceLightStatusBars = !darkTheme - WindowCompat.getInsetsController(activity.window, view).isAppearanceLightNavigationBars = !darkTheme + WindowCompat.getInsetsController(activity.window, view).isAppearanceLightStatusBars = + !darkTheme + WindowCompat.getInsetsController( + activity.window, + view + ).isAppearanceLightNavigationBars = !darkTheme } } @@ -162,6 +173,7 @@ fun ReVancedManagerTheme( ) } +@Suppress("UNUSED") enum class Theme(val displayName: Int) { SYSTEM(R.string.system), LIGHT(R.string.light), diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt index 09ac84f171..9e8589b5b7 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt @@ -15,9 +15,9 @@ import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootServiceException +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.PM import app.revanced.manager.util.toast @@ -127,7 +127,8 @@ class AppsViewModel( installedAppsRepository.delete(app) return@withContext null } - } catch (_: RootServiceException) { } + } catch (_: RootServiceException) { + } } val packageInfo = pm.getPackageInfo(app.currentPackageName) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt index f1276b6b48..de52b27d29 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt @@ -5,9 +5,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.sources.PatchBundleSource import app.revanced.manager.domain.sources.RemotePatchBundle -import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.util.mutableStateSetOf import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt index e0766c19f4..9adf550684 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() { var repositories: List? by mutableStateOf(null) - private set + private set init { viewModelScope.launch { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 34e2cfcffe..3dc9aebbf8 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -21,8 +21,8 @@ import app.revanced.manager.util.PM import app.revanced.manager.util.uiSafe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt index f58c129580..2ffff368c2 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt @@ -4,10 +4,11 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R -import app.revanced.manager.domain.sources.RemotePatchBundle import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.sources.RemotePatchBundle import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe import kotlinx.coroutines.Dispatchers @@ -17,7 +18,8 @@ class DeveloperOptionsViewModel( val prefs: PreferencesManager, private val app: Application, private val patchBundleRepository: PatchBundleRepository, - private val downloaderRepository: DownloaderRepository + private val downloaderRepository: DownloaderRepository, + private val downloadedAppRepository: DownloadedAppRepository, ) : ViewModel() { fun redownloadBundles() = viewModelScope.launch { uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) { @@ -44,6 +46,11 @@ class DeveloperOptionsViewModel( downloaderRepository.reset() } + fun clearDownloadedApps() = viewModelScope.launch { + downloadedAppRepository.deleteAll() + app.toast(app.getString(R.string.downloaded_apps_clear_done)) + } + fun resetOnboarding() = viewModelScope.launch { prefs.completedOnboarding.update(false) app.toast(app.getString(R.string.sideeffect_restart)) 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 63d20cd7b0..1285d33dae 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 @@ -57,7 +57,8 @@ class DownloadsViewModel( fun updateUsePrereleases(value: Boolean) = viewModelScope.launch { usePrereleases.update(value) - val apiSource = downloaderRepository.downloaderSources.first()[0]?.asRemoteOrNull ?: return@launch + val apiSource = + downloaderRepository.downloaderSources.first()[0]?.asRemoteOrNull ?: return@launch updateDownloader(apiSource) } @@ -111,9 +112,10 @@ class DownloadsViewModel( } } - fun setAutoUpdate(src: RemoteSource, value: Boolean) = viewModelScope.launch { - with(downloaderRepository) { - src.setAutoUpdate(value) + fun setAutoUpdate(src: RemoteSource, value: Boolean) = + viewModelScope.launch { + with(downloaderRepository) { + src.setAutoUpdate(value) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt index b904789ada..5af7ce8a30 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -13,12 +13,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R import app.revanced.manager.domain.manager.KeystoreManager +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SerializedSelection -import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.sources.PatchBundleSource -import app.revanced.manager.domain.manager.PreferencesManager -import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.util.JSON_MIMETYPE import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 70914d5122..c1b71eb76a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -104,6 +104,7 @@ class InstalledAppInfoViewModel( } return@launch } + Session.State.Succeeded -> {} } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt index 62d0f412b9..5e8f1535bb 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -100,7 +100,8 @@ class MainViewModel( } } - val storedPatchesFile = File(app.filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json") + // directly returns: /data/data/ + val storedPatchesFile = File(app.dataDir, "app_flutter/selected-patches.json") val patches: SerializedSelection? = if (storedPatchesFile.exists()) { json.decodeFromString(storedPatchesFile.readText()) @@ -132,7 +133,11 @@ class MainViewModel( applyLegacySettings(settings, patches, keystore) } - private fun applyLegacySettings(settings: LegacySettings, patches: SerializedSelection?, keystore: ByteArray?) = viewModelScope.launch { + private fun applyLegacySettings( + settings: LegacySettings, + patches: SerializedSelection?, + keystore: ByteArray? + ) = viewModelScope.launch { settings.themeMode?.let { theme -> val themeMap = mapOf( 0 to Theme.SYSTEM, @@ -168,16 +173,17 @@ class MainViewModel( patchSelectionRepository.import(0, selection) } settings.patchedApps?.let { apps -> - json.decodeFromString>(apps.removePrefix(LEGACY_LIST_PREFIX)).forEach { appJson -> - val patchedApp = json.decodeFromString(appJson) - installedAppRepository.addOrUpdate( - patchedApp.packageName, - patchedApp.packageName, - patchedApp.version, - if (patchedApp.isRooted) InstallType.MOUNT else InstallType.DEFAULT, - mapOf(0 to patchedApp.appliedPatches.toSet()) - ) - } + json.decodeFromString>(apps.removePrefix(LEGACY_LIST_PREFIX)) + .forEach { appJson -> + val patchedApp = json.decodeFromString(appJson) + installedAppRepository.addOrUpdate( + patchedApp.packageName, + patchedApp.packageName, + patchedApp.version, + if (patchedApp.isRooted) InstallType.MOUNT else InstallType.DEFAULT, + mapOf(0 to patchedApp.appliedPatches.toSet()) + ) + } } Log.d(tag, "Imported legacy settings") } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/OnboardingViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/OnboardingViewModel.kt index 648e44fd03..b32c8ff425 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/OnboardingViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/OnboardingViewModel.kt @@ -9,10 +9,10 @@ import androidx.compose.runtime.setValue import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import androidx.lifecycle.ViewModel -import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.util.PM import kotlinx.coroutines.flow.combine @@ -82,7 +82,11 @@ class OnboardingViewModel( currentStep = previousStep(currentStep) } - suspend fun applyAutoUpdatePrefs(managerEnabled: Boolean, patchesEnabled: Boolean, downloadersEnabled: Boolean) { + suspend fun applyAutoUpdatePrefs( + managerEnabled: Boolean, + patchesEnabled: Boolean, + downloadersEnabled: Boolean + ) { prefs.managerAutoUpdates.update(managerEnabled) with(patchBundleRepository) { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index be136c7d5b..f114ce5d92 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList +import androidx.core.content.FileProvider import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.map @@ -30,7 +31,6 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.await import app.revanced.manager.BuildConfig -import androidx.core.content.FileProvider import app.revanced.manager.R import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.room.apps.installed.InstallType @@ -50,12 +50,15 @@ import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.worker.PatcherWorker +import app.revanced.manager.ui.component.INTERNAL_STATUS_PATCHER_CRASHED +import app.revanced.manager.ui.component.INTERNAL_STATUS_PATCHER_KILLED import app.revanced.manager.ui.model.InstallerModel import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.navigation.Patcher +import app.revanced.manager.ui.model.replaceLineAtIndex import app.revanced.manager.ui.model.withState import app.revanced.manager.util.PM import app.revanced.manager.util.PatchSelection @@ -94,6 +97,7 @@ import ru.solrudev.ackpine.uninstaller.UninstallFailure import java.io.File import java.nio.file.Files import java.time.Duration +import kotlin.time.Duration.Companion.milliseconds import android.content.pm.PackageInstaller as AndroidPackageInstaller @OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class) @@ -181,7 +185,8 @@ class PatcherViewModel( } } val logPreviewText - get() = logs.takeLast(30).joinToString("\n") { (level, msg) -> "[${level.name}]: $msg" } + get() = logs.takeLast(30) + .joinToString("\n") { (level, msg) -> formatLogLine(app, level, msg) } val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList() @@ -190,7 +195,7 @@ class PatcherViewModel( val progress by derivedStateOf { val steps = steps.filter { it.id != StepId.ExecutePatches } - val current = steps.count { it.state == State.COMPLETED } + val current = steps.count { it.state == State.COMPLETED || it.state == State.FAILED } val total = steps.size current.toFloat() / total.toFloat() @@ -309,24 +314,62 @@ class PatcherViewModel( is ProgressEvent.Progress -> currentStep.withState( message = event.message ?: currentStep.message, - progress = event.current?.let { event.current to event.total } ?: currentStep.progress - ) + progress = event.current?.let { event.current to event.total } + ?: currentStep.progress + ).let { step -> + step.copy( + title = event.title ?: step.title, + trailingText = event.trailingText ?: step.trailingText, + ) + } - is ProgressEvent.Log -> currentStep.withState( - message = appendLog(currentStep.message, formatLogLine(event.level, event.message)) - ) + is ProgressEvent.Log -> { + val line = formatUiLogLine(app, event.level, event.message) + val message = currentStep.message + val updated = when { + !event.replaceLast -> currentStep.withState( + message = appendLog( + message, + line + ) + ) + + currentStep.index != null -> currentStep.withState( + message = replaceLineAtIndex(message, currentStep.index, line) + ) + + message.isNullOrEmpty() -> currentStep.withState(message = line, index = 0) + else -> { + val lastLineIndex = message.count { it == '\n' } + currentStep.withState( + message = replaceLineAtIndex(message, lastLineIndex, line), + index = lastLineIndex, + ) + } + } + updated.copy(messageTrailing = event.trailing) + } - is ProgressEvent.Completed -> currentStep.withState(State.COMPLETED, progress = null) + is ProgressEvent.Completed -> currentStep.withState(State.COMPLETED) .let { step -> if (step.id is StepId.ExecutePatch) step.copy(hide = false) else step } - is ProgressEvent.Failed -> currentStep.withState( - State.FAILED, - message = event.error.stackTrace, - progress = null - ).let { step -> - if (step.id is StepId.ExecutePatch) step.copy(hide = false) else step + is ProgressEvent.Failed -> { + when { + event.error.type.endsWith("PatcherKilledException") -> + packageInstallerStatus = INTERNAL_STATUS_PATCHER_KILLED + + event.error.type.endsWith("PatcherCrashedException") -> + packageInstallerStatus = INTERNAL_STATUS_PATCHER_CRASHED + } + currentStep.withState( + State.FAILED, + message = event.error.stackTrace, + progress = null + ).let { step -> + if (step.id is StepId.ExecutePatch) step.copy(hide = false) else step + } } } @@ -370,10 +413,11 @@ class PatcherViewModel( logger.warn("Failed purge attempt $attempt/${maxAttempts}: $e") } if (deleted && remaining.isEmpty()) break - delay(200) + delay(200.milliseconds) } - val detailed = "absPath=\"${fs.tempDir.absolutePath}\" before=$sizeBefore deleteOk=$deleted attempts=$attempt afterCount=${remaining.size} survivors=${remaining.joinToString { it.name }}" + val detailed = + "absPath=\"${fs.tempDir.absolutePath}\" before=$sizeBefore deleteOk=$deleted attempts=$attempt afterCount=${remaining.size} survivors=${remaining.joinToString { it.name }}" if (deleted && remaining.isEmpty()) { logger.info("Successfully cleared patcher temp files") @@ -408,9 +452,11 @@ class PatcherViewModel( } } - fun getOutputApkUri(): Uri = FileProvider.getUriForFile(app, "${app.packageName}.fileprovider", outputFile) + fun getOutputApkUri(): Uri = + FileProvider.getUriForFile(app, "${app.packageName}.fileprovider", outputFile) - fun logFileName() = "revanced_patcher_${packageName}${version?.let { "_$it" } ?: ""}_${System.currentTimeMillis()}.txt" + fun logFileName() = + "revanced_patcher_${packageName}${version?.let { "_$it" } ?: ""}_${System.currentTimeMillis()}.txt" fun prepareLogExport() = viewModelScope.launch { val logSnapshot = logs.toList() @@ -429,7 +475,8 @@ class PatcherViewModel( fun copyLogs(context: Context) = viewModelScope.launch { val logSnapshot = logs.toList() withContext(Dispatchers.Main) { - val clipboardManager = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipboardManager = + app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val content = buildLogExportText(app, logSnapshot) val clip = ClipData.newPlainText("Logs", content) clipboardManager.setPrimaryClip(clip) @@ -566,7 +613,7 @@ class PatcherViewModel( }.joinToString("\n") val logsContent = logSnapshot.joinToString("\n") { (level, msg) -> - formatLogLine(level, msg) + formatLogLine(app, level, msg) } return buildList { @@ -617,7 +664,8 @@ class PatcherViewModel( installerPkgName, packageName, input.selectedApp.version ?: withContext(Dispatchers.IO) { - pm.getPackageInfo(outputFile)?.versionName ?: error("Downloaded APK has no version name") + pm.getPackageInfo(outputFile)?.versionName + ?: error("Downloaded APK has no version name") }, InstallType.DEFAULT, input.selectedPatches, @@ -719,6 +767,12 @@ class PatcherViewModel( install(InstallType.DEFAULT) } + override fun retry() { + // Dismiss and exit to force a clean restart from the dashboard, as WorkManager state prevents in-place retries. + dismissPackageInstallerDialog() + onBack() + } + override fun reinstall() { viewModelScope.launch { try { @@ -760,7 +814,11 @@ class PatcherViewModel( LogLevel.ERROR -> Log.e(TAG, msg) } - fun formatAppLine(packageName: String, selectedVersion: String?, suggestedVersion: String?): String { + fun formatAppLine( + packageName: String, + selectedVersion: String?, + suggestedVersion: String? + ): String { val versionDetails = buildList { selectedVersion?.let { add("Selected: $it") } suggestedVersion?.let { add("Suggested: $it") } @@ -787,7 +845,8 @@ class PatcherViewModel( selectedPatches: PatchSelection, allowIncompatible: Boolean ): List { - val defaultSelection = bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include } + val defaultSelection = + bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include } val bundleNames = bundles.associate { it.uid to it.name } val knownBundleIds = bundles.map(PatchBundleInfo.Scoped::uid) val orderedBundleIds = knownBundleIds + (selectedPatches.keys + defaultSelection.keys) @@ -809,7 +868,11 @@ class PatcherViewModel( } } - fun formatLogLine(level: LogLevel, message: String) = "[${level.name}] $message" + fun formatLogLine(context: Context, level: LogLevel, message: String): String = + "${context.getString(level.displayName)}: $message" + + fun formatUiLogLine(context: Context, level: LogLevel, message: String): String = + if (level == LogLevel.INFO) message else formatLogLine(context, level, message) fun appendLog(current: String?, line: String): String = current?.takeIf { it.isNotBlank() } 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 038218727b..c9e0404d07 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 @@ -14,12 +14,12 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.R -import app.revanced.manager.domain.sources.PatchBundleSource -import app.revanced.manager.domain.sources.Source.State import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull import app.revanced.manager.domain.sources.Extensions.version +import app.revanced.manager.domain.sources.PatchBundleSource +import app.revanced.manager.domain.sources.Source.State import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection @@ -121,7 +121,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi patchSequence(allowIncompatiblePatches).any { it.include } // Don't show the warning if there are no default patches. - selectionWarningEnabled = bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches) + selectionWarningEnabled = + bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches) } } @@ -176,11 +177,12 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi } val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) } - fun selectionIsValid(bundles: List) = !readOnly && bundles.any { bundle -> - bundle.patchSequence(allowIncompatiblePatches).any { patch -> - isSelected(bundle.uid, patch) + fun selectionIsValid(bundles: List) = + !readOnly && bundles.any { bundle -> + bundle.patchSequence(allowIncompatiblePatches).any { patch -> + isSelected(bundle.uid, patch) + } } - } fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection -> selection[bundle]?.contains(patch.name) == true @@ -295,29 +297,31 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi customPatchSelection = update(currentSelection()) } - fun deselectAll(bundles: List, bundleUid: Int?) = viewModelScope.launch { - updateSelection { selection -> - bundles.fold(selection) { acc, bundle -> - if (bundleUid != null && bundle.uid != bundleUid) return@fold acc - acc.put(bundle.uid, persistentSetOf()) + fun deselectAll(bundles: List, bundleUid: Int?) = + viewModelScope.launch { + updateSelection { selection -> + bundles.fold(selection) { acc, bundle -> + if (bundleUid != null && bundle.uid != bundleUid) return@fold acc + acc.put(bundle.uid, persistentSetOf()) + } } } - } - fun invertSelection(bundles: List, bundleUid: Int?) = viewModelScope.launch { - updateSelection { selection -> - bundles.fold(selection) { acc, bundle -> - if (bundleUid != null && bundle.uid != bundleUid) return@fold acc - - val currentSelected = acc[bundle.uid] ?: persistentSetOf() - val inverted = bundle.patchSequence(allowIncompatiblePatches) - .filter { it.name !in currentSelected } - .map { it.name } - .toPersistentSet() - acc.put(bundle.uid, inverted) + fun invertSelection(bundles: List, bundleUid: Int?) = + viewModelScope.launch { + updateSelection { selection -> + bundles.fold(selection) { acc, bundle -> + if (bundleUid != null && bundle.uid != bundleUid) return@fold acc + + val currentSelected = acc[bundle.uid] ?: persistentSetOf() + val inverted = bundle.patchSequence(allowIncompatiblePatches) + .filter { it.name !in currentSelected } + .map { it.name } + .toPersistentSet() + acc.put(bundle.uid, inverted) + } } } - } fun restoreDefaults(bundleUid: Int?) = viewModelScope.launch { if (bundleUid == null) { @@ -332,14 +336,15 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi } } - fun deselectAllExcept(bundles: List, keepBundleUid: Int) = viewModelScope.launch { - updateSelection { selection -> - bundles.fold(selection) { acc, bundle -> - if (bundle.uid == keepBundleUid) return@fold acc - acc.put(bundle.uid, persistentSetOf()) + fun deselectAllExcept(bundles: List, keepBundleUid: Int) = + viewModelScope.launch { + updateSelection { selection -> + bundles.fold(selection) { acc, bundle -> + if (bundle.uid == keepBundleUid) return@fold acc + acc.put(bundle.uid, persistentSetOf()) + } } } - } companion object { const val SHOW_INCOMPATIBLE = 1 // 2^0 diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 2ed8f29774..3f840458de 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -31,15 +31,15 @@ import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.downloader.DownloaderHostApi +import app.revanced.manager.downloader.GetScope +import app.revanced.manager.downloader.Scope +import app.revanced.manager.downloader.UserInteractionException import app.revanced.manager.network.downloader.LoadedDownloader import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection -import app.revanced.manager.downloader.GetScope -import app.revanced.manager.downloader.DownloaderHostApi -import app.revanced.manager.downloader.Scope -import app.revanced.manager.downloader.UserInteractionException import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo @@ -51,7 +51,6 @@ import app.revanced.manager.util.isSplitApk import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast -import java.nio.file.Files import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers @@ -66,6 +65,7 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import org.koin.core.component.KoinComponent import org.koin.core.component.get +import java.nio.file.Files @OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class) class SelectedAppInfoViewModel( @@ -290,7 +290,8 @@ class SelectedAppInfoViewModel( } private fun loadSelectedStorageFile(uri: Uri) = - app.contentResolver.getType(uri)?.takeIf { it == APK_MIMETYPE || it.startsWith("application/") } + app.contentResolver.getType(uri) + ?.takeIf { it == APK_MIMETYPE || it.startsWith("application/") } ?.let { app.contentResolver.openInputStream(uri)?.use { stream -> with(sourceInputFile) { @@ -464,7 +465,8 @@ class SelectedAppInfoViewModel( bundleOptions.forEach patch@{ (patchName, values) -> // Get all valid option keys for the patch. val validOptionKeys = - patches[patchName]?.options?.map { it.name }?.toSet() ?: return@patch + patches[patchName]?.options?.map { it.name }?.toSet() + ?: return@patch this@bundleOptions[patchName] = values.filterKeys { key -> key in validOptionKeys 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 e4e3703136..31efcd18d6 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -130,16 +130,18 @@ class PM( } fun PackageInfo.label() = this.applicationInfo!!.loadLabel(app.packageManager).toString() - fun getResources(packageInfo: PackageInfo) = app.packageManager.getResourcesForApplication(packageInfo.applicationInfo!!) + fun getResources(packageInfo: PackageInfo) = + app.packageManager.getResourcesForApplication(packageInfo.applicationInfo!!) fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) - suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) { - uninstaller.createSession(pkg) { - confirmation = Confirmation.IMMEDIATE - config() - }.await() - } + suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = + withContext(Dispatchers.IO) { + uninstaller.createSession(pkg) { + confirmation = Confirmation.IMMEDIATE + config() + }.await() + } fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt index b5060999a1..5954bba488 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -10,7 +10,8 @@ import org.koin.core.component.inject object RequestInstallAppsContract : ActivityResultContract(), KoinComponent { private val pm: PM by inject() - override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) + override fun createIntent(context: Context, input: String) = + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) override fun parseResult(resultCode: Int, intent: Intent?): Boolean { return pm.canInstallPackages() diff --git a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt index 67dce3d463..6dbd01ea50 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt @@ -10,10 +10,19 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.RequiresApi @RequiresApi(Build.VERSION_CODES.R) -class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract() { - override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null)) +class RequestManageStorageContract(private val forceLaunch: Boolean = false) : + ActivityResultContract() { + override fun createIntent(context: Context, input: String) = Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.fromParts("package", context.packageName, null) + ) - override fun getSynchronousResult(context: Context, input: String): SynchronousResult? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null + override fun getSynchronousResult( + context: Context, + input: String + ): SynchronousResult? = + if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null - override fun parseResult(resultCode: Int, intent: Intent?) = Environment.isExternalStorageManager() + override fun parseResult(resultCode: Int, intent: Intent?) = + Environment.isExternalStorageManager() } \ No newline at end of file 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 a52ab6078c..c3ceae69b4 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -23,8 +23,8 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberUpdatedState @@ -33,12 +33,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.core.graphics.scale import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavBackStackEntry @@ -77,6 +82,7 @@ fun Context.openUrl(url: String) { flags = Intent.FLAG_ACTIVITY_NEW_TASK }) } + fun Context.shareApk(apkUri: Uri) { startActivity( Intent.createChooser(Intent(Intent.ACTION_SEND).apply { @@ -179,6 +185,7 @@ fun LocalDateTime.relativeTime(context: Context): String { R.string.hours_ago, duration.inWholeHours.toString() ) + duration.inWholeDays < 30 -> context.getString( R.string.days_ago, duration.inWholeDays.toString() @@ -388,4 +395,20 @@ fun blurBackground( rs.destroy() return workingBitmap -} \ No newline at end of file +} + +private val boldRegex = Regex("") + +internal fun formatStyled(text: String): AnnotatedString = buildAnnotatedString { + text.split(boldRegex).forEachIndexed { index, part -> + if (part.isEmpty()) return@forEachIndexed + + if (index % 2 == 0) { + append(part) + } else { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(part) + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt index 2a1418cdf6..3ec5a96883 100644 --- a/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt +++ b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt @@ -1,7 +1,12 @@ package app.revanced.manager.util.saver import androidx.compose.runtime.saveable.Saver -import kotlinx.collections.immutable.* +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.collections.immutable.toPersistentSet /** * Create a [Saver] for [PersistentList]s. diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index c4a15553e3..9f64c46b11 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -14,9 +14,8 @@ - + android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z" /> + + android:color="#FFF04E98" /> + android:color="#FF5F65D4" /> + android:color="#FF4E98F0" /> diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml index 7d34e15418..112bd5ea23 100644 --- a/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -13,9 +13,9 @@ + android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z" /> + android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,0 63.62,0H192.37C195.32,0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z" /> diff --git a/app/src/main/res/drawable/ic_launcher_splash_screen.xml b/app/src/main/res/drawable/ic_launcher_splash_screen.xml index 7c35b2f62d..a9ad879e02 100644 --- a/app/src/main/res/drawable/ic_launcher_splash_screen.xml +++ b/app/src/main/res/drawable/ic_launcher_splash_screen.xml @@ -14,9 +14,8 @@ - + android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z" /> + + android:color="#FFF04E98" /> + android:color="#FF5F65D4" /> + android:color="#FF4E98F0" /> diff --git a/app/src/main/res/drawable/ic_logo_ring.xml b/app/src/main/res/drawable/ic_logo_ring.xml index 150272260d..ac7e5761c5 100644 --- a/app/src/main/res/drawable/ic_logo_ring.xml +++ b/app/src/main/res/drawable/ic_logo_ring.xml @@ -10,7 +10,7 @@ android:fillColor="#FF1B1B1B" android:fillType="evenOdd" android:pathData="M400,400m-400,0a400,400 0,1 1,800 0a400,400 0,1 1,-800 0" - android:strokeLineJoin="round"/> + android:strokeLineJoin="round" /> + android:color="#FFF04E98" /> + android:color="#FF5F65D4" /> + android:color="#FF4E98F0" /> @@ -38,7 +38,7 @@ android:fillColor="#FFFFFFFF" android:fillType="evenOdd" android:pathData="M538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37 -2.02,-3.09 -5.46,-4.95 -9.16,-4.95h-14.16c-3.1,0 -5.91,1.83 -7.15,4.67 -12.47,28.4 -78.27,178.27 -100.25,228.33 -1.25,2.84 -4.05,4.67 -7.15,4.67 -3.1,0 -5.91,-1.83 -7.15,-4.67 -21.98,-50.06 -87.78,-199.93 -100.25,-228.33 -1.25,-2.84 -4.05,-4.67 -7.15,-4.67h-14.16c-3.69,0 -7.14,1.86 -9.16,4.95 -2.02,3.09 -2.34,6.99 -0.86,10.37 23.56,53.77 101.87,232.52 117.87,269.03 1.74,3.98 5.67,6.55 10.02,6.55h21.7c4.34,0 8.27,-2.57 10.02,-6.55 16,-36.51 94.32,-215.27 117.87,-269.03z" - android:strokeLineJoin="round"/> + android:strokeLineJoin="round" /> + android:color="#FFF04E98" /> + android:color="#FF5F65D4" /> + android:color="#FF4E98F0" /> diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml index ff824278ef..43c60944da 100644 --- a/app/src/main/res/drawable/ic_notification.xml +++ b/app/src/main/res/drawable/ic_notification.xml @@ -7,5 +7,5 @@ + android:pathData="M4.03516,3C3.80859,3 3.59375,3.11719 3.46875,3.30469C3.34375,3.49609 3.32422,3.73828 3.41406,3.94922C4.87109,7.27344 9.71875,18.3359 10.707,20.5938C10.8164,20.8398 11.0586,21 11.3281,21H12.6719C12.9375,21 13.1836,20.8398 13.293,20.5938C14.2813,18.3359 19.1289,7.27344 20.5859,3.94922C20.6758,3.73828 20.6563,3.49609 20.5313,3.30469C20.4063,3.11719 20.1914,3 19.9648,3H19.0898C18.8984,3 18.7227,3.11328 18.6445,3.28906C17.875,5.04688 13.8008,14.3203 12.4414,17.418C12.3672,17.5938 12.1914,17.707 12,17.707C11.8086,17.707 11.6328,17.5938 11.5586,17.418C10.1992,14.3203 6.125,5.04688 5.35547,3.28906C5.27734,3.11328 5.10156,3 4.91016,3H7.47266C7.26563,3 7.07422,3.10938 6.97266,3.28906C6.86719,3.46875 6.86719,3.69141 6.97266,3.87109C8.02344,5.69141 10.4453,9.88672 11.4961,11.7109C11.6016,11.8906 11.793,12 12,12C12.207,12 12.3984,11.8906 12.5039,11.7109C13.5547,9.88672 15.9766,5.69141 17.0273,3.87109C17.1328,3.69141 17.1328,3.46875 17.0273,3.28906C16.9258,3.10938 16.7344,3 16.5273,3H7.47266" /> diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 10bc7a5de4..c78bee3b53 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,6 +1,6 @@ - - - + + + diff --git a/app/src/main/res/values-az-rAZ/strings.xml b/app/src/main/res/values-az-rAZ/strings.xml index e972206661..526fd47219 100644 --- a/app/src/main/res/values-az-rAZ/strings.xml +++ b/app/src/main/res/values-az-rAZ/strings.xml @@ -477,7 +477,7 @@ Bu mənbədən əvvəlcədən quraşdırılmış tətbiqləri yeniləyə bilməy Saxla (%1$s) Yenilə Boş - Xahiş edildikdə Yenilə düyməsinə toxunun.\nReVanced Manager yenilənərkən bağlanacaq. + Yenilə düyməsinə toxunun.\nReVanced Manager yenilənərkən bağlanacaq.]]> Dəyişiklik qeydləri tapılmadı İndi %s dəq əvvəl diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index fcdc12962d..22d2c0c6b6 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -480,7 +480,7 @@ Nebudete moci aktualizovat dříve nainstalované aplikace z tohoto zdroje.Uložit (%1$s) Aktualizovat Prázdné - Klepněte na Aktualizovat, když budete vyzváni.\nReVanced Manager se při aktualizaci zavře. + Aktualizovat, když budete vyzváni.\nReVanced Manager se při aktualizaci zavře.]]> Nebyly nalezeny žádné protokoly změn Právě teď před %s min diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index f627cca11c..552afddbfa 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -480,7 +480,7 @@ Du vil ikke kunne opdatere tidligere installerede apps fra denne kilde. Gem (%1$s) Opdater Tom - Tryk på Opdater, når du bliver bedt om det.\nReVanced Manager vil lukke under opdatering. + Opdater, når du bliver bedt om det.\nReVanced Manager vil lukke under opdatering.]]> Ingen ændringslogger fundet Lige nu for %s min siden diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml index 5b2963e818..47e68843a3 100644 --- a/app/src/main/res/values-hy-rAM/strings.xml +++ b/app/src/main/res/values-hy-rAM/strings.xml @@ -480,7 +480,7 @@ Second \"item\" text" Պահպանել (%1$s) Թարմացնել Դատարկ - Հպեք Թարմացնել, երբ հուշում հայտնվի:\nReVanced Manager-ը կփակվի թարմացնելիս: + Թարմացնել, երբ հուշում հայտնվի:\nReVanced Manager-ը կփակվի թարմացնելիս:]]> Փոփոխությունների մատյաններ չեն գտնվել Հենց հիմա %s րոպե առաջ diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index e34598462a..9cf89438bd 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -467,7 +467,7 @@ Second \"item\" text" Simpan (%1$s) Perbarui Kosong - Ketuk Perbarui saat diminta. ReVanced Manager akan menutup saat memperbarui. + Perbarui saat diminta. ReVanced Manager akan menutup saat memperbarui.]]> Tidak ada daftar perubahan ditemukan Baru saja %s mnt lalu diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 4365b5d388..aaaba04157 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -480,7 +480,7 @@ U zult de eerder geïnstalleerde apps niet meer kunnen bijwerken via deze bron.< Opslaan (%1$s) Update Leeg - Tik op Updaten wanneer daarom wordt gevraagd.\nReVanced Manager wordt gesloten tijdens het updaten. + Updaten wanneer daarom wordt gevraagd.\nReVanced Manager wordt gesloten tijdens het updaten.]]> Geen wijzigingslogs gevonden Zojuist %sm geleden diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 3e359d6fac..65cf4d37c6 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -470,7 +470,7 @@ Second \"item\" text" Сохранить (%1$s) Обновить Пусто - Нажмите Обновить по запросу.\nReVanced Manager закроется при обновлении. + Обновить по запросу.\nReVanced Manager закроется при обновлении.]]> Список изменений не найден Только что %sm назад diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index 7969acbd3a..fa1c667aaf 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -468,7 +468,7 @@ Second \"item\" text" Ruaj (%1$s) Përditëso Bosh - Prekni Përditëso kur të kërkohet.\nReVanced Manager do të mbyllet gjatë përditësimit. + Përditëso kur të kërkohet.\nReVanced Manager do të mbyllet gjatë përditësimit.]]> Asnjë ndryshim nuk u gjet Tani %s min më parë diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 92532ca2ea..fe19e1b6f6 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -487,7 +487,7 @@ Den är endast kompatibel med följande version(er): %2$s Spara (%1$s) Uppdatera Tom - Tryck på Uppdatera när du blir ombedd.\nReVanced Manager stängs när den uppdateras. + Uppdatera när du blir ombedd.\nReVanced Manager stängs när den uppdateras.]]> Inga ändringsloggar hittades Just nu för %s min. sedan diff --git a/app/src/main/res/values-th-rTH/strings.xml b/app/src/main/res/values-th-rTH/strings.xml index d837b5cdc4..7497e7faa5 100644 --- a/app/src/main/res/values-th-rTH/strings.xml +++ b/app/src/main/res/values-th-rTH/strings.xml @@ -477,7 +477,7 @@ Second \"item\" text" บันทึก (%1$s) อัปเดต ว่างเปล่า - แตะที่ อัปเดต เมื่อมีข้อความแจ้ง\nReVanced Manager จะปิดตัวลงเมื่อมีการอัปเดต + อัปเดต เมื่อมีข้อความแจ้ง\nReVanced Manager จะปิดตัวลงเมื่อมีการอัปเดต]]> ไม่พบบันทึกการเปลี่ยนแปลง เมื่อสักครู่นี้ %s นาทีที่แล้ว diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index cf733f6b60..ced1ad3b8f 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -477,7 +477,7 @@ Second \"item\" text" Зберегти (%1$s) Оновити Пусто - Натисніть Оновити, коли з\'явиться запит.\nReVanced Manager закриється під час оновлення. + Оновити, коли з\'явиться запит.\nReVanced Manager закриється під час оновлення.]]> Список змін не знайдено Щойно %s хв тому diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index 6b9d4f857b..ff1d4e3af8 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -481,7 +481,7 @@ Nó chỉ tương thích với các phiên bản sau: %2$s Lưu (%1$s) Cập nhật Trống - Nhấn vào Cập nhật khi được nhắc.\nReVanced Manager sẽ đóng khi cập nhật. + Cập nhật khi được nhắc.\nReVanced Manager sẽ đóng khi cập nhật.]]> Không tìm thấy nhật ký thay đổi Vừa xong %s phút trước diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7e4438390b..b098a4d522 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -487,7 +487,7 @@ Second \"item\" text" 儲存 (%1$s) 更新 空白 - 出現提示時輕觸更新。\n更新時 ReVanced Manager 將關閉。 + 更新。\n更新時 ReVanced Manager 將關閉。]]> 未找到變更日誌 剛才 %s 分鐘前 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09a8f10a2f..667150a2fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,8 +25,8 @@ Second \"item\" text" Patches can’t be downloaded. Check your internet connection and try again. Configure automatic updates to keep ReVanced Manager and patches up to date - Patches and downloaders couldn’t be downloaded during setup. Tap Update to download them. - Patches and downloaders can’t be updated on a metered network. Tap Update to download them. + Update to download them.]]> + Update to download them.]]> ReVanced Manager will connect to %s in order to download initial versions if your device is connected to the internet Retry @@ -219,7 +219,7 @@ You won’t be able to update apps that were signed with the previous keystore.< Add downloader from local storage Downloader can receive updates These downloaders are missing - Tap Update to fix this issue + Update to fix this issue]]> Failed to update downloader: %s Failed to import downloader: %s @@ -270,7 +270,7 @@ You won’t be able to update apps that were signed with the previous keystore.< Minimum log level Trace Info - Warn + Warning Error Export debug logs Failed to read logs (exit code %d) @@ -292,6 +292,8 @@ You won’t be able to update apps that were signed with the previous keystore.< Force download all patches Reset patches Reset downloaders + Clear downloaded assets cache + Cleared downloaded assets cache Reset onboarding Show the onboarding screen on next app launch Reset announcement read @@ -566,6 +568,10 @@ It’s only compatible with these versions: %2$s Install invalid Not enough storage Timed out + Patcher was killed + The patcher subprocess was terminated by the system, usually due to low memory. Close other apps or reduce the number of selected patches and try again. + Patcher crashed + The patcher subprocess crashed. This may be caused by a bug, or a malformed input APK. Try a different version of the app, fewer patches, or report the issue with exported logs. There was a problem installing the app Installation was canceled Installation was blocked. Try adjusting the security settings of your device, and try again. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 20a6e351cc..e4c393705a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,6 @@ +