Compare commits
12 Commits
feat/trans
...
api@1.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53617d92d | ||
|
|
7471710f28 | ||
|
|
65babf5190 | ||
|
|
d937e686f8 | ||
|
|
b0016f7c05 | ||
|
|
763483b65f | ||
|
|
c59b3e4377 | ||
|
|
6bba8ab7b1 | ||
|
|
bc2a2cb397 | ||
|
|
797f8b3286 | ||
|
|
9209718a3b | ||
|
|
86c0ae320f |
9
.github/workflows/build_pull_request.yml
vendored
@@ -12,18 +12,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: |
|
||||
|
||||
2
.github/workflows/open_pull_request.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
|
||||
2
.github/workflows/pull_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: dev
|
||||
clean: true
|
||||
|
||||
2
.github/workflows/push_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Push strings
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
21
.github/workflows/release.yml
vendored
@@ -15,13 +15,14 @@ jobs:
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
@@ -31,12 +32,12 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -58,23 +59,25 @@ jobs:
|
||||
- name: Release API
|
||||
run: npx multi-semantic-release --tag-format 'api@${version}' --ignore-packages app
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Release
|
||||
id: release
|
||||
run: |
|
||||
echo "NEW_TAG=$(npx multi-semantic-release --tag-format 'v${version}' --ignore-packages api | tee | grep 'Created tag ' | sed -E 's/.*Created tag ([^ ]+).*/\1/')" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||
|
||||
- name: Attest
|
||||
if: steps.release.outputs.NEW_TAG != ''
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-name: 'ReVanced Manager ${{ steps.release.outputs.NEW_TAG }}'
|
||||
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
||||
|
||||
2
.github/workflows/update_documentation.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Dispatch event to documentation repository
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: peter-evans/repository-dispatch@v3
|
||||
- uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||
repository: revanced/revanced-documentation
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# api [1.0.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.2...api@1.0.0-dev.3) (2026-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for writing downloader UI with fragments ([#2998](https://github.com/ReVanced/revanced-manager/issues/2998)) ([7471710](https://github.com/ReVanced/revanced-manager/commit/7471710f287b4febb9546a7d7cd59d654b2754e9))
|
||||
|
||||
# api [1.0.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.1...api@1.0.0-dev.2) (2026-02-27)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
@@ -15,8 +14,7 @@ group = "app.revanced"
|
||||
dependencies {
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.fragment.ktx)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -67,12 +65,9 @@ apiValidation {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
name = "githubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-manager")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
|
||||
}
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.0.0-dev.2
|
||||
version = 1.0.0-dev.3
|
||||
|
||||
@@ -6,13 +6,17 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.jvm.java
|
||||
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.ERROR,
|
||||
@@ -34,6 +38,11 @@ interface Scope {
|
||||
* The package name of the downloader.
|
||||
*/
|
||||
val downloaderPackageName: String
|
||||
|
||||
/**
|
||||
* A data directory for this downloader package.
|
||||
*/
|
||||
val dataDir: File
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +58,25 @@ interface GetScope : Scope {
|
||||
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
||||
*/
|
||||
suspend fun requestStartActivity(intent: Intent): Intent?
|
||||
|
||||
/**
|
||||
* Starts an [Activity] using [requestStartActivity] which loads the specified [Fragment].
|
||||
* The fragment may reside in the downloader package.
|
||||
*
|
||||
* @param clazz The class of the fragment to launch.
|
||||
* @param args The fragment arguments.
|
||||
*/
|
||||
suspend fun requestStartFragment(clazz: Class<out Fragment>, args: Bundle?) =
|
||||
requestStartActivity(Intent().apply {
|
||||
setClassName(hostPackageName, "app.revanced.manager.DownloaderActivity")
|
||||
// We shouldn't use the downloader's resources if it is launching a fragment that resides in manager itself.
|
||||
if (clazz.classLoader != GetScope::class.java.classLoader) putExtra(
|
||||
"DOWNLOADER_NAME",
|
||||
downloaderPackageName
|
||||
)
|
||||
putExtra("FRAGMENT_CLASS_NAME", clazz.name)
|
||||
putExtra("FRAGMENT_ARGS", args)
|
||||
})
|
||||
}
|
||||
|
||||
interface BaseDownloadScope : Scope
|
||||
|
||||
@@ -3,8 +3,10 @@ package app.revanced.manager.downloader
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
@@ -31,6 +33,15 @@ suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity()
|
||||
Intent().apply { setClassName(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
|
||||
)
|
||||
|
||||
/**
|
||||
* Starts an [Activity] using [GetScope.requestStartActivity] which loads the specified [Fragment].
|
||||
* The fragment may reside in the downloader package.
|
||||
*
|
||||
* @param args The fragment arguments.
|
||||
*/
|
||||
suspend inline fun <reified T : Fragment> GetScope.requestStartFragment(args: Bundle?) =
|
||||
requestStartFragment(T::class.java, args)
|
||||
|
||||
/**
|
||||
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
|
||||
* @see [DownloaderScope.useService]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.manager.downloader.webview
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.downloader.DownloadUrl
|
||||
import app.revanced.manager.downloader.DownloaderScope
|
||||
@@ -8,6 +9,7 @@ import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.Downloader
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.requestStartFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -121,15 +123,11 @@ suspend fun <T> GetScope.runWebView(
|
||||
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
|
||||
scope.initialUrl = scope.block()
|
||||
|
||||
// Start the webview activity and wait until it finishes.
|
||||
requestStartActivity(Intent().apply {
|
||||
putExtra(
|
||||
WebViewActivity.KEY,
|
||||
WebViewActivity.Parameters(title, scope.binder)
|
||||
)
|
||||
setClassName(
|
||||
hostPackageName,
|
||||
WebViewActivity::class.qualifiedName!!
|
||||
// Start the webview and wait until it finishes.
|
||||
requestStartFragment<WebViewFragment>(Bundle().apply {
|
||||
putParcelable(
|
||||
WebViewFragment.KEY,
|
||||
WebViewFragment.Parameters(title, scope.binder)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
package app.revanced.manager.downloader.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -29,34 +35,59 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
@DownloaderHostApi
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
class WebViewFragment : Fragment(R.layout.webview_fragment) {
|
||||
private val vm by viewModels<WebViewModel>()
|
||||
lateinit var webView: WebView
|
||||
private val args by lazy {
|
||||
arguments?.getParcelable<Parameters>(KEY)!!
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
requireActivity().apply {
|
||||
enableEdgeToEdge()
|
||||
onBackPressedDispatcher.addCallback {
|
||||
if (webView.canGoBack()) webView.goBack()
|
||||
else cancelActivity()
|
||||
}
|
||||
actionBar?.apply {
|
||||
title = args.title
|
||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
addMenuProvider(
|
||||
object : MenuProvider {
|
||||
override fun onCreateMenu(
|
||||
menu: Menu,
|
||||
menuInflater: MenuInflater
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem) =
|
||||
if (menuItem.itemId == android.R.id.home) {
|
||||
cancelActivity()
|
||||
|
||||
true
|
||||
} else false
|
||||
},
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val vm by viewModels<WebViewModel>()
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_webview)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view.findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
val webView = findViewById<WebView>(R.id.webview)
|
||||
onBackPressedDispatcher.addCallback {
|
||||
if (webView.canGoBack()) webView.goBack()
|
||||
else cancelActivity()
|
||||
}
|
||||
webView = view.findViewById<WebView>(R.id.webview)
|
||||
|
||||
val params = intent.getParcelableExtra<Parameters>(KEY)!!
|
||||
actionBar?.apply {
|
||||
title = params.title
|
||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val events = IWebViewEvents.Stub.asInterface(params.events)!!
|
||||
val events = IWebViewEvents.Stub.asInterface(args.events)!!
|
||||
vm.setup(events)
|
||||
|
||||
webView.apply {
|
||||
@@ -73,13 +104,14 @@ class WebViewActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val activity = requireActivity()
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
vm.commands.collect {
|
||||
when (it) {
|
||||
is WebViewModel.Command.Finish -> {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
activity.setResult(Activity.RESULT_OK)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
||||
@@ -90,16 +122,11 @@ class WebViewActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun cancelActivity() {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
val activity = requireActivity()
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
cancelActivity()
|
||||
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
@Parcelize
|
||||
internal class Parameters(
|
||||
val title: String, val events: IBinder
|
||||
@@ -1 +0,0 @@
|
||||
<resources></resources>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
|
||||
<item name="android:windowActionBar">true</item>
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,3 +1,17 @@
|
||||
# app [1.26.0-dev.34](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.33...v1.26.0-dev.34) (2026-03-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve root installation ([#2895](https://github.com/ReVanced/revanced-manager/issues/2895)) ([763483b](https://github.com/ReVanced/revanced-manager/commit/763483b65f755201db087eecc6f3b02b65921177))
|
||||
|
||||
# app [1.26.0-dev.33](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.32...v1.26.0-dev.33) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Adjust ReVanced icons ([#2890](https://github.com/ReVanced/revanced-manager/issues/2890)) ([bc2a2cb](https://github.com/ReVanced/revanced-manager/commit/bc2a2cb3976234c8a74db93a593e103d77d22a07))
|
||||
|
||||
# app [1.26.0-dev.32](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.31...v1.26.0-dev.32) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.32
|
||||
version = 1.26.0-dev.34
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
<activity android:name=".DownloaderActivity" android:exported="false" android:theme="@style/Theme.DownloaderActivity" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
|
||||
62
app/src/main/java/app/revanced/manager/DownloaderActivity.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.fragment.app.commit
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
import app.revanced.manager.network.downloader.DownloaderPackageState
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class DownloaderActivity : FragmentActivity() {
|
||||
private val downloaderRepository: DownloaderRepository by inject()
|
||||
var downloaderPackageName = ""
|
||||
|
||||
private val downloaderPkgState
|
||||
get() = downloaderRepository
|
||||
.downloaderPackageStates
|
||||
.value[downloaderPackageName]
|
||||
?.let { it as? DownloaderPackageState.Loaded }
|
||||
|
||||
private var res: Resources? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val view = FragmentContainerView(this).apply {
|
||||
// The fragment manager requires an ID to work.
|
||||
id = R.id.fragment_container
|
||||
}
|
||||
setContentView(
|
||||
view,
|
||||
ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
)
|
||||
|
||||
downloaderPackageName = intent.getStringExtra("DOWNLOADER_NAME").orEmpty()
|
||||
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")!!
|
||||
val args = intent.getBundleExtra("FRAGMENT_ARGS")
|
||||
|
||||
res =
|
||||
downloaderPkgState?.context?.createConfigurationContext(super.resources.configuration)?.resources
|
||||
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val fragmentClass = classLoader!!.loadClass(fragmentClassName) as Class<Fragment>
|
||||
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.fragment_container, fragmentClass, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getClassLoader(): ClassLoader? =
|
||||
downloaderPkgState?.classLoader ?: super.classLoader
|
||||
|
||||
override fun getResources(): Resources? = res ?: super.resources
|
||||
}
|
||||
@@ -54,7 +54,11 @@ class RootInstaller(
|
||||
await()
|
||||
}
|
||||
|
||||
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
|
||||
suspend fun execute(vararg commands: String): Shell.Result {
|
||||
val stdout = mutableListOf<String>()
|
||||
val stderr = mutableListOf<String>()
|
||||
return getShell().newJob().add(*commands).to(stdout, stderr).exec()
|
||||
}
|
||||
|
||||
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||
|
||||
@@ -108,20 +112,18 @@ class RootInstaller(
|
||||
unmount(packageName)
|
||||
|
||||
stockAPK?.let { stockApp ->
|
||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||
// TODO: get user id programmatically
|
||||
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
|
||||
pm.getPackageInfo(patchedAPK)
|
||||
?: error("Failed to get package info for patched app")
|
||||
)
|
||||
)
|
||||
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
||||
}
|
||||
// TODO: get user id programmatically
|
||||
execute("pm uninstall -k --user 0 $packageName")
|
||||
|
||||
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
|
||||
execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"")
|
||||
.assertSuccess("Failed to install stock app")
|
||||
}
|
||||
|
||||
remoteFS.getFile(modulePath).mkdirs()
|
||||
remoteFS.getFile(modulePath).apply {
|
||||
if (!mkdirs() && !exists()) {
|
||||
throw Exception("Failed to create module directory")
|
||||
}
|
||||
}
|
||||
|
||||
listOf(
|
||||
"service.sh",
|
||||
@@ -142,7 +144,6 @@ class RootInstaller(
|
||||
}
|
||||
|
||||
"$modulePath/$packageName.apk".let { apkPath ->
|
||||
|
||||
remoteFS.getFile(patchedAPK.absolutePath)
|
||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||
.newInputStream().use { inputStream ->
|
||||
@@ -173,9 +174,48 @@ class RootInstaller(
|
||||
const val modulesPath = "/data/adb/modules"
|
||||
|
||||
private fun Shell.Result.assertSuccess(errorMessage: String) {
|
||||
if (!isSuccess) throw Exception(errorMessage)
|
||||
if (!isSuccess) {
|
||||
throw ShellCommandException(
|
||||
errorMessage,
|
||||
code,
|
||||
out,
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShellCommandException(
|
||||
val userMessage: String,
|
||||
val exitCode: Int,
|
||||
val stdout: List<String>,
|
||||
val stderr: List<String>
|
||||
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
|
||||
companion object {
|
||||
private fun format(
|
||||
message: String,
|
||||
exitCode: Int,
|
||||
stdout: List<String>,
|
||||
stderr: List<String>
|
||||
): String =
|
||||
buildString {
|
||||
appendLine(message)
|
||||
appendLine("Exit code: $exitCode")
|
||||
|
||||
val output = stdout.filter { it.isNotBlank() }
|
||||
val errors = stderr.filter { it.isNotBlank() }
|
||||
|
||||
if (output.isNotEmpty()) {
|
||||
appendLine("stdout:")
|
||||
output.forEach(::appendLine)
|
||||
}
|
||||
if (errors.isNotEmpty()) {
|
||||
appendLine("stderr:")
|
||||
errors.forEach(::appendLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RootServiceException : Exception("Root not available")
|
||||
@@ -6,8 +6,10 @@ import android.os.Parcelable
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
||||
import app.revanced.manager.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.util.PM
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
@@ -21,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.io.path.outputStream
|
||||
|
||||
class DownloadedAppRepository(
|
||||
private val app: Application,
|
||||
app: Application,
|
||||
db: AppDatabase,
|
||||
private val pm: PM
|
||||
) {
|
||||
@@ -35,6 +37,7 @@ class DownloadedAppRepository(
|
||||
|
||||
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
suspend fun download(
|
||||
downloader: LoadedDownloader,
|
||||
data: Parcelable,
|
||||
@@ -54,9 +57,7 @@ class DownloadedAppRepository(
|
||||
val downloadedBytes = AtomicLong(0)
|
||||
|
||||
channelFlow {
|
||||
val scope = object : OutputDownloadScope {
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
override val hostPackageName = app.packageName
|
||||
val scope = object : OutputDownloadScope, Scope by downloader.scopeImpl {
|
||||
override suspend fun reportSize(size: Long) {
|
||||
require(size > 0) { "Size must be greater than zero" }
|
||||
require(
|
||||
@@ -87,7 +88,7 @@ class DownloadedAppRepository(
|
||||
)
|
||||
}
|
||||
}
|
||||
downloader.download(scope, data, stream)
|
||||
downloader.impl.download(scope, data, stream)
|
||||
}
|
||||
}
|
||||
.conflate()
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.revanced.manager.domain.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
@@ -50,6 +52,8 @@ class DownloaderRepository(
|
||||
installed subtract acknowledged
|
||||
}
|
||||
|
||||
private val downloadersDir = app.getDir("downloaders", Context.MODE_PRIVATE)
|
||||
|
||||
suspend fun reload() {
|
||||
val downloaderPackages =
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -65,8 +69,12 @@ class DownloaderRepository(
|
||||
acknowledgedDownloader subtract installedDownloaderPackageNames.value
|
||||
if (uninstalledDownloader.isNotEmpty()) {
|
||||
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
|
||||
|
||||
this@DownloaderRepository.acknowledgedPackageNames.update(acknowledgedDownloader subtract uninstalledDownloader)
|
||||
trustDao.removeAll(uninstalledDownloader)
|
||||
withContext(Dispatchers.IO) {
|
||||
uninstalledDownloader.forEach { downloadersDir.resolve(it).deleteRecursively() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +117,8 @@ class DownloaderRepository(
|
||||
val scopeImpl = object : Scope {
|
||||
override val hostPackageName = app.packageName
|
||||
override val downloaderPackageName = downloaderContext.packageName
|
||||
override val dataDir =
|
||||
downloadersDir.resolve(downloaderPackageName).also(File::mkdirs)
|
||||
}
|
||||
|
||||
DownloaderPackageState.Loaded(
|
||||
@@ -126,11 +136,12 @@ class DownloaderRepository(
|
||||
className,
|
||||
downloaderContext.getString(downloader.name),
|
||||
packageInfo.versionName!!,
|
||||
downloader.get,
|
||||
downloader.download
|
||||
scopeImpl,
|
||||
downloader
|
||||
)
|
||||
},
|
||||
classLoader,
|
||||
downloaderContext,
|
||||
with(pm) { packageInfo.label() }
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.content.Context
|
||||
|
||||
sealed interface DownloaderPackageState {
|
||||
data object Untrusted : DownloaderPackageState
|
||||
|
||||
data class Loaded(
|
||||
val downloaders: List<LoadedDownloader>,
|
||||
val classLoader: ClassLoader,
|
||||
val context: Context,
|
||||
val name: String
|
||||
) : DownloaderPackageState
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import java.io.OutputStream
|
||||
import app.revanced.manager.downloader.Downloader
|
||||
import app.revanced.manager.downloader.Scope
|
||||
|
||||
class LoadedDownloader(
|
||||
val packageName: String,
|
||||
val className: String,
|
||||
val name: String,
|
||||
val version: String,
|
||||
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
|
||||
val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit
|
||||
val scopeImpl: Scope,
|
||||
val impl: Downloader<Parcelable>
|
||||
)
|
||||
@@ -38,6 +38,7 @@ import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toRemoteError
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
@@ -185,11 +186,7 @@ class PatcherWorker(
|
||||
downloaderRepository.loadedDownloadersFlow.first()
|
||||
.firstNotNullOfOrNull { downloader ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
override val hostPackageName =
|
||||
applicationContext.packageName
|
||||
|
||||
val getScope = object : GetScope, Scope by downloader.scopeImpl {
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result =
|
||||
args.handleStartActivityRequest(downloader, intent)
|
||||
@@ -204,7 +201,7 @@ class PatcherWorker(
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.get(
|
||||
downloader.impl.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
@@ -30,18 +29,18 @@ import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
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.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.withState
|
||||
import app.revanced.manager.util.PM
|
||||
@@ -80,6 +79,7 @@ import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
||||
class PatcherViewModel(
|
||||
@@ -404,24 +404,26 @@ class PatcherViewModel(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
@@ -437,17 +439,6 @@ class PatcherViewModel(
|
||||
currentPackageInfo.label()
|
||||
}
|
||||
|
||||
// Check for base APK, first check if the app is already installed
|
||||
if (existingPackageInfo == null) {
|
||||
// If the app is not installed, check if the output file is a base apk
|
||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||
// Exit if there is no base APK package
|
||||
packageInstallerStatus =
|
||||
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val inputVersion = input.selectedApp.version
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
|
||||
@@ -34,6 +34,7 @@ import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
@@ -192,9 +193,7 @@ class SelectedAppInfoViewModel(
|
||||
cancelDownloaderAction()
|
||||
downloaderAction = downloader to viewModelScope.launch {
|
||||
try {
|
||||
val scope = object : GetScope {
|
||||
override val hostPackageName = app.packageName
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
val scope = object : GetScope, Scope by downloader.scopeImpl {
|
||||
override suspend fun requestStartActivity(intent: Intent) =
|
||||
withContext(Dispatchers.Main) {
|
||||
if (launchedActivity != null) error("Previous activity has not finished")
|
||||
@@ -219,7 +218,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.get(scope, packageName, desiredVersion)
|
||||
downloader.impl.get(scope, packageName, desiredVersion)
|
||||
}?.let { (data, version) ->
|
||||
if (desiredVersion != null && version != desiredVersion) {
|
||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
<!-- Copyright 2024 ReVanced. Not licensed under GPL. See https://github.com/ReVanced/revanced-branding -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<group android:scaleX="0.23"
|
||||
android:scaleY="0.23"
|
||||
android:translateX="98.56"
|
||||
android:translateY="98.56">
|
||||
<path
|
||||
android:pathData="M253.85,4.9C254.32,3.82 254.22,2.57 253.58,1.58C252.93,0.6 251.83,0 250.64,0C243.29,0 230.47,0 225.95,0C224.96,0 224.06,0.59 223.66,1.5C216.03,18.88 144.1,182.7 130.29,214.16C129.89,215.07 128.99,215.66 128,215.66C127.01,215.66 126.11,215.07 125.71,214.16C111.9,182.7 39.97,18.88 32.34,1.5C31.94,0.59 31.04,0 30.05,0C25.53,0 12.71,0 5.36,0C4.17,0 3.07,0.6 2.42,1.58C1.78,2.57 1.68,3.82 2.15,4.9C16.78,38.3 101.47,231.61 111.24,253.9C111.8,255.18 113.06,256 114.45,256C120.29,256 135.71,256 141.55,256C142.94,256 144.2,255.18 144.76,253.9C154.52,231.61 239.22,38.3 253.85,4.9Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M130.59,131.75C130.06,132.68 129.07,133.25 128,133.25C126.93,133.25 125.94,132.68 125.4,131.75C113.45,111.06 63.88,25.19 51.93,4.5C51.4,3.57 51.4,2.43 51.93,1.5C52.47,0.57 53.46,-0 54.53,-0L201.47,-0C202.54,-0 203.53,0.57 204.06,1.5C204.6,2.43 204.6,3.57 204.06,4.5C192.12,25.19 142.54,111.06 130.59,131.75Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="128"
|
||||
android:startY="-0"
|
||||
android:endX="128"
|
||||
android:endY="254.6"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFF04E98"/>
|
||||
<item android:offset="0.5" android:color="#FF5F65D4"/>
|
||||
<item android:offset="1" android:color="#FF4E98F0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:scaleX="0.24"
|
||||
android:scaleY="0.24"
|
||||
android:translateX="97.28"
|
||||
android:translateY="97.28">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"/>
|
||||
<path
|
||||
android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,0 63.62,0H192.37C195.32,0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="128"
|
||||
android:startY="0"
|
||||
android:endX="128"
|
||||
android:endY="254.6"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:offset="0"
|
||||
android:color="#FFF04E98"/>
|
||||
<item
|
||||
android:offset="0.5"
|
||||
android:color="#FF5F65D4"/>
|
||||
<item
|
||||
android:offset="1"
|
||||
android:color="#FF4E98F0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
21
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!-- Copyright 2024 ReVanced. Not licensed under GPL. See https://github.com/ReVanced/revanced-branding -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
|
||||
<group
|
||||
android:scaleX="0.3"
|
||||
android:scaleY="0.3"
|
||||
android:translateX="89.6"
|
||||
android:translateY="89.6">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,0 63.62,0H192.37C195.32,0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
40
app/src/main/res/drawable/ic_launcher_splash_screen.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<!-- Copyright 2024 ReVanced. Not licensed under GPL. See https://github.com/ReVanced/revanced-branding -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
|
||||
<group
|
||||
android:scaleX="0.3"
|
||||
android:scaleY="0.3"
|
||||
android:translateX="89.6"
|
||||
android:translateY="89.6">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorPrimary"
|
||||
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0H228.81C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0H14.73C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256H137.55C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"/>
|
||||
<path
|
||||
android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,0 63.62,0H192.37C195.32,0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="128"
|
||||
android:startY="0"
|
||||
android:endX="128"
|
||||
android:endY="254.6"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:offset="0"
|
||||
android:color="#FFF04E98"/>
|
||||
<item
|
||||
android:offset="0.5"
|
||||
android:color="#FF5F65D4"/>
|
||||
<item
|
||||
android:offset="1"
|
||||
android:color="#FF4E98F0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,29 +1,64 @@
|
||||
<vector android:height="72dp" android:viewportHeight="800"
|
||||
android:viewportWidth="800" android:width="72dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#1b1b1b" android:fillType="evenOdd"
|
||||
android:pathData="M400,400m-400,0a400,400 0,1 1,800 0a400,400 0,1 1,-800 0" android:strokeLineJoin="round"/>
|
||||
<path android:fillType="evenOdd"
|
||||
android:pathData="m400,0c220.77,0 400,179.23 400,400s-179.23,400 -400,400 -400,-179.23 -400,-400 179.23,-400 400,-400zM400,36c200.9,-0 364,163.1 364,364s-163.1,364 -364,364 -364,-163.1 -364,-364 163.1,-364 364,-364z" android:strokeLineJoin="round">
|
||||
<!-- Copyright 2024 ReVanced. Not licensed under GPL. See https://github.com/ReVanced/revanced-branding -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="800"
|
||||
android:viewportHeight="800">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF1B1B1B"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M400,400m-400,0a400,400 0,1 1,800 0a400,400 0,1 1,-800 0"
|
||||
android:strokeLineJoin="round"/>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M400,0c220.77,0 400,179.23 400,400s-179.23,400 -400,400 -400,-179.23 -400,-400 179.23,-400 400,-400zM400,36c200.9,0 364,163.1 364,364s-163.1,364 -364,364 -364,-163.1 -364,-364 163.1,-364 364,-364z"
|
||||
android:strokeLineJoin="round">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="400" android:endY="800"
|
||||
android:startX="400" android:startY="0" android:type="linear">
|
||||
<item android:color="#FFF04E98" android:offset="0"/>
|
||||
<item android:color="#FF5F65D4" android:offset="0.5"/>
|
||||
<item android:color="#FF4E98F0" android:offset="1"/>
|
||||
<gradient
|
||||
android:startX="400"
|
||||
android:startY="0"
|
||||
android:endX="400"
|
||||
android:endY="800"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:offset="0"
|
||||
android:color="#FFF04E98"/>
|
||||
<item
|
||||
android:offset="0.5"
|
||||
android:color="#FF5F65D4"/>
|
||||
<item
|
||||
android:offset="1"
|
||||
android:color="#FF4E98F0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#fff" android:fillType="evenOdd"
|
||||
android:pathData="m538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37 -2.02,-3.09 -5.46,-4.95 -9.16,-4.95h-14.16c-3.1,0 -5.91,1.83 -7.15,4.67 -12.47,28.4 -78.27,178.27 -100.25,228.33 -1.25,2.84 -4.05,4.67 -7.15,4.67 -3.1,0 -5.91,-1.83 -7.15,-4.67 -21.98,-50.06 -87.78,-199.93 -100.25,-228.33 -1.25,-2.84 -4.05,-4.67 -7.15,-4.67h-14.16c-3.69,0 -7.14,1.86 -9.16,4.95 -2.02,3.09 -2.34,6.99 -0.86,10.37 23.56,53.77 101.87,232.52 117.87,269.03 1.74,3.98 5.67,6.55 10.02,6.55h21.7c4.34,-0 8.27,-2.57 10.02,-6.55 16,-36.51 94.32,-215.27 117.87,-269.03z" android:strokeLineJoin="round"/>
|
||||
<path android:fillType="evenOdd"
|
||||
android:pathData="m408.12,395.31c-1.67,2.9 -4.77,4.69 -8.12,4.69s-6.44,-1.79 -8.12,-4.69c-17,-29.44 -56.16,-97.26 -73.15,-126.7 -1.67,-2.9 -1.67,-6.47 0,-9.38s4.77,-4.69 8.12,-4.69h146.31c3.35,0 6.44,1.79 8.12,4.69s1.67,6.47 -0,9.38c-17,29.44 -56.16,97.26 -73.15,126.7z" android:strokeLineJoin="round">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37 -2.02,-3.09 -5.46,-4.95 -9.16,-4.95h-14.16c-3.1,0 -5.91,1.83 -7.15,4.67 -12.47,28.4 -78.27,178.27 -100.25,228.33 -1.25,2.84 -4.05,4.67 -7.15,4.67 -3.1,0 -5.91,-1.83 -7.15,-4.67 -21.98,-50.06 -87.78,-199.93 -100.25,-228.33 -1.25,-2.84 -4.05,-4.67 -7.15,-4.67h-14.16c-3.69,0 -7.14,1.86 -9.16,4.95 -2.02,3.09 -2.34,6.99 -0.86,10.37 23.56,53.77 101.87,232.52 117.87,269.03 1.74,3.98 5.67,6.55 10.02,6.55h21.7c4.34,0 8.27,-2.57 10.02,-6.55 16,-36.51 94.32,-215.27 117.87,-269.03z"
|
||||
android:strokeLineJoin="round"/>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M408.12,395.31c-1.67,2.9 -4.77,4.69 -8.12,4.69s-6.44,-1.79 -8.12,-4.69c-17,-29.44 -56.16,-97.26 -73.15,-126.7 -1.67,-2.9 -1.67,-6.47 0,-9.38s4.77,-4.69 8.12,-4.69h146.31c3.35,0 6.44,1.79 8.12,4.69s1.67,6.47 0,9.38c-17,29.44 -56.16,97.26 -73.15,126.7z"
|
||||
android:strokeLineJoin="round">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="400" android:endY="543.86"
|
||||
android:startX="400" android:startY="254.54" android:type="linear">
|
||||
<item android:color="#FFF04E98" android:offset="0"/>
|
||||
<item android:color="#FF5F65D4" android:offset="0.5"/>
|
||||
<item android:color="#FF4E98F0" android:offset="1"/>
|
||||
<gradient
|
||||
android:startX="400"
|
||||
android:startY="254.54"
|
||||
android:endX="400"
|
||||
android:endY="543.86"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:offset="0"
|
||||
android:color="#FFF04E98"/>
|
||||
<item
|
||||
android:offset="0.5"
|
||||
android:color="#FF5F65D4"/>
|
||||
<item
|
||||
android:offset="1"
|
||||
android:color="#FF4E98F0"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.3 KiB |
11
app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- Copyright 2024 ReVanced. Not licensed under GPL. See https://github.com/ReVanced/revanced-branding -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M4.03516,3C3.80859,3 3.59375,3.11719 3.46875,3.30469C3.34375,3.49609 3.32422,3.73828 3.41406,3.94922C4.87109,7.27344 9.71875,18.3359 10.707,20.5938C10.8164,20.8398 11.0586,21 11.3281,21H12.6719C12.9375,21 13.1836,20.8398 13.293,20.5938C14.2813,18.3359 19.1289,7.27344 20.5859,3.94922C20.6758,3.73828 20.6563,3.49609 20.5313,3.30469C20.4063,3.11719 20.1914,3 19.9648,3H19.0898C18.8984,3 18.7227,3.11328 18.6445,3.28906C17.875,5.04688 13.8008,14.3203 12.4414,17.418C12.3672,17.5938 12.1914,17.707 12,17.707C11.8086,17.707 11.6328,17.5938 11.5586,17.418C10.1992,14.3203 6.125,5.04688 5.35547,3.28906C5.27734,3.11328 5.10156,3 4.91016,3H7.47266C7.26563,3 7.07422,3.10938 6.97266,3.28906C6.86719,3.46875 6.86719,3.69141 6.97266,3.87109C8.02344,5.69141 10.4453,9.88672 11.4961,11.7109C11.6016,11.8906 11.793,12 12,12C12.207,12 12.3984,11.8906 12.5039,11.7109C13.5547,9.88672 15.9766,5.69141 17.0273,3.87109C17.1328,3.69141 17.1328,3.46875 17.0273,3.28906C16.9258,3.10938 16.7344,3 16.5273,3H7.47266"/>
|
||||
</vector>
|
||||
@@ -2,5 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.5 KiB |
@@ -7,6 +7,5 @@
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="ic_launcher_background">#1B1B1B</color>
|
||||
</resources>
|
||||
<color name="ic_launcher_background">#FF1B1B1B</color>
|
||||
</resources>
|
||||
|
||||
4
app/src/main/res/values/id.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item type="id" name="fragment_container" />
|
||||
</resources>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="patch_count">
|
||||
<item quantity="one">%d patch</item>
|
||||
<item quantity="other">%d patches</item>
|
||||
</plurals>
|
||||
<plurals name="patches_executed">
|
||||
<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>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -516,4 +516,16 @@ Click on the patches to see more details.</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="new_announcement">New announcement:\n%s</string>
|
||||
|
||||
<plurals name="patch_count">
|
||||
<item quantity="one">%d patch</item>
|
||||
<item quantity="other">%d patches</item>
|
||||
</plurals>
|
||||
<plurals name="patches_executed">
|
||||
<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>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ReVancedManager" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_splash_screen</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
<style name="Theme.DownloaderActivity" parent="Theme.AppCompat.DayNight">
|
||||
<item name="android:windowActionBar">true</item>
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -3,4 +3,5 @@ android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
@@ -1,11 +1,12 @@
|
||||
[versions]
|
||||
ktx = "1.17.0"
|
||||
ktx = "1.18.0"
|
||||
material3 = "1.5.0-alpha14"
|
||||
ui-tooling = "1.10.0"
|
||||
viewmodel-lifecycle = "2.10.0"
|
||||
splash-screen = "1.2.0"
|
||||
activity = "1.12.2"
|
||||
appcompat = "1.7.1"
|
||||
fragment = "1.8.9"
|
||||
preferences-datastore = "1.2.0"
|
||||
work-runtime = "2.11.0"
|
||||
compose-bom = "2025.12.01"
|
||||
@@ -13,15 +14,15 @@ navigation = "2.9.7"
|
||||
accompanist = "0.37.3"
|
||||
placeholder = "1.0.12"
|
||||
reorderable = "3.0.0"
|
||||
serialization = "1.9.0"
|
||||
serialization = "1.10.0"
|
||||
collection = "0.4.0"
|
||||
datetime = "0.7.1"
|
||||
room-version = "2.8.4"
|
||||
revanced-patcher = "22.0.0"
|
||||
revanced-library = "4.0.0"
|
||||
koin = "4.1.1"
|
||||
ktor = "3.3.3"
|
||||
markdown-renderer = "0.39.0"
|
||||
ktor = "3.4.1"
|
||||
markdown-renderer = "0.39.2"
|
||||
fading-edges = "1.0.4"
|
||||
kotlin = "2.3.10"
|
||||
android-gradle-plugin = "8.13.2"
|
||||
@@ -37,7 +38,7 @@ kotlin-process = "1.5.1"
|
||||
hidden-api-stub = "4.4.0"
|
||||
binary-compatibility-validator = "0.18.1"
|
||||
semver-parser = "3.0.0"
|
||||
ackpine = "0.19.1"
|
||||
ackpine = "0.20.6"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@@ -46,6 +47,7 @@ runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", ve
|
||||
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
|
||||
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
|
||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
|
||||
fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
|
||||
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
|
||||
@@ -14,12 +14,10 @@ dependencyResolutionManagement {
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
maven {
|
||||
name = "githubPackages"
|
||||
// A repository must be specified for some reason. "registry" is a dummy.
|
||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
|
||||
}
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||