mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-03-13 08:41:57 +08:00
Compare commits
7 Commits
v1.26.0-de
...
api@1.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53617d92d | ||
|
|
7471710f28 | ||
|
|
65babf5190 | ||
|
|
d937e686f8 | ||
|
|
b0016f7c05 | ||
|
|
763483b65f | ||
|
|
c59b3e4377 |
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: ./gradlew assembleRelease --no-daemon
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: revanced-manager
|
name: revanced-manager
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
2
.github/workflows/open_pull_request.yml
vendored
2
.github/workflows/open_pull_request.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
|
|||||||
2
.github/workflows/pull_strings.yml
vendored
2
.github/workflows/pull_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
clean: true
|
clean: true
|
||||||
|
|||||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Push strings
|
- name: Push strings
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: ./gradlew assembleRelease
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@v6
|
uses: crazy-max/ghaction-import-gpg@v7
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
|||||||
2
.github/workflows/update_documentation.yml
vendored
2
.github/workflows/update_documentation.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Dispatch event to documentation repository
|
name: Dispatch event to documentation repository
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/repository-dispatch@v3
|
- uses: peter-evans/repository-dispatch@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||||
repository: revanced/revanced-documentation
|
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)
|
# 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.android.library)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.compose.compiler)
|
|
||||||
alias(libs.plugins.binary.compatibility.validator)
|
alias(libs.plugins.binary.compatibility.validator)
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
signing
|
signing
|
||||||
@@ -15,8 +14,7 @@ group = "app.revanced"
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.ktx)
|
implementation(libs.androidx.ktx)
|
||||||
implementation(libs.runtime.ktx)
|
implementation(libs.runtime.ktx)
|
||||||
implementation(libs.activity.compose)
|
implementation(libs.fragment.ktx)
|
||||||
implementation(libs.appcompat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|||||||
@@ -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.content.ServiceConnection
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlin.jvm.java
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
level = RequiresOptIn.Level.ERROR,
|
level = RequiresOptIn.Level.ERROR,
|
||||||
@@ -34,6 +38,11 @@ interface Scope {
|
|||||||
* The package name of the downloader.
|
* The package name of the downloader.
|
||||||
*/
|
*/
|
||||||
val downloaderPackageName: String
|
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.
|
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
||||||
*/
|
*/
|
||||||
suspend fun requestStartActivity(intent: Intent): Intent?
|
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
|
interface BaseDownloadScope : Scope
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package app.revanced.manager.downloader
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +33,15 @@ suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity()
|
|||||||
Intent().apply { setClassName(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
|
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].
|
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
|
||||||
* @see [DownloaderScope.useService]
|
* @see [DownloaderScope.useService]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.downloader.webview
|
package app.revanced.manager.downloader.webview
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.downloader.DownloadUrl
|
import app.revanced.manager.downloader.DownloadUrl
|
||||||
import app.revanced.manager.downloader.DownloaderScope
|
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.Scope
|
||||||
import app.revanced.manager.downloader.Downloader
|
import app.revanced.manager.downloader.Downloader
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.downloader.DownloaderHostApi
|
||||||
|
import app.revanced.manager.downloader.requestStartFragment
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -121,15 +123,11 @@ suspend fun <T> GetScope.runWebView(
|
|||||||
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
|
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
|
||||||
scope.initialUrl = scope.block()
|
scope.initialUrl = scope.block()
|
||||||
|
|
||||||
// Start the webview activity and wait until it finishes.
|
// Start the webview and wait until it finishes.
|
||||||
requestStartActivity(Intent().apply {
|
requestStartFragment<WebViewFragment>(Bundle().apply {
|
||||||
putExtra(
|
putParcelable(
|
||||||
WebViewActivity.KEY,
|
WebViewFragment.KEY,
|
||||||
WebViewActivity.Parameters(title, scope.binder)
|
WebViewFragment.Parameters(title, scope.binder)
|
||||||
)
|
|
||||||
setClassName(
|
|
||||||
hostPackageName,
|
|
||||||
WebViewActivity::class.qualifiedName!!
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
package app.revanced.manager.downloader.webview
|
package app.revanced.manager.downloader.webview
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -29,34 +35,59 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(DownloaderHostApi::class)
|
||||||
@DownloaderHostApi
|
@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")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val vm by viewModels<WebViewModel>()
|
ViewCompat.setOnApplyWindowInsetsListener(view.findViewById(R.id.main)) { v, insets ->
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_webview)
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
val webView = findViewById<WebView>(R.id.webview)
|
webView = view.findViewById<WebView>(R.id.webview)
|
||||||
onBackPressedDispatcher.addCallback {
|
|
||||||
if (webView.canGoBack()) webView.goBack()
|
|
||||||
else cancelActivity()
|
|
||||||
}
|
|
||||||
|
|
||||||
val params = intent.getParcelableExtra<Parameters>(KEY)!!
|
val events = IWebViewEvents.Stub.asInterface(args.events)!!
|
||||||
actionBar?.apply {
|
|
||||||
title = params.title
|
|
||||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val events = IWebViewEvents.Stub.asInterface(params.events)!!
|
|
||||||
vm.setup(events)
|
vm.setup(events)
|
||||||
|
|
||||||
webView.apply {
|
webView.apply {
|
||||||
@@ -73,13 +104,14 @@ class WebViewActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val activity = requireActivity()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
vm.commands.collect {
|
vm.commands.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is WebViewModel.Command.Finish -> {
|
is WebViewModel.Command.Finish -> {
|
||||||
setResult(RESULT_OK)
|
activity.setResult(Activity.RESULT_OK)
|
||||||
finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
||||||
@@ -90,16 +122,11 @@ class WebViewActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelActivity() {
|
private fun cancelActivity() {
|
||||||
setResult(RESULT_CANCELED)
|
val activity = requireActivity()
|
||||||
finish()
|
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
|
@Parcelize
|
||||||
internal class Parameters(
|
internal class Parameters(
|
||||||
val title: String, val events: IBinder
|
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,10 @@
|
|||||||
|
# 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)
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version = 1.26.0-dev.33
|
version = 1.26.0-dev.34
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
|||||||
62
app/src/main/java/app/revanced/manager/DownloaderActivity.kt
Normal file
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()
|
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
|
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||||
|
|
||||||
@@ -108,20 +112,18 @@ class RootInstaller(
|
|||||||
unmount(packageName)
|
unmount(packageName)
|
||||||
|
|
||||||
stockAPK?.let { stockApp ->
|
stockAPK?.let { stockApp ->
|
||||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
// TODO: get user id programmatically
|
||||||
// TODO: get user id programmatically
|
execute("pm uninstall -k --user 0 $packageName")
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
listOf(
|
||||||
"service.sh",
|
"service.sh",
|
||||||
@@ -142,7 +144,6 @@ class RootInstaller(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"$modulePath/$packageName.apk".let { apkPath ->
|
"$modulePath/$packageName.apk".let { apkPath ->
|
||||||
|
|
||||||
remoteFS.getFile(patchedAPK.absolutePath)
|
remoteFS.getFile(patchedAPK.absolutePath)
|
||||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||||
.newInputStream().use { inputStream ->
|
.newInputStream().use { inputStream ->
|
||||||
@@ -173,9 +174,48 @@ class RootInstaller(
|
|||||||
const val modulesPath = "/data/adb/modules"
|
const val modulesPath = "/data/adb/modules"
|
||||||
|
|
||||||
private fun Shell.Result.assertSuccess(errorMessage: String) {
|
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")
|
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
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
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.network.downloader.LoadedDownloader
|
||||||
import app.revanced.manager.downloader.OutputDownloadScope
|
import app.revanced.manager.downloader.OutputDownloadScope
|
||||||
|
import app.revanced.manager.downloader.Scope
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
@@ -21,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
|
|||||||
import kotlin.io.path.outputStream
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
class DownloadedAppRepository(
|
class DownloadedAppRepository(
|
||||||
private val app: Application,
|
app: Application,
|
||||||
db: AppDatabase,
|
db: AppDatabase,
|
||||||
private val pm: PM
|
private val pm: PM
|
||||||
) {
|
) {
|
||||||
@@ -35,6 +37,7 @@ class DownloadedAppRepository(
|
|||||||
|
|
||||||
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
|
@OptIn(DownloaderHostApi::class)
|
||||||
suspend fun download(
|
suspend fun download(
|
||||||
downloader: LoadedDownloader,
|
downloader: LoadedDownloader,
|
||||||
data: Parcelable,
|
data: Parcelable,
|
||||||
@@ -54,9 +57,7 @@ class DownloadedAppRepository(
|
|||||||
val downloadedBytes = AtomicLong(0)
|
val downloadedBytes = AtomicLong(0)
|
||||||
|
|
||||||
channelFlow {
|
channelFlow {
|
||||||
val scope = object : OutputDownloadScope {
|
val scope = object : OutputDownloadScope, Scope by downloader.scopeImpl {
|
||||||
override val downloaderPackageName = downloader.packageName
|
|
||||||
override val hostPackageName = app.packageName
|
|
||||||
override suspend fun reportSize(size: Long) {
|
override suspend fun reportSize(size: Long) {
|
||||||
require(size > 0) { "Size must be greater than zero" }
|
require(size > 0) { "Size must be greater than zero" }
|
||||||
require(
|
require(
|
||||||
@@ -87,7 +88,7 @@ class DownloadedAppRepository(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloader.download(scope, data, stream)
|
downloader.impl.download(scope, data, stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.conflate()
|
.conflate()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.revanced.manager.domain.repository
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(DownloaderHostApi::class)
|
||||||
@@ -50,6 +52,8 @@ class DownloaderRepository(
|
|||||||
installed subtract acknowledged
|
installed subtract acknowledged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val downloadersDir = app.getDir("downloaders", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
suspend fun reload() {
|
suspend fun reload() {
|
||||||
val downloaderPackages =
|
val downloaderPackages =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -65,8 +69,12 @@ class DownloaderRepository(
|
|||||||
acknowledgedDownloader subtract installedDownloaderPackageNames.value
|
acknowledgedDownloader subtract installedDownloaderPackageNames.value
|
||||||
if (uninstalledDownloader.isNotEmpty()) {
|
if (uninstalledDownloader.isNotEmpty()) {
|
||||||
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
|
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
|
||||||
|
|
||||||
this@DownloaderRepository.acknowledgedPackageNames.update(acknowledgedDownloader subtract uninstalledDownloader)
|
this@DownloaderRepository.acknowledgedPackageNames.update(acknowledgedDownloader subtract uninstalledDownloader)
|
||||||
trustDao.removeAll(uninstalledDownloader)
|
trustDao.removeAll(uninstalledDownloader)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
uninstalledDownloader.forEach { downloadersDir.resolve(it).deleteRecursively() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +117,8 @@ class DownloaderRepository(
|
|||||||
val scopeImpl = object : Scope {
|
val scopeImpl = object : Scope {
|
||||||
override val hostPackageName = app.packageName
|
override val hostPackageName = app.packageName
|
||||||
override val downloaderPackageName = downloaderContext.packageName
|
override val downloaderPackageName = downloaderContext.packageName
|
||||||
|
override val dataDir =
|
||||||
|
downloadersDir.resolve(downloaderPackageName).also(File::mkdirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloaderPackageState.Loaded(
|
DownloaderPackageState.Loaded(
|
||||||
@@ -126,11 +136,12 @@ class DownloaderRepository(
|
|||||||
className,
|
className,
|
||||||
downloaderContext.getString(downloader.name),
|
downloaderContext.getString(downloader.name),
|
||||||
packageInfo.versionName!!,
|
packageInfo.versionName!!,
|
||||||
downloader.get,
|
scopeImpl,
|
||||||
downloader.download
|
downloader
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
classLoader,
|
classLoader,
|
||||||
|
downloaderContext,
|
||||||
with(pm) { packageInfo.label() }
|
with(pm) { packageInfo.label() }
|
||||||
)
|
)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package app.revanced.manager.network.downloader
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
sealed interface DownloaderPackageState {
|
sealed interface DownloaderPackageState {
|
||||||
data object Untrusted : DownloaderPackageState
|
data object Untrusted : DownloaderPackageState
|
||||||
|
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val downloaders: List<LoadedDownloader>,
|
val downloaders: List<LoadedDownloader>,
|
||||||
val classLoader: ClassLoader,
|
val classLoader: ClassLoader,
|
||||||
|
val context: Context,
|
||||||
val name: String
|
val name: String
|
||||||
) : DownloaderPackageState
|
) : DownloaderPackageState
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package app.revanced.manager.network.downloader
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.downloader.OutputDownloadScope
|
import app.revanced.manager.downloader.Downloader
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.downloader.Scope
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
class LoadedDownloader(
|
class LoadedDownloader(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val className: String,
|
val className: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
|
val scopeImpl: Scope,
|
||||||
val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit
|
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.patcher.toRemoteError
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.downloader.GetScope
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.downloader.DownloaderHostApi
|
||||||
|
import app.revanced.manager.downloader.Scope
|
||||||
import app.revanced.manager.downloader.UserInteractionException
|
import app.revanced.manager.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
@@ -185,11 +186,7 @@ class PatcherWorker(
|
|||||||
downloaderRepository.loadedDownloadersFlow.first()
|
downloaderRepository.loadedDownloadersFlow.first()
|
||||||
.firstNotNullOfOrNull { downloader ->
|
.firstNotNullOfOrNull { downloader ->
|
||||||
try {
|
try {
|
||||||
val getScope = object : GetScope {
|
val getScope = object : GetScope, Scope by downloader.scopeImpl {
|
||||||
override val downloaderPackageName = downloader.packageName
|
|
||||||
override val hostPackageName =
|
|
||||||
applicationContext.packageName
|
|
||||||
|
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
val result =
|
val result =
|
||||||
args.handleStartActivityRequest(downloader, intent)
|
args.handleStartActivityRequest(downloader, intent)
|
||||||
@@ -204,7 +201,7 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
downloader.get(
|
downloader.impl.get(
|
||||||
getScope,
|
getScope,
|
||||||
selectedApp.packageName,
|
selectedApp.packageName,
|
||||||
selectedApp.version
|
selectedApp.version
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
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.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
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.ProgressEvent
|
||||||
import app.revanced.manager.patcher.StepId
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.LogLevel
|
import app.revanced.manager.patcher.logger.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
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.InstallerModel
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
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.Step
|
||||||
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.withState
|
import app.revanced.manager.ui.model.withState
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
@@ -80,6 +79,7 @@ import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
||||||
class PatcherViewModel(
|
class PatcherViewModel(
|
||||||
@@ -404,24 +404,26 @@ class PatcherViewModel(
|
|||||||
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||||
?: throw Exception("Failed to load application info")
|
?: 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) {
|
when (installType) {
|
||||||
InstallType.DEFAULT -> {
|
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
|
// Check if the app is mounted as root
|
||||||
// If it is, unmount it first, silently
|
// If it is, unmount it first, silently
|
||||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||||
@@ -437,17 +439,6 @@ class PatcherViewModel(
|
|||||||
currentPackageInfo.label()
|
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
|
val inputVersion = input.selectedApp.version
|
||||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||||
?: throw Exception("Failed to determine input APK version")
|
?: 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.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.downloader.GetScope
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.downloader.DownloaderHostApi
|
||||||
|
import app.revanced.manager.downloader.Scope
|
||||||
import app.revanced.manager.downloader.UserInteractionException
|
import app.revanced.manager.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
@@ -192,9 +193,7 @@ class SelectedAppInfoViewModel(
|
|||||||
cancelDownloaderAction()
|
cancelDownloaderAction()
|
||||||
downloaderAction = downloader to viewModelScope.launch {
|
downloaderAction = downloader to viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val scope = object : GetScope {
|
val scope = object : GetScope, Scope by downloader.scopeImpl {
|
||||||
override val hostPackageName = app.packageName
|
|
||||||
override val downloaderPackageName = downloader.packageName
|
|
||||||
override suspend fun requestStartActivity(intent: Intent) =
|
override suspend fun requestStartActivity(intent: Intent) =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (launchedActivity != null) error("Previous activity has not finished")
|
if (launchedActivity != null) error("Previous activity has not finished")
|
||||||
@@ -219,7 +218,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
downloader.get(scope, packageName, desiredVersion)
|
downloader.impl.get(scope, packageName, desiredVersion)
|
||||||
}?.let { (data, version) ->
|
}?.let { (data, version) ->
|
||||||
if (desiredVersion != null && version != desiredVersion) {
|
if (desiredVersion != null && version != desiredVersion) {
|
||||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
app.toast(app.getString(R.string.downloader_invalid_version))
|
||||||
|
|||||||
4
app/src/main/res/values/id.xml
Normal file
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="export">Export</string>
|
||||||
<string name="confirm">Confirm</string>
|
<string name="confirm">Confirm</string>
|
||||||
<string name="new_announcement">New announcement:\n%s</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>
|
</resources>
|
||||||
|
|||||||
@@ -6,4 +6,9 @@
|
|||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.DownloaderActivity" parent="Theme.AppCompat.DayNight">
|
||||||
|
<item name="android:windowActionBar">true</item>
|
||||||
|
<item name="android:windowNoTitle">false</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ android.useAndroidX=true
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
[versions]
|
[versions]
|
||||||
ktx = "1.17.0"
|
ktx = "1.18.0"
|
||||||
material3 = "1.5.0-alpha14"
|
material3 = "1.5.0-alpha14"
|
||||||
ui-tooling = "1.10.0"
|
ui-tooling = "1.10.0"
|
||||||
viewmodel-lifecycle = "2.10.0"
|
viewmodel-lifecycle = "2.10.0"
|
||||||
splash-screen = "1.2.0"
|
splash-screen = "1.2.0"
|
||||||
activity = "1.12.2"
|
activity = "1.12.2"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
|
fragment = "1.8.9"
|
||||||
preferences-datastore = "1.2.0"
|
preferences-datastore = "1.2.0"
|
||||||
work-runtime = "2.11.0"
|
work-runtime = "2.11.0"
|
||||||
compose-bom = "2025.12.01"
|
compose-bom = "2025.12.01"
|
||||||
@@ -13,15 +14,15 @@ navigation = "2.9.7"
|
|||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
placeholder = "1.0.12"
|
placeholder = "1.0.12"
|
||||||
reorderable = "3.0.0"
|
reorderable = "3.0.0"
|
||||||
serialization = "1.9.0"
|
serialization = "1.10.0"
|
||||||
collection = "0.4.0"
|
collection = "0.4.0"
|
||||||
datetime = "0.7.1"
|
datetime = "0.7.1"
|
||||||
room-version = "2.8.4"
|
room-version = "2.8.4"
|
||||||
revanced-patcher = "22.0.0"
|
revanced-patcher = "22.0.0"
|
||||||
revanced-library = "4.0.0"
|
revanced-library = "4.0.0"
|
||||||
koin = "4.1.1"
|
koin = "4.1.1"
|
||||||
ktor = "3.3.3"
|
ktor = "3.4.1"
|
||||||
markdown-renderer = "0.39.0"
|
markdown-renderer = "0.39.2"
|
||||||
fading-edges = "1.0.4"
|
fading-edges = "1.0.4"
|
||||||
kotlin = "2.3.10"
|
kotlin = "2.3.10"
|
||||||
android-gradle-plugin = "8.13.2"
|
android-gradle-plugin = "8.13.2"
|
||||||
@@ -37,7 +38,7 @@ kotlin-process = "1.5.1"
|
|||||||
hidden-api-stub = "4.4.0"
|
hidden-api-stub = "4.4.0"
|
||||||
binary-compatibility-validator = "0.18.1"
|
binary-compatibility-validator = "0.18.1"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
ackpine = "0.19.1"
|
ackpine = "0.20.6"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# 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" }
|
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" }
|
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
|
||||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
|
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" }
|
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" }
|
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
|
||||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
|||||||
Reference in New Issue
Block a user