mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-11 13:46:17 +00:00
Compare commits
28 Commits
v1.26.0-de
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7615453eec | ||
|
|
4c0b6b02e9 | ||
|
|
fe84b22b6f | ||
|
|
1b21f5d4ab | ||
|
|
72b1db9a2f | ||
|
|
2805ac6540 | ||
|
|
b16931ca79 | ||
|
|
dfeca09d00 | ||
|
|
44c06e2197 | ||
|
|
df31b39cc8 | ||
|
|
25d82e869c | ||
|
|
9d9a0e81db | ||
|
|
ffa42099e3 | ||
|
|
11dd6e4064 | ||
|
|
35fb59b31d | ||
|
|
18a4df9af9 | ||
|
|
bd69b45a69 | ||
|
|
0d26df03f4 | ||
|
|
c436a7a100 | ||
|
|
dbb6c01e89 | ||
|
|
e0d529c2df | ||
|
|
0300da9eac | ||
|
|
4e5057ecad | ||
|
|
1ef5c1c5c5 | ||
|
|
39f52c1242 | ||
|
|
0c62837454 | ||
|
|
3d75ffe6a7 | ||
|
|
d188d5a410 |
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,3 +1,7 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: 'feat: '
|
||||
labels: ['Feature request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
43
.github/workflows/pull_strings.yml
vendored
Normal file
43
.github/workflows/pull_strings.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Pull strings
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
pull:
|
||||
name: Pull strings
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: dev
|
||||
clean: true
|
||||
|
||||
- name: Pull strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: feat/translations
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Open pull request
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: feat/translations
|
||||
destination_branch: dev
|
||||
pr_title: "chore: Sync translations"
|
||||
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||
26
.github/workflows/push_strings.yml
vendored
Normal file
26
.github/workflows/push_strings.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Push strings
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- app/src/main/res/values/strings.xml
|
||||
|
||||
jobs:
|
||||
push:
|
||||
name: Push strings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Push strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -24,14 +24,6 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||
public static final field $stable I
|
||||
}
|
||||
@@ -85,14 +77,6 @@ public final class app/revanced/manager/plugin/downloader/Package : android/os/P
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||
}
|
||||
|
||||
@@ -159,14 +143,6 @@ public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEve
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -17,9 +19,16 @@ dependencies {
|
||||
implementation(libs.appcompat)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
@@ -42,10 +51,6 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,78 @@
|
||||
# app [1.26.0-dev.20](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.19...v1.26.0-dev.20) (2026-01-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Save FAB freaking out in select patches screen ([4c0b6b0](https://github.com/ReVanced/revanced-manager/commit/4c0b6b02e95a8d6f655bcf5c25493b1f9a4a4dcd))
|
||||
|
||||
# app [1.26.0-dev.19](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.18...v1.26.0-dev.19) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **locales:** use buildconfig instead of generating kt file ([72b1db9](https://github.com/ReVanced/revanced-manager/commit/72b1db9a2f33ab5d5fffd8ba83c05901eff19bea))
|
||||
|
||||
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prevent trailing comma when no locales are generated ([b16931c](https://github.com/ReVanced/revanced-manager/commit/b16931ca79d5ce4d17c75f6dd3bf6f976b8ff7be))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](https://github.com/ReVanced/revanced-manager/commit/df31b39cc8c1fbf00bc3301468e8e7e4b283caf2))
|
||||
|
||||
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
|
||||
|
||||
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
|
||||
|
||||
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
||||
|
||||
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||
|
||||
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
|
||||
|
||||
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve trust plugin dialog design ([#2420](https://github.com/ReVanced/revanced-manager/issues/2420)) ([0300da9](https://github.com/ReVanced/revanced-manager/commit/0300da9eac6c0fc29dbbb66622c0d52f4cf68934))
|
||||
|
||||
# app [1.26.0-dev.11](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.10...v1.26.0-dev.11) (2025-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add pure black theme ([#2824](https://github.com/ReVanced/revanced-manager/issues/2824)) ([3d75ffe](https://github.com/ReVanced/revanced-manager/commit/3d75ffe6a7a39efdebe13dbd07c937c1de409ead))
|
||||
|
||||
# app [1.26.0-dev.10](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.9...v1.26.0-dev.10) (2025-10-06)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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 {
|
||||
@@ -9,6 +12,7 @@ plugins {
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.devtools)
|
||||
alias(libs.plugins.about.libraries)
|
||||
alias(libs.plugins.about.libraries.android)
|
||||
signing
|
||||
}
|
||||
|
||||
@@ -81,7 +85,8 @@ dependencies {
|
||||
implementation(libs.koin.workmanager)
|
||||
|
||||
// Licenses
|
||||
implementation(libs.about.libraries)
|
||||
implementation(libs.about.libraries.core)
|
||||
implementation(libs.about.libraries.m3)
|
||||
|
||||
// Ktor
|
||||
implementation(libs.ktor.core)
|
||||
@@ -108,6 +113,10 @@ dependencies {
|
||||
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
|
||||
// Ackpine
|
||||
implementation(libs.ackpine.core)
|
||||
implementation(libs.ackpine.ktx)
|
||||
}
|
||||
|
||||
buildscript {
|
||||
@@ -122,7 +131,7 @@ buildscript {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
defaultConfig {
|
||||
@@ -139,13 +148,25 @@ android {
|
||||
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||
}
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
val resDir = file("src/main/res")
|
||||
val locales = resDir.listFiles()
|
||||
.orEmpty()
|
||||
//noinspection WrongGradleMethod
|
||||
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
|
||||
//noinspection WrongGradleMethod
|
||||
.map { it.name.removePrefix("values-").replace("-r", "-") }
|
||||
.sorted()
|
||||
//noinspection WrongGradleMethod
|
||||
.joinToString(prefix = "{", separator = ",", postfix = "}") { "\"$it\"" }
|
||||
|
||||
buildConfigField("String[]", "SUPPORTED_LOCALES", locales)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
@@ -217,20 +238,14 @@ android {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -243,6 +258,18 @@ android {
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
library {
|
||||
// Enable the duplication mode, allows to merge, or link dependencies which relate
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
// Configure the duplication rule, to match "duplicates" with
|
||||
duplicationRule = DuplicateRule.EXACT
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.10
|
||||
version = 1.26.0-dev.20
|
||||
|
||||
@@ -51,9 +51,6 @@
|
||||
|
||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
@@ -75,5 +72,15 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,4 @@
|
||||
// ProgressEventParcel.aidl
|
||||
package app.revanced.manager.patcher;
|
||||
|
||||
parcelable ProgressEventParcel;
|
||||
@@ -1,11 +1,12 @@
|
||||
// IPatcherEvents.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||
|
||||
// Interface for sending events back to the main app process.
|
||||
oneway interface IPatcherEvents {
|
||||
void log(String level, String msg);
|
||||
void patchSucceeded();
|
||||
void progress(String name, String state, String msg);
|
||||
void event(in ProgressEventParcel event);
|
||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||
void finished(String exceptionStackTrace);
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package app.revanced.manager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
@@ -59,11 +59,10 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -81,6 +80,7 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
val pureBlackTheme by vm.prefs.pureBlackTheme.getAsState()
|
||||
|
||||
EventEffect(vm.legacyImportActivityFlow) {
|
||||
try {
|
||||
@@ -91,7 +91,8 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor
|
||||
dynamicColor = dynamicColor,
|
||||
pureBlackTheme = pureBlackTheme
|
||||
) {
|
||||
ReVancedManager(vm)
|
||||
}
|
||||
@@ -183,7 +184,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
@@ -241,7 +242,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
|
||||
workerModule,
|
||||
viewModelModule,
|
||||
databaseModule,
|
||||
rootModule
|
||||
rootModule,
|
||||
ackpineModule
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
|
||||
/**
|
||||
* Returns true if it is safe to download large files.
|
||||
*/
|
||||
fun isSafe() = isConnected() && isUnmetered()
|
||||
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
|
||||
}
|
||||
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
|
||||
val ackpineModule = module {
|
||||
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||
|
||||
single {
|
||||
provideInstaller(androidContext())
|
||||
}
|
||||
single {
|
||||
provideUninstaller(androidContext())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.ui.viewmodel.*
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.*
|
||||
import org.koin.dsl.module
|
||||
|
||||
val viewModelModule = module {
|
||||
|
||||
@@ -9,6 +9,7 @@ class PreferencesManager(
|
||||
context: Context
|
||||
) : BasePreferencesManager(context, "settings") {
|
||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||
val pureBlackTheme = booleanPreference("pure_black_theme", false)
|
||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||
|
||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||
@@ -33,4 +34,6 @@ class PreferencesManager(
|
||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||
|
||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||
|
||||
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
|
||||
}
|
||||
|
||||
@@ -286,28 +286,29 @@ class PatchBundleRepository(
|
||||
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||
}
|
||||
|
||||
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
||||
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||
try {
|
||||
createStream().use { patches -> replace(patches) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(tag, "Got exception while importing bundle", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||
suspend fun createLocal(createStream: suspend () -> InputStream) =
|
||||
dispatchAction("Add bundle") {
|
||||
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||
try {
|
||||
createStream().use { patches -> replace(patches) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(tag, "Got exception while importing bundle", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||
}
|
||||
|
||||
deleteLocalFile()
|
||||
}
|
||||
|
||||
deleteLocalFile()
|
||||
}
|
||||
}
|
||||
|
||||
doReload()
|
||||
}
|
||||
doReload()
|
||||
}
|
||||
|
||||
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||
dispatchAction("Add bundle ($url)") { state ->
|
||||
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||
update(src)
|
||||
update(src, force = true)
|
||||
state.copy(sources = state.sources.put(src.uid, src))
|
||||
}
|
||||
|
||||
@@ -329,32 +330,38 @@ class PatchBundleRepository(
|
||||
state.copy(sources = state.sources.put(uid, newSrc))
|
||||
}
|
||||
|
||||
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||
suspend fun update(
|
||||
vararg sources: RemotePatchBundle,
|
||||
showToast: Boolean = false,
|
||||
force: Boolean = false
|
||||
) {
|
||||
val uids = sources.map { it.uid }.toSet()
|
||||
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
|
||||
}
|
||||
|
||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
|
||||
|
||||
/**
|
||||
* Updates all bundles that should be automatically updated.
|
||||
*/
|
||||
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
||||
suspend fun updateCheck() =
|
||||
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
|
||||
|
||||
private inner class Update(
|
||||
private val force: Boolean = false,
|
||||
private val redownload: Boolean = false,
|
||||
private val showToast: Boolean = false,
|
||||
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||
) : Action<State> {
|
||||
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||
|
||||
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
|
||||
|
||||
override suspend fun ActionContext.execute(
|
||||
current: State
|
||||
) = coroutineScope {
|
||||
if (!networkInfo.isSafe()) {
|
||||
if (!networkInfo.isSafe(force)) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@coroutineScope current
|
||||
}
|
||||
@@ -367,7 +374,7 @@ class PatchBundleRepository(
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
|
||||
val newVersion = with(it) {
|
||||
if (force) downloadLatest() else update()
|
||||
if (redownload) downloadLatest() else update()
|
||||
} ?: return@async null
|
||||
|
||||
it to newVersion
|
||||
|
||||
@@ -16,8 +16,11 @@ import io.ktor.http.isSuccess
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.core.isNotEmpty
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import io.ktor.utils.io.exhausted
|
||||
import io.ktor.utils.io.readRemaining
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
@@ -69,14 +72,12 @@ class HttpService(
|
||||
) {
|
||||
http.prepareGet(builder).execute { httpResponse ->
|
||||
if (httpResponse.status.isSuccess()) {
|
||||
val channel: ByteReadChannel = httpResponse.body()
|
||||
withContext(Dispatchers.IO) {
|
||||
while (!channel.isClosedForRead) {
|
||||
val channel: ByteReadChannel = httpResponse.body()
|
||||
val sink = outputStream.asSink()
|
||||
while (!channel.exhausted()) {
|
||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||
while (packet.isNotEmpty) {
|
||||
val bytes = packet.readBytes()
|
||||
outputStream.write(bytes)
|
||||
}
|
||||
packet.transferTo(sink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
sealed class ProgressEvent : Parcelable {
|
||||
abstract val stepId: StepId?
|
||||
|
||||
data class Started(override val stepId: StepId) : ProgressEvent()
|
||||
|
||||
data class Progress(
|
||||
override val stepId: StepId,
|
||||
val current: Long? = null,
|
||||
val total: Long? = null,
|
||||
val message: String? = null,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Completed(
|
||||
override val stepId: StepId,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Failed(
|
||||
override val stepId: StepId?,
|
||||
val error: RemoteError,
|
||||
) : ProgressEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable wrapper for [ProgressEvent].
|
||||
*
|
||||
* Required because AIDL does not support sealed classes.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
|
||||
|
||||
fun ProgressEventParcel.toEvent(): ProgressEvent = event
|
||||
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
|
||||
|
||||
@Parcelize
|
||||
sealed class StepId : Parcelable {
|
||||
data object DownloadAPK : StepId()
|
||||
data object LoadPatches : StepId()
|
||||
data object ReadAPK : StepId()
|
||||
data object ExecutePatches : StepId()
|
||||
data class ExecutePatch(val index: Int) : StepId()
|
||||
data object WriteAPK : StepId()
|
||||
data object SignAPK : StepId()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class RemoteError(
|
||||
val type: String,
|
||||
val message: String?,
|
||||
val stackTrace: String,
|
||||
) : Parcelable
|
||||
|
||||
fun Exception.toRemoteError() = RemoteError(
|
||||
type = this::class.java.name,
|
||||
message = this.message,
|
||||
stackTrace = this.stackTraceToString(),
|
||||
)
|
||||
|
||||
|
||||
inline fun <T> runStep(
|
||||
stepId: StepId,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
block: () -> T,
|
||||
): T = try {
|
||||
onEvent(ProgressEvent.Started(stepId))
|
||||
val value = block()
|
||||
onEvent(ProgressEvent.Completed(stepId))
|
||||
value
|
||||
} catch (error: Exception) {
|
||||
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
|
||||
throw error
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.Session.Companion.component1
|
||||
import app.revanced.manager.patcher.Session.Companion.component2
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
@@ -22,15 +21,10 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
private val androidContext: Context,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
private val onEvent: (ProgressEvent) -> Unit,
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
onProgress(name, state, message)
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
@@ -42,86 +36,68 @@ class Session(
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
this().collect { (patch, exception) ->
|
||||
if (patch !in selectedPatches) return@collect
|
||||
val index = selectedPatches.indexOf(patch)
|
||||
if (index == -1) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
)
|
||||
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
|
||||
nextPatchIndex++
|
||||
|
||||
onPatchCompleted()
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
||||
onEvent(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_executed,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
runStep(StepId.ExecutePatches, onEvent) {
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
addHandler(logger.handler)
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
runStep(StepId.WriteAPK, onEvent) {
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import java.io.File
|
||||
@@ -13,7 +14,7 @@ import java.io.File
|
||||
/**
|
||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||
*/
|
||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patchList
|
||||
}
|
||||
|
||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onEvent,
|
||||
)
|
||||
}
|
||||
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
context,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onPatchCompleted = onPatchCompleted,
|
||||
onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
session.use { s ->
|
||||
s.run(
|
||||
File(outputFile),
|
||||
patchList
|
||||
)
|
||||
|
||||
@@ -10,12 +10,13 @@ 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.Parameters
|
||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.patcher.toEvent
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
override fun event(event: ProgressEventParcel?) {
|
||||
event?.let { onEvent(it.toEvent()) }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
||||
override fun finished(exceptionStackTrace: String?) {
|
||||
binder.exit()
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.content.Context
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
)
|
||||
}
|
||||
@@ -8,12 +8,15 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.patcher.toParcel
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
|
||||
/**
|
||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||
*/
|
||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
private var eventBinder: IPatcherEvents? = null
|
||||
|
||||
private val scope =
|
||||
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
override fun exit() = exitProcess(0)
|
||||
|
||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||
|
||||
eventBinder = events
|
||||
|
||||
scope.launch {
|
||||
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||
val allPatches = PatchBundle.Loader.patches(
|
||||
parameters.configurations.map { it.bundle },
|
||||
parameters.packageName
|
||||
)
|
||||
|
||||
parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
.filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patches.values
|
||||
patches.values
|
||||
}
|
||||
}
|
||||
|
||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onEvent = ::onEvent,
|
||||
)
|
||||
}
|
||||
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
androidContext = context,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onPatchCompleted = { events.patchSucceeded() },
|
||||
onProgress = { name, state, message ->
|
||||
events.progress(name, state?.name, message)
|
||||
}
|
||||
).use {
|
||||
session.use {
|
||||
it.run(File(parameters.outputFile), patchList)
|
||||
}
|
||||
|
||||
@@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
val ipcInterface = PatcherProcess()
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||
|
||||
@@ -29,14 +29,17 @@ 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.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
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.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class PatcherWorker(
|
||||
context: Context,
|
||||
@@ -71,11 +72,9 @@ class PatcherWorker(
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onProgress: ProgressEventHandler
|
||||
val onEvent: (ProgressEvent) -> Unit,
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
@@ -140,10 +139,6 @@ class PatcherWorker(
|
||||
}
|
||||
|
||||
private suspend fun runPatcher(args: Args): Result {
|
||||
|
||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
args.onProgress(name, state, message)
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
@@ -163,51 +158,65 @@ class PatcherWorker(
|
||||
args.input.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
onDownload = { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
)
|
||||
}
|
||||
).also { args.setInputFile(it) }
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
|
||||
selectedApp.data
|
||||
)
|
||||
|
||||
download(plugin, data)
|
||||
download(plugin, data)
|
||||
}
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = applicationContext.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result = args.handleStartActivityRequest(plugin, 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
|
||||
)
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName =
|
||||
applicationContext.packageName
|
||||
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result =
|
||||
args.handleStartActivityRequest(plugin, 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) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
@@ -227,12 +236,12 @@ class PatcherWorker(
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
args.onEvent,
|
||||
)
|
||||
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
runStep(StepId.SignAPK, args.onEvent) {
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
}
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
Result.success()
|
||||
@@ -241,11 +250,11 @@ class PatcherWorker(
|
||||
tag,
|
||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||
)
|
||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
Result.failure()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class InstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||
|
||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import coil.compose.AsyncImage
|
||||
import io.github.fornewid.placeholder.material3.placeholder
|
||||
import com.eygraber.compose.placeholder.material3.placeholder
|
||||
|
||||
@Composable
|
||||
fun AppIcon(
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import app.revanced.manager.R
|
||||
import io.github.fornewid.placeholder.material3.placeholder
|
||||
import com.eygraber.compose.placeholder.material3.placeholder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
|
||||
enum class DialogKind(
|
||||
val flag: Int,
|
||||
val title: Int,
|
||||
@StringRes val contentStringResId: Int,
|
||||
@param:StringRes val contentStringResId: Int,
|
||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||
val dismissButton: InstallerStatusDialogButton? = null,
|
||||
@@ -133,10 +134,8 @@ enum class DialogKind(
|
||||
title = R.string.installation_storage_issue_dialog_title,
|
||||
contentStringResId = R.string.installation_storage_issue_description,
|
||||
),
|
||||
|
||||
@RequiresApi(34)
|
||||
FAILURE_TIMEOUT(
|
||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
title = R.string.installation_timeout_dialog_title,
|
||||
contentStringResId = R.string.installation_timeout_description,
|
||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||
|
||||
@@ -14,7 +14,7 @@ fun LoadingIndicator(
|
||||
progress: () -> Float? = { null },
|
||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||
) {
|
||||
progress()?.let {
|
||||
|
||||
@@ -18,8 +18,6 @@ fun Markdown(
|
||||
colors = markdownColor(
|
||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||
codeText = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
linkText = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
typography = markdownTypography(
|
||||
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||
|
||||
@@ -29,7 +29,8 @@ fun SearchBar(
|
||||
) {
|
||||
val colors = SearchBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
dividerColor = MaterialTheme.colorScheme.outline
|
||||
dividerColor = MaterialTheme.colorScheme.outline,
|
||||
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
||||
)
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ fun SearchView(
|
||||
) {
|
||||
val colors = SearchBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
dividerColor = MaterialTheme.colorScheme.outline
|
||||
dividerColor = MaterialTheme.colorScheme.outline,
|
||||
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
||||
)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.manager.ui.component.patcher
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
@@ -21,6 +20,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
|
||||
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.ProgressKey
|
||||
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.StepProgressProvider
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -52,21 +50,10 @@ import kotlin.math.floor
|
||||
fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
stepProgressProvider: StepProgressProvider
|
||||
isExpanded: Boolean = false,
|
||||
onExpand: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
val categoryColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
||||
label = "category"
|
||||
)
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
||||
label = "card"
|
||||
)
|
||||
|
||||
val state = remember(steps) {
|
||||
when {
|
||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||
@@ -76,62 +63,69 @@ fun Steps(
|
||||
}
|
||||
}
|
||||
|
||||
val filteredSteps = remember(steps) {
|
||||
val failedCount = steps.count { it.state == State.FAILED }
|
||||
|
||||
steps.filter { step ->
|
||||
// Show hidden steps if it's the only failed step.
|
||||
!step.hide || (step.state == State.FAILED && failedCount == 1)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state == State.RUNNING || state == State.FAILED)
|
||||
onExpand()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.fillMaxWidth()
|
||||
.background(cardColor)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { expanded = !expanded }
|
||||
.background(categoryColor)
|
||||
.clickable(true, onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
|
||||
Text(stringResource(category.displayName))
|
||||
Text(stringResource(category.displayName))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
val stepProgress = remember(stepCount, steps) {
|
||||
stepCount?.let { (current, total) -> "$current/$total" }
|
||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
||||
}
|
||||
Text(
|
||||
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stepProgress,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
||||
}
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
) {
|
||||
steps.forEach { step ->
|
||||
val (progress, progressText) = when (step.progressKey) {
|
||||
null -> null
|
||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${downloaded.megaBytes} MB"
|
||||
}
|
||||
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.name,
|
||||
name = step.title,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
progress = progress,
|
||||
progressText = progressText
|
||||
progressText = progressText,
|
||||
isFirst = index == 0,
|
||||
isLast = index == filteredSteps.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -145,7 +139,9 @@ fun SubStep(
|
||||
state: State,
|
||||
message: String? = null,
|
||||
progress: Float? = null,
|
||||
progressText: String? = null
|
||||
progressText: String? = null,
|
||||
isFirst: Boolean = false,
|
||||
isLast: Boolean = false,
|
||||
) {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
@@ -156,22 +152,22 @@ fun SubStep(
|
||||
clickable { messageExpanded = !messageExpanded }
|
||||
else this
|
||||
}
|
||||
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StepIcon(state, progress, size = 20.dp)
|
||||
}
|
||||
StepIcon(
|
||||
size = 18.dp,
|
||||
state = state,
|
||||
progress = progress,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
@@ -201,7 +197,7 @@ fun SubStep(
|
||||
text = message.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -211,40 +207,44 @@ fun SubStep(
|
||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING ->
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
Crossfade(targetState = state, label = "State CrossFade") { state ->
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = Color(0xFF59B463),
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING -> {
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package app.revanced.manager.ui.model
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
enum class StepCategory(@param:StringRes val displayName: Int) {
|
||||
PREPARING(R.string.patcher_step_group_preparing),
|
||||
PATCHING(R.string.patcher_step_group_patching),
|
||||
SAVING(R.string.patcher_step_group_saving)
|
||||
@@ -15,19 +16,20 @@ enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
enum class ProgressKey {
|
||||
DOWNLOAD
|
||||
}
|
||||
|
||||
interface StepProgressProvider {
|
||||
val downloadProgress: Pair<Long, Long?>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val name: String,
|
||||
val id: StepId,
|
||||
val title: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
||||
val progress: Pair<Long, Long?>? = null,
|
||||
val hide: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
|
||||
fun Step.withState(
|
||||
state: State = this.state,
|
||||
message: String? = this.message,
|
||||
progress: Pair<Long, Long?>? = this.progress
|
||||
) = copy(state = state, message = message, progress = progress)
|
||||
@@ -36,7 +36,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.SecondaryTabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -100,6 +101,7 @@ fun DashboardScreen(
|
||||
false
|
||||
)
|
||||
val androidContext = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val composableScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = DashboardPage.DASHBOARD.ordinal,
|
||||
@@ -234,7 +236,7 @@ fun DashboardScreen(
|
||||
when (pagerState.currentPage) {
|
||||
DashboardPage.DASHBOARD.ordinal -> {
|
||||
if (availablePatches < 1) {
|
||||
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
||||
androidContext.toast(resources.getString(R.string.no_patch_found))
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
DashboardPage.BUNDLES.ordinal
|
||||
@@ -259,7 +261,7 @@ fun DashboardScreen(
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
TabRow(
|
||||
SecondaryTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
@@ -69,6 +70,7 @@ fun PatcherScreen(
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val exportApkLauncher =
|
||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
||||
|
||||
@@ -79,7 +81,7 @@ fun PatcherScreen(
|
||||
|
||||
fun onPageBack() = when {
|
||||
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
|
||||
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress))
|
||||
else -> onLeave()
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ fun PatcherScreen(
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
viewModel.steps.groupBy { it.category }
|
||||
viewModel.steps.groupBy { it.category }.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +215,12 @@ fun PatcherScreen(
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
|
||||
|
||||
val expandCategory: (StepCategory?) -> Unit = { category ->
|
||||
expandedCategory = category
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { viewModel.progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -224,14 +232,17 @@ fun PatcherScreen(
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = steps.toList(),
|
||||
items = steps,
|
||||
key = { it.first }
|
||||
) { (category, steps) ->
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
||||
stepProgressProvider = viewModel
|
||||
isExpanded = expandedCategory == category,
|
||||
onExpand = { expandCategory(category) },
|
||||
onClick = {
|
||||
expandCategory(if (expandedCategory == category) null else category)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -49,10 +49,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -81,9 +83,11 @@ import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.isScrollingUp
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onSave: (PatchSelection?, Options) -> Unit,
|
||||
@@ -231,7 +235,8 @@ fun PatchesSelectorScreen(
|
||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||
|
||||
// Show universal warning if universal patch is selected and the toggle is off
|
||||
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
|
||||
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning =
|
||||
true
|
||||
|
||||
// Toggle the patch otherwise
|
||||
else -> viewModel.togglePatch(uid, patch)
|
||||
@@ -360,6 +365,21 @@ fun PatchesSelectorScreen(
|
||||
) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
|
||||
val isScrollingUp =
|
||||
patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp()
|
||||
val expanded by produceState(true, isScrollingUp) {
|
||||
val state = isScrollingUp ?: return@produceState
|
||||
value = state.value
|
||||
|
||||
// Use snapshotFlow and sample to prevent the value from changing too often.
|
||||
snapshotFlow { state.value }
|
||||
.sample(333L)
|
||||
.collect {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
@@ -375,8 +395,7 @@ fun PatchesSelectorScreen(
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
},
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||
?: true,
|
||||
expanded = expanded,
|
||||
onClick = {
|
||||
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||
}
|
||||
@@ -392,7 +411,7 @@ fun PatchesSelectorScreen(
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
if (bundles.size > 1) {
|
||||
ScrollableTabRow(
|
||||
SecondaryScrollableTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
@@ -106,7 +106,7 @@ fun RequiredOptionsScreen(
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
if (list.isEmpty()) return@Column
|
||||
else if (list.size > 1) ScrollableTabRow(
|
||||
else if (list.size > 1) SecondaryScrollableTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
|
||||
@@ -25,12 +25,14 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -67,6 +69,7 @@ fun SelectedAppInfoScreen(
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
@@ -76,12 +79,12 @@ fun SelectedAppInfoScreen(
|
||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
val selectedPatchCount = remember(patches) {
|
||||
patches.values.sumOf { it.size }
|
||||
val patches by remember {
|
||||
derivedStateOf {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
}
|
||||
val selectedPatchCount = patches.values.sumOf { it.size }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
@@ -117,7 +120,7 @@ fun SelectedAppInfoScreen(
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
context.toast(resources.getString(R.string.no_patches_selected))
|
||||
|
||||
return@patchClick
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import app.revanced.manager.ui.model.navigation.Settings
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
private data class Section(
|
||||
@StringRes val name: Int,
|
||||
@StringRes val description: Int,
|
||||
@param:StringRes val name: Int,
|
||||
@param:StringRes val description: Int,
|
||||
val image: ImageVector,
|
||||
val destination: Settings.Destination,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -67,8 +69,9 @@ fun AboutSettingsScreen(
|
||||
viewModel: AboutViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
// painterResource() is broken on release builds for some reason.
|
||||
val icon = rememberDrawablePainter(drawable = remember {
|
||||
val icon = rememberDrawablePainter(drawable = remember(resources) {
|
||||
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
||||
})
|
||||
|
||||
@@ -76,7 +79,7 @@ fun AboutSettingsScreen(
|
||||
viewModel.socials.partition(ReVancedSocial::preferred)
|
||||
}
|
||||
|
||||
val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
|
||||
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) {
|
||||
preferredSocials.map {
|
||||
Triple(
|
||||
getSocialIcon(it.name),
|
||||
@@ -89,7 +92,7 @@ fun AboutSettingsScreen(
|
||||
viewModel.donate?.let {
|
||||
Triple(
|
||||
Icons.Outlined.FavoriteBorder,
|
||||
context.getString(R.string.donate),
|
||||
resources.getString(R.string.donate),
|
||||
third = {
|
||||
context.openUrl(it)
|
||||
}
|
||||
@@ -98,7 +101,7 @@ fun AboutSettingsScreen(
|
||||
viewModel.contact?.let {
|
||||
Triple(
|
||||
Icons.Outlined.MailOutline,
|
||||
context.getString(R.string.contact),
|
||||
resources.getString(R.string.contact),
|
||||
third = {
|
||||
context.openUrl("mailto:$it")
|
||||
}
|
||||
@@ -131,7 +134,7 @@ fun AboutSettingsScreen(
|
||||
stringResource(R.string.contributors_description),
|
||||
third = nav@{
|
||||
if (!viewModel.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
context.toast(resources.getString(R.string.no_network_toast))
|
||||
return@nav
|
||||
}
|
||||
|
||||
@@ -153,7 +156,7 @@ fun AboutSettingsScreen(
|
||||
LaunchedEffect(developerTaps) {
|
||||
if (developerTaps == 0) return@LaunchedEffect
|
||||
if (showDeveloperSettings) {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled))
|
||||
developerTaps = 0
|
||||
return@LaunchedEffect
|
||||
}
|
||||
@@ -161,7 +164,7 @@ fun AboutSettingsScreen(
|
||||
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
||||
if (remaining > 0) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(
|
||||
resources.getString(
|
||||
R.string.developer_options_taps,
|
||||
remaining
|
||||
),
|
||||
@@ -169,7 +172,7 @@ fun AboutSettingsScreen(
|
||||
)
|
||||
} else if (remaining == 0) {
|
||||
viewModel.showDeveloperSettings.update(true)
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled))
|
||||
}
|
||||
|
||||
// Reset the counter
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -64,9 +65,10 @@ fun AdvancedSettingsScreen(
|
||||
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val memoryLimit = remember {
|
||||
val resources = LocalResources.current
|
||||
val memoryLimit = remember(resources) {
|
||||
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||
context.getString(
|
||||
resources.getString(
|
||||
R.string.device_memory_limit_format,
|
||||
activityManager.memoryClass,
|
||||
activityManager.largeMemoryClass
|
||||
@@ -183,7 +185,7 @@ fun AdvancedSettingsScreen(
|
||||
ClipData.newPlainText("Device Information", deviceContent)
|
||||
)
|
||||
|
||||
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
||||
context.toast(resources.getString(R.string.toast_copied_to_clipboard))
|
||||
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
),
|
||||
headlineContent = stringResource(R.string.about_device),
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -10,10 +12,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -28,6 +33,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -36,10 +42,10 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||
@@ -52,6 +58,7 @@ fun DownloadsSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: DownloadsViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
@@ -75,7 +82,7 @@ fun DownloadsSettingsScreen(
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (viewModel.appSelection.isNotEmpty()) {
|
||||
IconButton(onClick = { showDeleteConfirmationDialog = true }) {
|
||||
IconButton(onClick = { viewModel.deleteApps() }) {
|
||||
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
@@ -121,6 +128,11 @@ fun DownloadsSettingsScreen(
|
||||
.digest(androidSignature.toByteArray())
|
||||
hash.toHexString(format = HexFormat.UpperCase)
|
||||
}
|
||||
val appName = remember {
|
||||
packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
||||
?.toString()
|
||||
?: packageName
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||
@@ -130,6 +142,8 @@ fun DownloadsSettingsScreen(
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.revokePluginTrust(packageName)
|
||||
@@ -147,10 +161,10 @@ fun DownloadsSettingsScreen(
|
||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||
title = R.string.downloader_plugin_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
R.string.downloader_plugin_trust_dialog_body
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.trustPlugin(packageName)
|
||||
@@ -226,6 +240,8 @@ fun DownloadsSettingsScreen(
|
||||
private fun TrustDialog(
|
||||
@StringRes title: Int,
|
||||
body: String,
|
||||
pluginName: String,
|
||||
signature: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
@@ -238,10 +254,39 @@ private fun TrustDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(title)) },
|
||||
text = { Text(body) }
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(body)
|
||||
Card {
|
||||
Column(
|
||||
Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_plugin,
|
||||
pluginName
|
||||
),
|
||||
)
|
||||
OutlinedCard(
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_signature,
|
||||
signature.chunked(2).joinToString(" ")
|
||||
), modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
@@ -18,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -47,6 +52,7 @@ fun GeneralSettingsScreen(
|
||||
val prefs = viewModel.prefs
|
||||
val coroutineScope = viewModel.viewModelScope
|
||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showThemePicker) {
|
||||
ThemePicker(
|
||||
@@ -54,6 +60,17 @@ fun GeneralSettingsScreen(
|
||||
onConfirm = { viewModel.setTheme(it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (showLanguagePicker) {
|
||||
LanguagePicker(
|
||||
supportedLocales = viewModel.getSupportedLocales(),
|
||||
currentLocale = viewModel.getCurrentLocale(),
|
||||
onDismiss = { showLanguagePicker = false },
|
||||
onConfirm = { viewModel.setLocale(it) },
|
||||
getDisplayName = { viewModel.getLocaleDisplayName(it) }
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
@@ -73,6 +90,24 @@ fun GeneralSettingsScreen(
|
||||
) {
|
||||
GroupHeader(stringResource(R.string.appearance))
|
||||
|
||||
val currentLocale = viewModel.getCurrentLocale()
|
||||
val currentLanguageDisplay = remember(currentLocale) {
|
||||
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
|
||||
}
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showLanguagePicker = true },
|
||||
headlineContent = stringResource(R.string.language),
|
||||
supportingContent = stringResource(R.string.language_description),
|
||||
trailingContent = {
|
||||
FilledTonalButton(onClick = { showLanguagePicker = true }) {
|
||||
Text(
|
||||
currentLanguageDisplay
|
||||
?: stringResource(R.string.language_system_default)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val theme by prefs.theme.getAsState()
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
@@ -96,6 +131,22 @@ fun GeneralSettingsScreen(
|
||||
description = R.string.dynamic_color_description
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(theme != Theme.LIGHT) {
|
||||
BooleanItem(
|
||||
preference = prefs.pureBlackTheme,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.pure_black_theme,
|
||||
description = R.string.pure_black_theme_description
|
||||
)
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.networking))
|
||||
BooleanItem(
|
||||
preference = prefs.allowMeteredNetworks,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.allow_metered_networks,
|
||||
description = R.string.allow_metered_networks_description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,4 +190,64 @@ private fun ThemePicker(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LanguagePicker(
|
||||
supportedLocales: List<Locale>,
|
||||
currentLocale: Locale?,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Locale?) -> Unit,
|
||||
getDisplayName: (Locale) -> String
|
||||
) {
|
||||
var selectedLocale by remember { mutableStateOf(currentLocale) }
|
||||
val systemDefaultString = stringResource(R.string.language_system_default)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.language)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedLocale = null },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HapticRadioButton(
|
||||
selected = selectedLocale == null,
|
||||
onClick = { selectedLocale = null }
|
||||
)
|
||||
Text(systemDefaultString)
|
||||
}
|
||||
|
||||
supportedLocales.forEach { locale ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedLocale = locale },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HapticRadioButton(
|
||||
selected = selectedLocale == locale,
|
||||
onClick = { selectedLocale = locale }
|
||||
)
|
||||
Text(getDisplayName(locale))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm(selectedLocale)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -64,6 +65,7 @@ fun ImportExportSettingsScreen(
|
||||
vm: ImportExportViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
||||
|
||||
@@ -108,7 +110,7 @@ fun ImportExportSettingsScreen(
|
||||
vm.viewModelScope.launch {
|
||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||
val result = vm.tryKeystoreImport(alias, pass)
|
||||
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||
if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +168,7 @@ fun ImportExportSettingsScreen(
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!vm.canExport()) {
|
||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
||||
context.toast(resources.getString(R.string.export_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
|
||||
@@ -7,15 +7,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.Scrollbar
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||
import com.mikepenz.aboutlibraries.ui.compose.libraryColors
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -33,16 +36,23 @@ fun LicensesSettingsScreen(
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||
val chipColors = LibraryDefaults.chipColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
|
||||
LibrariesContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
libraries = libraries,
|
||||
lazyListState = lazyListState,
|
||||
colors = LibraryDefaults.libraryColors(
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
||||
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
libraryBackgroundColor = MaterialTheme.colorScheme.background,
|
||||
libraryContentColor = MaterialTheme.colorScheme.onBackground,
|
||||
versionChipColors = chipColors,
|
||||
licenseChipColors = chipColors,
|
||||
fundingChipColors = chipColors,
|
||||
)
|
||||
)
|
||||
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@@ -33,6 +34,7 @@ fun UpdatesSettingsScreen(
|
||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
@@ -57,7 +59,7 @@ fun UpdatesSettingsScreen(
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
context.toast(resources.getString(R.string.no_network_toast))
|
||||
return@launch
|
||||
}
|
||||
if (vm.checkForUpdates()) onUpdateClick()
|
||||
@@ -70,7 +72,7 @@ fun UpdatesSettingsScreen(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
context.toast(resources.getString(R.string.no_network_toast))
|
||||
return@clickable
|
||||
}
|
||||
onChangelogClick()
|
||||
|
||||
@@ -80,6 +80,7 @@ private val LightColorScheme = lightColorScheme(
|
||||
fun ReVancedManagerTheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
pureBlackTheme: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
@@ -93,6 +94,10 @@ fun ReVancedManagerTheme(
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}.let {
|
||||
if (darkTheme && pureBlackTheme)
|
||||
it.copy(background = Color.Black, surface = Color.Black)
|
||||
else it
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -54,6 +54,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
||||
patchBundleRepository.update(
|
||||
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||
showToast = true,
|
||||
force = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +66,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
||||
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||
if (src !is RemotePatchBundle) return@launch
|
||||
|
||||
patchBundleRepository.update(src, showToast = true)
|
||||
patchBundleRepository.update(src, showToast = true, force = true)
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.SupportedLocales
|
||||
import app.revanced.manager.util.resetListItemColorsCached
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class GeneralSettingsViewModel(
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||
prefs.theme.update(theme)
|
||||
resetListItemColorsCached()
|
||||
}
|
||||
|
||||
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
|
||||
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
|
||||
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
|
||||
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
|
||||
}
|
||||
@@ -36,8 +36,8 @@ import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
sealed class ResetDialogState(
|
||||
@StringRes val titleResId: Int,
|
||||
@StringRes val descriptionResId: Int,
|
||||
@param:StringRes val titleResId: Int,
|
||||
@param:StringRes val descriptionResId: Int,
|
||||
val onConfirm: () -> Unit,
|
||||
val dialogOptionName: String? = null
|
||||
) {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -19,7 +13,6 @@ 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.repository.InstalledAppRepository
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
class InstalledAppInfoViewModel(
|
||||
packageName: String
|
||||
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
fun uninstall() {
|
||||
val app = installedApp ?: return
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
||||
|
||||
InstallType.MOUNT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(app.currentPackageName)
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val extraStatus =
|
||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||
val extraStatusMessage =
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
installedApp?.let {
|
||||
installedAppRepository.delete(it)
|
||||
}
|
||||
onBackClick()
|
||||
viewModelScope.launch {
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
val msg = result.failure.message.orEmpty()
|
||||
context.toast(
|
||||
this@InstalledAppInfoViewModel.context.getString(
|
||||
R.string.uninstall_app_fail,
|
||||
msg
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
it,
|
||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
||||
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||
}
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
@@ -16,7 +14,6 @@ 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.ContextCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
@@ -32,32 +29,35 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
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.StepProgressProvider
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.withState
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.asCode
|
||||
import app.revanced.manager.util.saveableVar
|
||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,6 +66,15 @@ import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.installer.getSession
|
||||
import ru.solrudev.ackpine.session.ProgressSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
@@ -73,7 +82,7 @@ import java.time.Duration
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class PatcherViewModel(
|
||||
private val input: Patcher.ViewModelParams
|
||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
||||
) : ViewModel(), KoinComponent, InstallerModel {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val pm: PM by inject()
|
||||
@@ -81,6 +90,7 @@ class PatcherViewModel(
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
private val selectedApp = input.selectedApp
|
||||
@@ -95,7 +105,6 @@ class PatcherViewModel(
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
private set
|
||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||
key = "packageInstallerStatus",
|
||||
stateSaver = autoSaver()
|
||||
@@ -104,7 +113,7 @@ class PatcherViewModel(
|
||||
}
|
||||
private set
|
||||
|
||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||
@@ -123,6 +132,18 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This coroutine scope is used to await installations.
|
||||
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
|
||||
*/
|
||||
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* Holds the package name of the Apk we are trying to install.
|
||||
*/
|
||||
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
|
||||
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
|
||||
|
||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
|
||||
@@ -138,35 +159,15 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
||||
private var completedPatchCount by savedStateHandle.saveable {
|
||||
// SavedStateHandle.saveable only supports the boxed version.
|
||||
@Suppress("AutoboxingStateCreation") mutableStateOf(
|
||||
0
|
||||
)
|
||||
}
|
||||
val patchesProgress get() = completedPatchCount to patchCount
|
||||
override var downloadProgress by savedStateHandle.saveable(
|
||||
key = "downloadProgress",
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<Pair<Long, Long?>?>(null)
|
||||
}
|
||||
private set
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(
|
||||
app,
|
||||
input.selectedApp
|
||||
).toMutableStateList()
|
||||
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
||||
}
|
||||
private var currentStepIndex = 0
|
||||
|
||||
val progress by derivedStateOf {
|
||||
val current = steps.count {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
} + completedPatchCount
|
||||
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||
|
||||
val total = steps.size - 1 + patchCount
|
||||
val current = steps.count { it.state == State.COMPLETED }
|
||||
val total = steps.size
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
@@ -174,67 +175,46 @@ class PatcherViewModel(
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
onDownloadProgress = {
|
||||
withContext(Dispatchers.Main) {
|
||||
downloadProgress = it
|
||||
}
|
||||
},
|
||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
try {
|
||||
// Wait for the dialog interaction.
|
||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||
currentActivityRequest = this to plugin.name
|
||||
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
ParcelUuid(
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
// Wait for the dialog interaction.
|
||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||
currentActivityRequest = this to plugin.name
|
||||
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
} finally {
|
||||
launchedActivity = null
|
||||
}
|
||||
} finally {
|
||||
launchedActivity = null
|
||||
currentActivityRequest = null
|
||||
}
|
||||
} finally {
|
||||
currentActivityRequest = null
|
||||
}
|
||||
}
|
||||
},
|
||||
onProgress = { name, state, message ->
|
||||
viewModelScope.launch {
|
||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||
copy(
|
||||
name = name ?: this.name,
|
||||
state = state ?: this.state,
|
||||
message = message ?: this.message
|
||||
)
|
||||
}
|
||||
|
||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||
currentStepIndex++
|
||||
|
||||
steps[currentStepIndex] =
|
||||
steps[currentStepIndex].copy(state = State.RUNNING)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onEvent = ::handleProgressEvent,
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
val patcherSucceeded =
|
||||
@@ -246,64 +226,26 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
InstallService.APP_INSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
InstallService.EXTRA_INSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
installedAppRepository.addOrUpdate(
|
||||
installedPackageName!!,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
installerSessionId?.uuid?.let { id ->
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
// The process was killed during installation. Await the session again.
|
||||
withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.getSession(id)
|
||||
}?.let {
|
||||
awaitInstallation(it)
|
||||
}
|
||||
} else packageInstallerStatus = pmStatus
|
||||
|
||||
}
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
||||
packageInstallerStatus = pmStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
installerBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
},
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
@@ -313,7 +255,6 @@ class PatcherViewModel(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installerBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
@@ -327,7 +268,37 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
|
||||
val stepIndex = steps.indexOfFirst {
|
||||
event.stepId?.let { id -> id == it.id }
|
||||
?: (it.state == State.RUNNING || it.state == State.WAITING)
|
||||
}
|
||||
|
||||
if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
|
||||
when (event) {
|
||||
is ProgressEvent.Started -> withState(State.RUNNING)
|
||||
|
||||
is ProgressEvent.Progress -> withState(
|
||||
message = event.message ?: message,
|
||||
progress = event.current?.let { event.current to event.total } ?: progress
|
||||
)
|
||||
|
||||
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
|
||||
|
||||
is ProgressEvent.Failed -> {
|
||||
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
|
||||
withState(
|
||||
State.FAILED,
|
||||
message = event.error.stackTrace,
|
||||
progress = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
installerCoroutineScope.cancel()
|
||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
@@ -372,44 +343,93 @@ class PatcherViewModel(
|
||||
|
||||
fun open() = installedPackageName?.let(pm::launch)
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
var pmInstallStarted = false
|
||||
try {
|
||||
isInstalling = true
|
||||
private suspend fun startInstallation(file: File, packageName: String) {
|
||||
val session = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
installerPkgName = packageName
|
||||
}
|
||||
awaitInstallation(session)
|
||||
}
|
||||
|
||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||
Dispatchers.Main
|
||||
) {
|
||||
val result = installerCoroutineScope.async {
|
||||
try {
|
||||
installerSessionId = ParcelUuid(session.id)
|
||||
withContext(Dispatchers.IO) {
|
||||
session.await()
|
||||
}
|
||||
} finally {
|
||||
installerSessionId = null
|
||||
}
|
||||
}.await()
|
||||
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
}
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName = installerPkgName
|
||||
installedAppRepository.addOrUpdate(
|
||||
installerPkgName,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
pm.installApp(listOf(outputFile))
|
||||
pmInstallStarted = true
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
var needsRootUninstall = false
|
||||
try {
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
val currentPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (
|
||||
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
|
||||
existingPackageInfo
|
||||
)
|
||||
) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
InstallType.MOUNT -> {
|
||||
try {
|
||||
val packageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||
}
|
||||
|
||||
InstallType.MOUNT -> {
|
||||
val label = with(pm) {
|
||||
packageInfo.label()
|
||||
currentPackageInfo.label()
|
||||
}
|
||||
|
||||
// Check for base APK, first check if the app is already installed
|
||||
@@ -417,15 +437,17 @@ class PatcherViewModel(
|
||||
// If the app is not installed, check if the output file is a base apk
|
||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||
// Exit if there is no base APK package
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
||||
packageInstallerStatus =
|
||||
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val inputVersion = input.selectedApp.version
|
||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
|
||||
needsRootUninstall = true
|
||||
// Install as root
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
@@ -436,7 +458,7 @@ class PatcherViewModel(
|
||||
)
|
||||
|
||||
installedAppRepository.addOrUpdate(
|
||||
packageInfo.packageName,
|
||||
currentPackageInfo.packageName,
|
||||
packageName,
|
||||
inputVersion,
|
||||
InstallType.MOUNT,
|
||||
@@ -448,21 +470,20 @@ class PatcherViewModel(
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
needsRootUninstall = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
} finally {
|
||||
if (!pmInstallStarted) isInstalling = false
|
||||
isInstalling = false
|
||||
if (needsRootUninstall) {
|
||||
try {
|
||||
withContext(NonCancellable) {
|
||||
rootInstaller.uninstall(packageName)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,12 +494,27 @@ class PatcherViewModel(
|
||||
|
||||
override fun reinstall() {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
try {
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
val pkgName = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(outputFile)?.packageName
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
when (val result = pm.uninstallPackage(pkgName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
return@launch
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
startInstallation(outputFile, pkgName)
|
||||
}
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,34 +533,66 @@ class PatcherViewModel(
|
||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||
}
|
||||
|
||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||
val needsDownload =
|
||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedApp: SelectedApp,
|
||||
selectedPatches: PatchSelection
|
||||
): List<Step> = buildList {
|
||||
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
||||
add(
|
||||
Step(
|
||||
StepId.DownloadAPK,
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
|
||||
return listOfNotNull(
|
||||
Step(
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING,
|
||||
state = State.RUNNING,
|
||||
progressKey = ProgressKey.DOWNLOAD,
|
||||
).takeIf { needsDownload },
|
||||
add(
|
||||
Step(
|
||||
StepId.LoadPatches,
|
||||
context.getString(R.string.patcher_step_load_patches),
|
||||
StepCategory.PREPARING,
|
||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
||||
),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
add(
|
||||
Step(
|
||||
StepId.ReadAPK,
|
||||
context.getString(R.string.patcher_step_unpack),
|
||||
StepCategory.PREPARING
|
||||
),
|
||||
|
||||
)
|
||||
)
|
||||
add(
|
||||
Step(
|
||||
StepId.ExecutePatches,
|
||||
context.getString(R.string.execute_patches),
|
||||
StepCategory.PATCHING
|
||||
),
|
||||
StepCategory.PATCHING,
|
||||
hide = true
|
||||
)
|
||||
)
|
||||
|
||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||
add(
|
||||
Step(
|
||||
StepId.ExecutePatch(index),
|
||||
name,
|
||||
StepCategory.PATCHING
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
Step(
|
||||
StepId.WriteAPK,
|
||||
context.getString(R.string.patcher_step_write_patched),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
)
|
||||
add(
|
||||
Step(
|
||||
StepId.SignAPK,
|
||||
context.getString(R.string.patcher_step_sign_apk),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -129,8 +128,6 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
val state = mutableStateOf<Options>(emptyMap())
|
||||
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
@@ -141,7 +138,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
mutableStateOf(emptyMap())
|
||||
}
|
||||
private set
|
||||
|
||||
@@ -149,8 +146,6 @@ class SelectedAppInfoViewModel(
|
||||
if (input.patches != null)
|
||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||
|
||||
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
||||
|
||||
// Try to get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||
@@ -160,7 +155,7 @@ class SelectedAppInfoViewModel(
|
||||
selectionState = SelectionState.Customized(previous)
|
||||
}
|
||||
|
||||
selection
|
||||
mutableStateOf(SelectionState.Default)
|
||||
}
|
||||
|
||||
var showSourceSelector by mutableStateOf(false)
|
||||
@@ -311,7 +306,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
enum class Error(@StringRes val resourceId: Int) {
|
||||
enum class Error(@param:StringRes val resourceId: Int) {
|
||||
NoPlugins(R.string.downloader_no_plugins_available)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import io.ktor.client.plugins.onDownload
|
||||
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
|
||||
class UpdateViewModel(
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
@@ -39,10 +39,11 @@ class UpdateViewModel(
|
||||
private val app: Application by inject()
|
||||
private val reVancedAPI: ReVancedAPI by inject()
|
||||
private val http: HttpService by inject()
|
||||
private val pm: PM by inject()
|
||||
private val networkInfo: NetworkInfo by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
// TODO: save state to handle process death.
|
||||
var downloadedSize by mutableLongStateOf(0L)
|
||||
private set
|
||||
var totalSize by mutableLongStateOf(0L)
|
||||
@@ -62,14 +63,17 @@ class UpdateViewModel(
|
||||
private set
|
||||
|
||||
private val location = fs.tempDir.resolve("updater.apk")
|
||||
private val job = viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +82,7 @@ class UpdateViewModel(
|
||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||
val release = releaseInfo!!
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
||||
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
|
||||
showInternetCheckDialog = true
|
||||
} else {
|
||||
state = State.DOWNLOADING
|
||||
@@ -86,8 +90,10 @@ class UpdateViewModel(
|
||||
http.download(location) {
|
||||
url(release.downloadUrl)
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
downloadedSize = bytesSentTotal
|
||||
totalSize = contentLength
|
||||
withContext(Dispatchers.Main) {
|
||||
downloadedSize = bytesSentTotal
|
||||
contentLength?.let { totalSize = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
installUpdate()
|
||||
@@ -98,50 +104,36 @@ class UpdateViewModel(
|
||||
|
||||
fun installUpdate() = viewModelScope.launch {
|
||||
state = State.INSTALLING
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}.await()
|
||||
}
|
||||
|
||||
pm.installApp(listOf(location))
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let {
|
||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||
val extra =
|
||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
|
||||
when(pmStatus) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
else -> {
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
installError = extra
|
||||
state = State.FAILED
|
||||
}
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||
else -> {
|
||||
val msg = failure.message.orEmpty()
|
||||
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||
installError = msg
|
||||
state = State.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installBroadcastReceiver)
|
||||
|
||||
job.cancel()
|
||||
location.delete()
|
||||
}
|
||||
|
||||
enum class State(@StringRes val title: Int) {
|
||||
enum class State(@param:StringRes val title: Int) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update),
|
||||
CAN_INSTALL(R.string.ready_to_install_update),
|
||||
|
||||
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
/**
|
||||
* Converts an Ackpine installation failure into a PM status code
|
||||
*/
|
||||
fun InstallFailure.asCode() = when (this) {
|
||||
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
|
||||
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
|
||||
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
|
||||
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an Ackpine uninstallation failure into a PM status code
|
||||
*/
|
||||
fun UninstallFailure.asCode() = when (this) {
|
||||
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
@@ -2,11 +2,8 @@ package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.PackageInfoFlags
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
@@ -16,8 +13,6 @@ import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
import ru.solrudev.ackpine.uninstaller.createSession
|
||||
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
@@ -40,7 +38,8 @@ data class AppInfo(
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
class PM(
|
||||
private val app: Application,
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
patchBundleRepository: PatchBundleRepository,
|
||||
private val uninstaller: PackageUninstaller
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -145,17 +144,11 @@ class PM(
|
||||
false
|
||||
)
|
||||
|
||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
apks.forEach { apk -> session.writeApk(apk) }
|
||||
session.commit(app.installIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
||||
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 {
|
||||
@@ -164,44 +157,4 @@ class PM(
|
||||
}
|
||||
|
||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
setRequestUpdateOwnership(true)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.app.LocaleConfig
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import app.revanced.manager.BuildConfig
|
||||
import java.util.Locale
|
||||
|
||||
object SupportedLocales {
|
||||
fun getSupportedLocales(context: Context): List<Locale> {
|
||||
var result: List<Locale>? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) result = runCatching {
|
||||
LocaleConfig(context).supportedLocales?.toList()
|
||||
}.getOrNull()
|
||||
|
||||
return result ?: generated
|
||||
}
|
||||
|
||||
fun getCurrentLocale(): Locale? =
|
||||
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
|
||||
|
||||
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
|
||||
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
|
||||
)
|
||||
|
||||
fun getDisplayName(locale: Locale) =
|
||||
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
|
||||
|
||||
private fun LocaleList.toList() = (0 until size()).map { get(it) }
|
||||
|
||||
private val generated by lazy {
|
||||
listOf(
|
||||
Locale.ENGLISH,
|
||||
*BuildConfig.SUPPORTED_LOCALES.map(Locale::forLanguageTag).toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -33,6 +31,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -42,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format.MonthNames
|
||||
@@ -50,9 +48,11 @@ import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Clock
|
||||
|
||||
typealias PatchSelection = Map<Int, Set<String>>
|
||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||
@@ -82,6 +82,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (error: Exception) {
|
||||
// You can only toast on the main thread.
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
@@ -166,7 +168,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
||||
else -> LocalDateTime.Format {
|
||||
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
||||
char(' ')
|
||||
dayOfMonth()
|
||||
day()
|
||||
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
||||
chars(", ")
|
||||
year()
|
||||
@@ -193,7 +195,12 @@ val transparentListItemColors
|
||||
.also { transparentListItemColorsCached = it }
|
||||
|
||||
@Composable
|
||||
fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
|
||||
fun <T> EventEffect(
|
||||
flow: Flow<T>,
|
||||
vararg keys: Any?,
|
||||
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||
block: suspend (T) -> Unit
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val currentBlock by rememberUpdatedState(block)
|
||||
|
||||
@@ -209,40 +216,36 @@ fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = L
|
||||
const val isScrollingUpSensitivity = 10
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrollingUp(): State<Boolean> {
|
||||
return remember(this) {
|
||||
var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
|
||||
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
|
||||
fun LazyListState.isScrollingUp() = produceState(true, this) {
|
||||
var previousIndex = firstVisibleItemIndex
|
||||
var previousScrollOffset = firstVisibleItemScrollOffset
|
||||
|
||||
derivedStateOf {
|
||||
val indexChanged = previousIndex != firstVisibleItemIndex
|
||||
val offsetChanged =
|
||||
kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
|
||||
snapshotFlow {
|
||||
firstVisibleItemIndex to firstVisibleItemScrollOffset
|
||||
}.collect { (index, scrollOffset) ->
|
||||
val indexChanged = previousIndex != index
|
||||
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
|
||||
|
||||
if (indexChanged) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else if (offsetChanged) {
|
||||
previousScrollOffset > firstVisibleItemScrollOffset
|
||||
} else {
|
||||
true
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
}
|
||||
value = when {
|
||||
indexChanged -> previousIndex > index
|
||||
offsetChanged -> previousScrollOffset > scrollOffset
|
||||
else -> value
|
||||
}
|
||||
previousIndex = index
|
||||
previousScrollOffset = scrollOffset
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support sensitivity
|
||||
@Composable
|
||||
fun ScrollState.isScrollingUp(): State<Boolean> {
|
||||
return remember(this) {
|
||||
var previousScrollOffset by mutableIntStateOf(value)
|
||||
derivedStateOf {
|
||||
(previousScrollOffset >= value).also {
|
||||
previousScrollOffset = value
|
||||
}
|
||||
fun ScrollState.isScrollingUp() = produceState(true, this) {
|
||||
var previousScrollOffset = this@isScrollingUp.value
|
||||
|
||||
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset ->
|
||||
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) {
|
||||
value = previousScrollOffset >= scrollOffset
|
||||
}
|
||||
|
||||
previousScrollOffset = scrollOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<item quantity="other">%d patches</item>
|
||||
</plurals>
|
||||
<plurals name="patches_executed">
|
||||
<item quantity="one">Executed %d patch</item>
|
||||
<item quantity="other">Executed %d patches</item>
|
||||
<item quantity="one">Execute %d patch</item>
|
||||
<item quantity="other">Execute %d patches</item>
|
||||
</plurals>
|
||||
<plurals name="selected_count">
|
||||
<item quantity="other">%d selected</item>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<!--
|
||||
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||
|
||||
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||
replace \n with an encoded new line character.
|
||||
|
||||
Bad:
|
||||
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||
Good:
|
||||
<string name="summary_key">"First 'item' text
|
||||
Second \"item\" text"</string>
|
||||
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">ReVanced Manager</string>
|
||||
<string name="patcher">Patcher</string>
|
||||
<string name="patcher">Patcher test</string>
|
||||
<string name="patches">Patches</string>
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</string>
|
||||
@@ -55,7 +69,7 @@
|
||||
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||
|
||||
<string name="apk_source_selector_item">Select APK source</string>
|
||||
<string name="apk_source_auto">Using all APK downloader</string>
|
||||
<string name="apk_source_auto">Using all APK downloaders</string>
|
||||
<string name="apk_source_downloader">Using %s</string>
|
||||
<string name="apk_source_installed">Using installed APK</string>
|
||||
<string name="apk_source_local">Using a local APK file</string>
|
||||
@@ -69,7 +83,7 @@
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
|
||||
<string name="general">General</string>
|
||||
<string name="general_description">Theme, dynamic color</string>
|
||||
<string name="general_description">Language, theme, dynamic color</string>
|
||||
<string name="updates">Updates</string>
|
||||
<string name="updates_description">Check for updates and view changelogs</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
@@ -86,21 +100,34 @@
|
||||
<string name="contributors_description">View the contributors of ReVanced</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="dynamic_color_description">Adapt colors to the wallpaper</string>
|
||||
<string name="pure_black_theme">Pure black theme</string>
|
||||
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="language_description">Choose the app display language</string>
|
||||
<string name="language_system_default">System default</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">"Selecting incompatible patches can result in a broken app.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="suggested_version_safeguard_confirmation">"Selecting an app that is not the suggested version may cause unexpected issues.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
<string name="patch_selection_safeguard">Allow changing patch selection and options</string>
|
||||
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches and customization of options</string>
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string>
|
||||
<string name="patch_selection_safeguard_confirmation">"Changing the selection of patches may cause unexpected issues.
|
||||
|
||||
Enable anyways?"</string>
|
||||
<string name="universal_patches_safeguard">Allow using universal patches</string>
|
||||
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</string>
|
||||
<string name="universal_patches_safeguard_confirmation">"Universal patches are not as well tested as those that target specific apps.
|
||||
|
||||
Enable anyways?"</string>
|
||||
<string name="import_keystore">Import keystore</string>
|
||||
<string name="import_keystore_description">Import a custom keystore</string>
|
||||
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||
@@ -116,7 +143,9 @@
|
||||
<string name="export_keystore_success">Exported keystore</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
|
||||
<string name="regenerate_keystore_dialog_description">"You are about to regenerate your keystore the manager will use during the patching process.
|
||||
|
||||
You will not be able to update the previously installed apps from this source."</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
<string name="import_patch_selection">Import patch selection</string>
|
||||
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
|
||||
@@ -132,8 +161,8 @@
|
||||
<string name="reset_patch_options_description">Reset the stored patch options</string>
|
||||
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
||||
<string name="patch_selection_reset_all">Reset patch selection globally</string>
|
||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_all_description">Resets all the patch selections</string>
|
||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all patch selections. You will need to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_all_description">Resets all patch selections</string>
|
||||
<string name="patch_selection_reset_package">Reset patch selection for app</string>
|
||||
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
|
||||
@@ -147,7 +176,7 @@
|
||||
<string name="patch_options_reset_patches_dialog_description">You are about to reset the patch options for \"%s\". You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_patches_description">Resets the patch options for a specific collection of patches</string>
|
||||
<string name="patch_options_reset_all">Reset patch options globally</string>
|
||||
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||
<string name="downloader_plugins">Plugins</string>
|
||||
<string name="downloader_plugin_state_trusted">Trusted</string>
|
||||
@@ -155,10 +184,12 @@
|
||||
<string name="downloader_plugin_state_untrusted">Untrusted</string>
|
||||
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
|
||||
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Continuing will allow this plugin to run on your system.\n\nOnly enable this plugin if you trust it. Plugins can execute arbitrary code and may compromise your device.</string>
|
||||
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
|
||||
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
|
||||
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
|
||||
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found.</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="loading_body">Loading…</string>
|
||||
@@ -189,9 +220,13 @@
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="networking">Networking</string>
|
||||
<string name="allow_metered_networks">Allow metered networks</string>
|
||||
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
|
||||
The application might still warn about metered networks for manual operations.</string>
|
||||
<string name="downloaded_apps">Downloaded apps</string>
|
||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
|
||||
<string name="process_runtime_memory_limit">Patcher process memory limit</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string>
|
||||
<string name="debug_logs_export">Export debug logs</string>
|
||||
@@ -199,7 +234,7 @@
|
||||
<string name="debug_logs_export_failed">Failed to export logs</string>
|
||||
<string name="debug_logs_export_success">Exported logs</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="api_url_description">The API used to download necessary files.</string>
|
||||
<string name="api_url_description">The API used to download necessary files</string>
|
||||
<string name="api_url_dialog_title">Change API URL</string>
|
||||
<string name="api_url_dialog_description">Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
||||
@@ -235,14 +270,23 @@
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
<string name="non_suggested_version_warning_title">Non suggested version</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="non_suggested_version_warning_description">"The version of the app you have selected does not match the suggested version.
|
||||
Please use the suggested version: %s
|
||||
|
||||
To continue anyway, disable \"Require suggested app version\" in the advanced settings."</string>
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nYou need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches.</string>
|
||||
<string name="selection_warning_description">"It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
|
||||
|
||||
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches."</string>
|
||||
<string name="universal_patch_warning_description">"Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
|
||||
|
||||
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches."</string>
|
||||
<string name="this_version">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string>
|
||||
<string name="app_version_not_compatible">"This patch is not compatible with the selected app version (%1$s)
|
||||
|
||||
It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
@@ -276,7 +320,7 @@
|
||||
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
||||
<string name="downloader_error">Downloader error: %s</string>
|
||||
<string name="downloader_no_plugins_installed">No downloader installed.</string>
|
||||
<string name="downloader_no_plugins_available">There are downloader installed but none is trusted. Check your settings.</string>
|
||||
<string name="downloader_no_plugins_available">There are downloaders installed but none are trusted. Check your settings.</string>
|
||||
<string name="already_patched">Already patched</string>
|
||||
|
||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
@@ -384,7 +428,8 @@
|
||||
<string name="save_with_count">Save (%1$s)</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="empty">Empty</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted.\nReVanced Manager will close when updating.</string>
|
||||
<string name="installing_message">"Tap on <b>Update</b> when prompted.
|
||||
ReVanced Manager will close when updating."</string>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
@@ -403,7 +448,9 @@
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
|
||||
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_confirmation_metered">"You are currently on a metered connection, and data charges from your service provider may apply.
|
||||
|
||||
Do you still want to continue?"</string>
|
||||
<string name="download_update_confirmation">Download update?</string>
|
||||
<string name="no_contributors_found">No contributors found</string>
|
||||
<string name="select">Select</string>
|
||||
@@ -441,12 +488,14 @@
|
||||
<string name="patches_prereleases">Use pre-releases</string>
|
||||
<string name="patches_prereleases_description">Use pre-release versions of %s</string>
|
||||
<string name="patches_url">Patches URL</string>
|
||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="incompatible_patches_dialog">"These patches are not compatible with the selected app version (%1$s).
|
||||
|
||||
Click on the patches to see more details."</string>
|
||||
<string name="incompatible_patch">Incompatible patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Show a popup notification whenever a new update is available on launch</string>
|
||||
<string name="failed_to_import_keystore">Failed to import keystore</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
|
||||
@@ -6,5 +6,6 @@ plugins {
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
alias(libs.plugins.about.libraries) apply false
|
||||
alias(libs.plugins.about.libraries.android) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
}
|
||||
|
||||
9
crowdin.yml
Normal file
9
crowdin.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
project_id_env: "CROWDIN_PROJECT_ID"
|
||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: app/src/main/res/values/strings.xml
|
||||
dest: manager.xml
|
||||
translation: app/src/main/res/values-%android_code%/strings.xml
|
||||
skip_untranslated_strings: true
|
||||
@@ -13,9 +13,9 @@ Learn how to use ReVanced Manager to patch apps.
|
||||
7. Tap on the `Install` button to install the patched app[^4]
|
||||
|
||||
[^1]: Here you can see all the apps that are supported by ReVanced.
|
||||
You can also add custom apps by tapping on the `+` button in the top right corner.
|
||||
You can also add custom apps by tapping on the `Select from storage` button at the top.
|
||||
[^2]: It is recommended to use the default set of patches by tapping on the `Reset` button in the bottom right corner.
|
||||
[^3]: By default, all available downloader will be used to download the app.
|
||||
[^3]: By default, all available downloaders will be used to download the app.
|
||||
If you want to use a specific downloader, you can change it here.
|
||||
[^4]: You can export the patched app or the patch logs in the bottom left corner.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Learn how to manage downloaders.
|
||||
Refer to the [template](https://github.com/ReVanced/revanced-manager-downloader-template) if you are developer who wants to create a plugin.
|
||||
|
||||
Downloaders are Apk files and are installed, updated and uninstalled just like regular Android apps.
|
||||
Downloaders are APK files and are installed, updated and uninstalled just like regular Android apps.
|
||||
Downloaders can execute arbitrary code inside ReVanced Manager and must be marked as trusted before use. Manager will show a notification in the dashboard when a new downloader is discovered.
|
||||
Trust can also be granted and revoked under `Settings` > `Downloads`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Learn how to update ReVanced Manager.
|
||||
3. Configure the update settings accordingly[^1]
|
||||
|
||||
[^1]: By default, ReVanced Manager will check for updates automatically on launch
|
||||
and let you when an update is available.
|
||||
and let you know when an update is available.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
|
||||
@@ -4,29 +4,29 @@ Learn how to configure ReVanced Manager.
|
||||
|
||||
## 🔧 Settings
|
||||
|
||||
- **Import & export**: Import or export patch selections, patch options and the signing keystore
|
||||
- **Downloads**: Enable or disable ReVanced Manager downloader and manage past downloaded apps here
|
||||
- **Import & export**: Import or export patch selections, patch options and the signing keystore
|
||||
- **Advanced**:
|
||||
- **API URL**: Set the URL of the ReVanced API, ReVanced Manager will use
|
||||
- **Disable version compatibility check**: Patching versions of apps the patches are explicitly compatible with is enforced.
|
||||
Disabling this will allow patching versions of apps the patches are not explicitly compatible with
|
||||
> ⚠️ Warning
|
||||
> Patches may fail patching versions they are not explicitly compatible with.
|
||||
> Unless you know what you are doing, it is recommended to keep this enabled.
|
||||
- **Allow changing patch selection**: The default selection of patches is enforced.
|
||||
Enabling this will allow you to change the patch selection
|
||||
> ⚠️ Warning
|
||||
> Changing the selection may cause unexpected issues.
|
||||
> Patches may fail on app versions they are not explicitly compatible with.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **Require suggested app version**: Specific versions of apps is enforced based on the patch selection automatically.
|
||||
Disabling this will allow you to patch any version of apps
|
||||
> ⚠️ Warning
|
||||
> Patches not compatible with the selected version of the app will not be used.
|
||||
> Unless you know what you are doing, it is recommended to keep this enabled.
|
||||
- **Allow universal patches**: Patches that do not specify compatibility with an app explicitly are forcibly disabled.
|
||||
- **Allow changing patch selection and options**: The default selection of patches is enforced.
|
||||
Enabling this will allow you to change the patch selection
|
||||
> ⚠️ Warning
|
||||
> Changing the selection may cause unexpected issues.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **Allow using universal patches**: Patches that do not specify compatibility with an app are forcibly disabled.
|
||||
Enabling this will allow selecting such patches
|
||||
> ⚠️ Warning
|
||||
> Universal patches do not specify compatibility with an app explicitly may not work on all apps regardless.
|
||||
> Universal patches do not specify compatibility with an app and may not work on all apps regardless.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **About**: View more information and links about ReVanced and ReVanced Manager.
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
[versions]
|
||||
ktx = "1.16.0"
|
||||
material3 = "1.3.2"
|
||||
ui-tooling = "1.8.1"
|
||||
viewmodel-lifecycle = "2.9.0"
|
||||
splash-screen = "1.0.1"
|
||||
activity = "1.10.1"
|
||||
appcompat = "1.7.0"
|
||||
preferences-datastore = "1.1.2"
|
||||
work-runtime = "2.10.1"
|
||||
compose-bom = "2025.05.00"
|
||||
navigation = "2.8.6"
|
||||
accompanist = "0.37.0"
|
||||
placeholder = "1.1.2"
|
||||
reorderable = "2.4.3"
|
||||
serialization = "1.8.0"
|
||||
collection = "0.3.8"
|
||||
datetime = "0.6.1"
|
||||
room-version = "2.7.1"
|
||||
ktx = "1.17.0"
|
||||
material3 = "1.4.0"
|
||||
ui-tooling = "1.10.0"
|
||||
viewmodel-lifecycle = "2.10.0"
|
||||
splash-screen = "1.2.0"
|
||||
activity = "1.12.2"
|
||||
appcompat = "1.7.1"
|
||||
preferences-datastore = "1.2.0"
|
||||
work-runtime = "2.11.0"
|
||||
compose-bom = "2025.12.01"
|
||||
navigation = "2.9.6"
|
||||
accompanist = "0.37.3"
|
||||
placeholder = "1.0.12"
|
||||
reorderable = "3.0.0"
|
||||
serialization = "1.9.0"
|
||||
collection = "0.4.0"
|
||||
datetime = "0.7.1"
|
||||
room-version = "2.8.4"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
koin = "3.5.3"
|
||||
ktor = "2.3.9"
|
||||
markdown-renderer = "0.30.0"
|
||||
koin = "4.1.1"
|
||||
ktor = "3.3.3"
|
||||
markdown-renderer = "0.39.0"
|
||||
fading-edges = "1.0.4"
|
||||
kotlin = "2.1.10"
|
||||
android-gradle-plugin = "8.9.1"
|
||||
dev-tools-gradle-plugin = "2.1.10-1.0.29"
|
||||
about-libraries-gradle-plugin = "12.1.2"
|
||||
kotlin = "2.3.0"
|
||||
android-gradle-plugin = "8.13.2"
|
||||
dev-tools-gradle-plugin = "2.3.4"
|
||||
about-libraries = "13.2.1"
|
||||
coil = "2.7.0"
|
||||
app-icon-loader-coil = "1.5.0"
|
||||
libsu = "6.0.0"
|
||||
@@ -34,9 +34,10 @@ scrollbars = "1.0.4"
|
||||
enumutil = "1.1.1"
|
||||
compose-icons = "1.2.4"
|
||||
kotlin-process = "1.5.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
binary-compatibility-validator = "0.17.0"
|
||||
hidden-api-stub = "4.4.0"
|
||||
binary-compatibility-validator = "0.18.1"
|
||||
semver-parser = "3.0.0"
|
||||
ackpine = "0.19.1"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@@ -67,7 +68,7 @@ coil-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appi
|
||||
accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" }
|
||||
|
||||
# Placeholder
|
||||
placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-material3", version.ref = "placeholder"}
|
||||
placeholder-material3 = { group = "com.eygraber", name = "compose-placeholder-material3", version.ref = "placeholder" }
|
||||
|
||||
# Kotlinx
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||
@@ -90,7 +91,8 @@ koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-comp
|
||||
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
|
||||
|
||||
# About Libraries
|
||||
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
|
||||
about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" }
|
||||
about-libraries-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "about-libraries" }
|
||||
|
||||
# Ktor
|
||||
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
@@ -133,6 +135,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
||||
# Semantic versioning parser
|
||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
|
||||
|
||||
# Ackpine
|
||||
ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
|
||||
ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
@@ -141,5 +147,6 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
|
||||
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
|
||||
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" }
|
||||
about-libraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "about-libraries" }
|
||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
||||
|
||||
Reference in New Issue
Block a user