Compare commits

...

4 Commits

Author SHA1 Message Date
oSumAtrIX
6d9fd1aa36 feat: Multiple downloader per APK 2026-01-08 23:35:05 +01:00
oSumAtrIX
8d0ee814fc Merge synonym plugin to downloader 2026-01-08 22:00:04 +01:00
semantic-release-bot
25d82e869c chore: Release v1.26.0-dev.17 [skip ci]
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)

### Bug Fixes

* allow updating patches on metered networks ([9d9a0e8](9d9a0e81db))
2026-01-06 21:45:09 +00:00
Ax333l
9d9a0e81db fix: allow updating patches on metered networks 2026-01-06 22:37:25 +01:00
50 changed files with 647 additions and 609 deletions

View File

@@ -73,7 +73,7 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
Some of the features ReVanced Manager provides are:
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader system
- 💉 **Patch**: Select and apply patches to any Android app
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings

View File

@@ -1,19 +1,19 @@
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
public abstract interface class app/revanced/manager/downloader/BaseDownloadScope : app/revanced/manager/downloader/Scope {
}
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
public final class app/revanced/manager/downloader/ConstantsKt {
public static final field DOWNLOADER_HOST_PERMISSION Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
public final class app/revanced/manager/downloader/DownloadUrl : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/downloader/DownloadUrl;
public static synthetic fun copy$default (Lapp/revanced/manager/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/downloader/DownloadUrl;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getHeaders ()Ljava/util/Map;
@@ -24,58 +24,61 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
public final class app/revanced/manager/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/DownloadUrl;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun newArray (I)[Lapp/revanced/manager/downloader/DownloadUrl;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Downloader {
public final class app/revanced/manager/downloader/Downloader {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
public final class app/revanced/manager/downloader/DownloaderBuilder {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
public abstract interface annotation class app/revanced/manager/downloader/DownloaderHostApi : java/lang/annotation/Annotation {
}
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
public final class app/revanced/manager/downloader/DownloaderKt {
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/downloader/DownloaderBuilder;
}
public final class app/revanced/manager/downloader/DownloaderScope : app/revanced/manager/downloader/Scope {
public static final field $stable I
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
public fun getDownloaderPackageName ()Ljava/lang/String;
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
public final class app/revanced/manager/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
}
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
public abstract interface class app/revanced/manager/downloader/GetScope : app/revanced/manager/downloader/Scope {
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
public abstract interface class app/revanced/manager/downloader/InputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
}
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
public abstract interface class app/revanced/manager/downloader/OutputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
public final class app/revanced/manager/downloader/Package : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/downloader/Package;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getName ()Ljava/lang/String;
@@ -85,98 +88,95 @@ public final class app/revanced/manager/plugin/downloader/Package : android/os/P
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
public final class app/revanced/manager/downloader/Package$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/Package;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
public final fun newArray (I)[Lapp/revanced/manager/downloader/Package;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
}
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
public abstract interface class app/revanced/manager/downloader/Scope {
public abstract fun getDownloaderPackageName ()Ljava/lang/String;
public abstract fun getHostPackageName ()Ljava/lang/String;
public abstract fun getPluginPackageName ()Ljava/lang/String;
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public abstract class app/revanced/manager/downloader/UserInteractionException : java/lang/Exception {
public static final field $stable I
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
public abstract class app/revanced/manager/downloader/UserInteractionException$Activity : app/revanced/manager/downloader/UserInteractionException {
public static final field $stable I
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public final class app/revanced/manager/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/downloader/UserInteractionException$Activity {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public final class app/revanced/manager/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/downloader/UserInteractionException$Activity {
public static final field $stable I
public final fun getIntent ()Landroid/content/Intent;
public final fun getResultCode ()I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
public final class app/revanced/manager/downloader/UserInteractionException$RequestDenied : app/revanced/manager/downloader/UserInteractionException {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final class app/revanced/manager/downloader/webview/APIKt {
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/downloader/DownloaderBuilder;
public static final fun runWebView (Lapp/revanced/manager/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
public class app/revanced/manager/downloader/webview/IWebView$Default : app/revanced/manager/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun finish ()V
public fun load (Ljava/lang/String;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
public abstract class app/revanced/manager/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebView;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public class app/revanced/manager/downloader/webview/IWebViewEvents$Default : app/revanced/manager/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun pageLoad (Ljava/lang/String;)V
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
public fun ready (Lapp/revanced/manager/downloader/webview/IWebView;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public abstract class app/revanced/manager/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebViewEvents;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
public final class app/revanced/manager/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public final fun newArray (I)[Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
public abstract interface class app/revanced/manager/downloader/webview/WebViewCallbackScope : app/revanced/manager/downloader/Scope {
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
public final class app/revanced/manager/downloader/webview/WebViewScope : app/revanced/manager/downloader/Scope {
public static final field $stable I
public final fun download (Lkotlin/jvm/functions/Function5;)V
public fun getDownloaderPackageName ()Ljava/lang/String;
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
}

View File

@@ -18,7 +18,7 @@ dependencies {
}
android {
namespace = "app.revanced.manager.plugin.downloader"
namespace = "app.revanced.manager.downloader"
compileSdk = 35
defaultConfig {
@@ -52,7 +52,7 @@ android {
}
apiValidation {
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
nonPublicMarkers += "app.revanced.manager.downloader.DownloaderHostApi"
}
publishing {

View File

@@ -0,0 +1,8 @@
// IWebView.aidl
package app.revanced.manager.downloader.webview;
@JavaPassthrough(annotation="@app.revanced.manager.downloader.DownloaderHostApi")
oneway interface IWebView {
void load(String url);
void finish();
}

View File

@@ -0,0 +1,11 @@
// IWebViewEvents.aidl
package app.revanced.manager.downloader.webview;
import app.revanced.manager.downloader.webview.IWebView;
@JavaPassthrough(annotation="@app.revanced.manager.downloader.DownloaderHostApi")
oneway interface IWebViewEvents {
void ready(IWebView iface);
void pageLoad(String url);
void download(String url, String mimetype, String userAgent);
}

View File

@@ -1,8 +0,0 @@
// IWebView.aidl
package app.revanced.manager.plugin.downloader.webview;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebView {
void load(String url);
void finish();
}

View File

@@ -1,11 +0,0 @@
// IWebViewEvents.aidl
package app.revanced.manager.plugin.downloader.webview;
import app.revanced.manager.plugin.downloader.webview.IWebView;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebViewEvents {
void ready(IWebView iface);
void pageLoad(String url);
void download(String url, String mimetype, String userAgent);
}

View File

@@ -0,0 +1,7 @@
package app.revanced.manager.downloader
/**
* The permission ID of the special downloader host permission. Only ReVanced Manager will have this permission.
* Downloader UI activities and internal services can be protected using this permission.
*/
const val DOWNLOADER_HOST_PERMISSION = "app.revanced.manager.permission.DOWNLOADER_HOST"

View File

@@ -1,4 +1,4 @@
package app.revanced.manager.plugin.downloader
package app.revanced.manager.downloader
import android.content.ComponentName
import android.content.Context
@@ -15,10 +15,10 @@ import kotlin.coroutines.suspendCoroutine
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
message = "This API is only intended for downloader hosts, don't use it in a downloader.",
)
@Retention(AnnotationRetention.BINARY)
annotation class PluginHostApi
annotation class DownloaderHostApi
/**
* The base interface for all DSL scopes.
@@ -30,9 +30,9 @@ interface Scope {
val hostPackageName: String
/**
* The package name of the plugin.
* The package name of the downloader.
*/
val pluginPackageName: String
val downloaderPackageName: String
}
/**
@@ -43,7 +43,7 @@ interface GetScope : Scope {
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
*
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
* @throws UserInteractionException.RequestDenied User decided to skip this downloader.
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
@@ -67,14 +67,14 @@ class DownloaderScope<T : Parcelable> internal constructor(
private val scopeImpl: Scope,
internal val context: Context
) : Scope by scopeImpl {
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
// Returning an InputStream is the primary way for downloader to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to downloader.
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
/**
* Define the download block of the plugin.
* Define the download block of the downloader.
*/
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
download = { app, outputStream ->
@@ -88,7 +88,7 @@ class DownloaderScope<T : Parcelable> internal constructor(
}
/**
* Define the get block of the plugin.
* Define the get block of the downloader.
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
*/
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
@@ -123,7 +123,7 @@ class DownloaderScope<T : Parcelable> internal constructor(
}
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi
@DownloaderHostApi
fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(scopeImpl, context)) {
block()
@@ -136,12 +136,12 @@ class DownloaderBuilder<T : Parcelable> internal constructor(private val block:
}
class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
@property:DownloaderHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:DownloaderHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
)
/**
* Define a downloader plugin.
* Define a downloader.
*/
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
@@ -149,17 +149,17 @@ fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = Download
* @see GetScope.requestStartActivity
*/
sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied @PluginHostApi constructor() :
class RequestDenied @DownloaderHostApi constructor() :
UserInteractionException("Request denied by user")
sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
class Cancelled @DownloaderHostApi constructor() : Activity("Interaction cancelled")
/**
* @param resultCode The result code of the activity.
* @param intent The [Intent] of the activity.
*/
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
class NotCompleted @DownloaderHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode")
}
}

View File

@@ -1,4 +1,4 @@
package app.revanced.manager.plugin.downloader
package app.revanced.manager.downloader
import android.app.Activity
import android.app.Service
@@ -28,7 +28,7 @@ fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadSc
*/
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
requestStartActivity(
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
Intent().apply { setClassName(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
)
/**
@@ -38,5 +38,5 @@ suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity()
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
noinline block: suspend (IBinder) -> R
) = useService(
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
Intent().apply { setClassName(downloaderPackageName, SERVICE::class.qualifiedName!!) }, block
)

View File

@@ -1,4 +1,4 @@
package app.revanced.manager.plugin.downloader
package app.revanced.manager.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -7,7 +7,7 @@ import java.net.URI
/**
* A simple parcelable data class for storing a package name and version.
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
* This can be used as the data type for downloader that only need a name and version to implement their [DownloaderScope.download] function.
*
* @param name The package name.
* @param version The version.

View File

@@ -1,12 +1,12 @@
package app.revanced.manager.plugin.downloader.webview
package app.revanced.manager.downloader.webview
import android.content.Intent
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.DownloaderScope
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.downloader.DownloadUrl
import app.revanced.manager.downloader.DownloaderScope
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -32,7 +32,7 @@ interface WebViewCallbackScope<T> : Scope {
suspend fun load(url: String)
}
@OptIn(PluginHostApi::class)
@OptIn(DownloaderHostApi::class)
class WebViewScope<T> internal constructor(
coroutineScope: CoroutineScope,
private val scopeImpl: Scope,
@@ -110,7 +110,7 @@ private value class Container<U>(val value: U)
* @param title The string displayed in the action bar.
* @param block The control block.
*/
@OptIn(PluginHostApi::class)
@OptIn(DownloaderHostApi::class)
suspend fun <T> GetScope.runWebView(
title: String,
block: suspend WebViewScope<T>.() -> InitialUrl

View File

@@ -1,4 +1,4 @@
package app.revanced.manager.plugin.downloader.webview
package app.revanced.manager.downloader.webview
import android.annotation.SuppressLint
import android.os.Bundle
@@ -20,15 +20,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.R
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.R
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@OptIn(PluginHostApi::class)
@PluginHostApi
@OptIn(DownloaderHostApi::class)
@DownloaderHostApi
class WebViewActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -110,7 +110,7 @@ class WebViewActivity : ComponentActivity() {
}
}
@OptIn(PluginHostApi::class)
@OptIn(DownloaderHostApi::class)
internal class WebViewModel : ViewModel() {
init {
CookieManager.getInstance().apply {

View File

@@ -1,7 +0,0 @@
package app.revanced.manager.plugin.downloader
/**
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
* Plugin UI activities and internal services can be protected using this permission.
*/
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"

View File

@@ -1,3 +1,10 @@
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
### Bug Fixes
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)

View File

@@ -1 +1 @@
version = 1.26.0-dev.16
version = 1.26.0-dev.17

View File

@@ -1,7 +1,7 @@
-dontobfuscate
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
-keep class app.revanced.manager.plugin.** { *; }
-keep class app.revanced.manager.downloader.** { *; }
-keep class app.revanced.patcher.** { *; }
-keep class com.android.tools.smali.** { *; }
-keep class kotlin.** { *; }

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d0119047505da435972c5247181de675",
"identityHash": "9a937123afc782978d185d00c35d20e0",
"entities": [
{
"tableName": "patch_bundles",
@@ -21,10 +21,9 @@
"notNull": true
},
{
"fieldPath": "version",
"fieldPath": "versionHash",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "source",
@@ -44,9 +43,7 @@
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"tableName": "patch_selections",
@@ -127,7 +124,6 @@
"patch_name"
]
},
"indices": [],
"foreignKeys": [
{
"table": "patch_selections",
@@ -177,9 +173,7 @@
"package_name",
"version"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"tableName": "installed_app",
@@ -215,9 +209,7 @@
"columnNames": [
"current_package_name"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"tableName": "applied_patch",
@@ -378,7 +370,6 @@
"key"
]
},
"indices": [],
"foreignKeys": [
{
"table": "option_groups",
@@ -394,7 +385,7 @@
]
},
{
"tableName": "trusted_downloader_plugins",
"tableName": "trusted_downloader",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
"fields": [
{
@@ -415,15 +406,12 @@
"columnNames": [
"package_name"
]
},
"indices": [],
"foreignKeys": []
}
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a937123afc782978d185d00c35d20e0')"
]
}
}

View File

@@ -3,13 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">
<permission
android:name="app.revanced.manager.permission.PLUGIN_HOST"
android:name="app.revanced.manager.permission.DOWNLOADER_HOST"
android:protectionLevel="signature"
android:label="@string/plugin_host_permission_label"
android:description="@string/plugin_host_permission_description"
android:label="@string/downloader_host_permission_label"
android:description="@string/downloader_host_permission_description"
/>
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
<uses-permission android:name="app.revanced.manager.permission.DOWNLOADER_HOST" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -49,7 +49,7 @@
</intent-filter>
</activity>
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -129,7 +129,7 @@ private fun ReVancedManager(vm: MainViewModel) {
onUpdateClick = {
navController.navigate(Update())
},
onDownloaderPluginClick = {
onDownloaderClick = {
navController.navigate(Settings.Downloads)
},
onAppClick = { packageName ->

View File

@@ -7,7 +7,7 @@ import android.util.Log
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
@@ -29,7 +29,7 @@ class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val downloaderRepository: DownloaderRepository by inject()
private val fs: Filesystem by inject()
override fun onCreate() {
@@ -70,7 +70,7 @@ class ManagerApplication : Application() {
prefs.preload()
}
scope.launch(Dispatchers.Default) {
downloaderPluginRepository.reload()
downloaderRepository.reload()
}
scope.launch(Dispatchers.Default) {
with(patchBundleRepository) {

View File

@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
/**
* Returns true if it is safe to download large files.
*/
fun isSafe() = isConnected() && isUnmetered()
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
}

View File

@@ -16,12 +16,12 @@ import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.options.Option
import app.revanced.manager.data.room.options.OptionDao
import app.revanced.manager.data.room.options.OptionGroup
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
import app.revanced.manager.data.room.downloader.TrustedDownloader
import app.revanced.manager.data.room.downloader.TrustedDownloaderDao
import kotlin.random.Random
@Database(
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloader::class],
version = 1
)
@TypeConverters(Converters::class)
@@ -31,7 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
abstract fun optionDao(): OptionDao
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
abstract fun trustedDownloaderDao(): TrustedDownloaderDao
companion object {
fun generateUid() = Random.Default.nextInt()

View File

@@ -1,11 +1,11 @@
package app.revanced.manager.data.room.plugins
package app.revanced.manager.data.room.downloader
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "trusted_downloader_plugins")
class TrustedDownloaderPlugin(
@Entity(tableName = "trusted_downloader")
class TrustedDownloader(
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "signature") val signature: ByteArray
)

View File

@@ -0,0 +1,22 @@
package app.revanced.manager.data.room.downloader
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
@Dao
interface TrustedDownloaderDao {
@Query("SELECT signature FROM trusted_downloader WHERE package_name = :packageName")
suspend fun getTrustedSignature(packageName: String): ByteArray?
@Upsert
suspend fun upsertTrust(downloader: TrustedDownloader)
@Query("DELETE FROM trusted_downloader WHERE package_name = :packageName")
suspend fun remove(packageName: String)
@Transaction
@Query("DELETE FROM trusted_downloader WHERE package_name IN (:packageNames)")
suspend fun removeAll(packageNames: Set<String>)
}

View File

@@ -1,22 +0,0 @@
package app.revanced.manager.data.room.plugins
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
@Dao
interface TrustedDownloaderPluginDao {
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
suspend fun getTrustedSignature(packageName: String): ByteArray?
@Upsert
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
suspend fun remove(packageName: String)
@Transaction
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
suspend fun removeAll(packageNames: Set<String>)
}

View File

@@ -21,7 +21,7 @@ val repositoryModule = module {
// It is best to load patch bundles ASAP
createdAtStart()
}
singleOf(::DownloaderPluginRepository)
singleOf(::DownloaderRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)

View File

@@ -31,7 +31,9 @@ class PreferencesManager(
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
val acknowledgedDownloader = stringSetPreference("acknowledged_downloader", emptySet())
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
}

View File

@@ -6,8 +6,8 @@ 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.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.plugin.downloader.OutputDownloadScope
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.downloader.OutputDownloadScope
import app.revanced.manager.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
@@ -36,7 +36,7 @@ class DownloadedAppRepository(
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
suspend fun download(
plugin: LoadedDownloaderPlugin,
downloader: LoadedDownloader,
data: Parcelable,
expectedPackageName: String,
expectedVersion: String?,
@@ -55,7 +55,7 @@ class DownloadedAppRepository(
channelFlow {
val scope = object : OutputDownloadScope {
override val pluginPackageName = plugin.packageName
override val downloaderPackageName = downloader.packageName
override val hostPackageName = app.packageName
override suspend fun reportSize(size: Long) {
require(size > 0) { "Size must be greater than zero" }
@@ -87,7 +87,7 @@ class DownloadedAppRepository(
)
}
}
plugin.download(scope, data, stream)
downloader.download(scope, data, stream)
}
}
.conflate()

View File

@@ -1,168 +0,0 @@
package app.revanced.manager.domain.repository
import android.app.Application
import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.downloader.DownloaderPluginState
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.plugin.downloader.DownloaderBuilder
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.util.PM
import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.lang.reflect.Modifier
@OptIn(PluginHostApi::class)
class DownloaderPluginRepository(
private val pm: PM,
private val prefs: PreferencesManager,
private val app: Application,
db: AppDatabase
) {
private val trustDao = db.trustedDownloaderPluginDao()
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
val pluginStates = _pluginStates.asStateFlow()
val loadedPluginsFlow = pluginStates.map { states ->
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
}
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
val newPluginPackageNames = combine(
installedPluginPackageNames,
acknowledgedDownloaderPlugins.flow
) { installed, acknowledged ->
installed subtract acknowledged
}
suspend fun reload() {
val plugins =
withContext(Dispatchers.IO) {
pm.getPackagesWithFeature(PLUGIN_FEATURE)
.associate { it.packageName to loadPlugin(it.packageName) }
}
_pluginStates.value = plugins
installedPluginPackageNames.value = plugins.keys
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
if (uninstalledPlugins.isNotEmpty()) {
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
trustDao.removeAll(uninstalledPlugins)
}
}
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
val plugin =
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
return plugin to data.unwrapWith(plugin)
}
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
try {
if (!verify(packageName)) return DownloaderPluginState.Untrusted
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(tag, "Got exception while verifying plugin $packageName", e)
return DownloaderPluginState.Failed(e)
}
return try {
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
val classLoader =
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
val pluginContext = app.createPackageContext(packageName, 0)
val downloader = classLoader
.loadClass(className)
.getDownloaderBuilder()
.build(
scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val pluginPackageName = pluginContext.packageName
},
context = pluginContext
)
DownloaderPluginState.Loaded(
LoadedDownloaderPlugin(
packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName!!,
downloader.get,
downloader.download,
classLoader
)
)
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e(tag, "Failed to load plugin $packageName", t)
DownloaderPluginState.Failed(t)
}
}
suspend fun trustPackage(packageName: String) {
trustDao.upsertTrust(
TrustedDownloaderPlugin(
packageName,
pm.getSignature(packageName).toByteArray()
)
)
reload()
prefs.edit {
acknowledgedDownloaderPlugins += packageName
}
}
suspend fun revokeTrustForPackage(packageName: String) =
trustDao.remove(packageName).also { reload() }
suspend fun acknowledgeAllNewPlugins() =
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
private suspend fun verify(packageName: String): Boolean {
val expectedSignature =
trustDao.getTrustedSignature(packageName) ?: return false
return pm.hasSignature(packageName, expectedSignature)
}
private companion object {
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
@Suppress("UNCHECKED_CAST")
fun Class<*>.getDownloaderBuilder() =
declaredMethods
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
?.let { it(null) as DownloaderBuilder<Parcelable> }
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
}
}

View File

@@ -0,0 +1,173 @@
package app.revanced.manager.domain.repository
import android.app.Application
import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.downloader.TrustedDownloader
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.downloader.DownloaderPackageState
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.downloader.DownloaderBuilder
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.Scope
import app.revanced.manager.util.PM
import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.lang.reflect.Modifier
@OptIn(DownloaderHostApi::class)
class DownloaderRepository(
private val pm: PM,
private val prefs: PreferencesManager,
private val app: Application,
db: AppDatabase
) {
private val trustDao = db.trustedDownloaderDao()
private val _downloaderPackageStates = MutableStateFlow(emptyMap<String, DownloaderPackageState>())
val downloaderPackageStates = _downloaderPackageStates.asStateFlow()
val loadedDownloaderPackageFlow = downloaderPackageStates.map { states ->
states.values.filterIsInstance<DownloaderPackageState.Loaded>().flatMap { it.downloader }
}
private val acknowledgedPackageDownloader = prefs.acknowledgedDownloader
private val installedDownloaderPackageNames = MutableStateFlow(emptySet<String>())
val newDownloaderPackageNames = combine(
installedDownloaderPackageNames,
acknowledgedPackageDownloader.flow
) { installed, acknowledged ->
installed subtract acknowledged
}
suspend fun reload() {
val downloaderPackages =
withContext(Dispatchers.IO) {
pm.getPackagesWithFeature(DOWNLOADER_FEATURE)
.associate { it.packageName to loadDownloader(it.packageName) }
}
_downloaderPackageStates.value = downloaderPackages
installedDownloaderPackageNames.value = downloaderPackages.keys
val acknowledgedDownloader = this@DownloaderRepository.acknowledgedPackageDownloader.get()
val uninstalledDownloader = acknowledgedDownloader subtract installedDownloaderPackageNames.value
if (uninstalledDownloader.isNotEmpty()) {
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
this@DownloaderRepository.acknowledgedPackageDownloader.update(acknowledgedDownloader subtract uninstalledDownloader)
trustDao.removeAll(uninstalledDownloader)
}
}
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloader, Parcelable> {
val downloader =
(_downloaderPackageStates.value[data.downloaderPackageName] as? DownloaderPackageState.Loaded)
?.downloader?.first { it.name == data.downloaderName }
?: throw Exception("Downloader package name ${data.downloaderPackageName} is not available")
return downloader to data.unwrapWith(downloader)
}
private suspend fun loadDownloader(packageName: String): DownloaderPackageState {
try {
if (!verify(packageName)) return DownloaderPackageState.Untrusted
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(tag, "Got exception while verifying downloader $packageName", e)
return DownloaderPackageState.Failed(e)
}
return try {
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
val classNames = packageInfo.applicationInfo!!.metaData.getStringArray(METADATA_DOWNLOADER_CLASSES)
?: throw Exception("Missing metadata attribute $METADATA_DOWNLOADER_CLASSES")
val classLoader =
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
val downloaderContext = app.createPackageContext(packageName, 0)
val scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloaderContext.packageName
}
DownloaderPackageState.Loaded(
classNames.map { className ->
val downloader = classLoader
.loadClass(className)
.getDownloaderBuilder()
.build(
scopeImpl = scopeImpl,
context = downloaderContext
)
LoadedDownloader(
packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName!!,
downloader.get,
downloader.download,
classLoader
)
}
)
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e(tag, "Failed to load downloader $packageName", t)
DownloaderPackageState.Failed(t)
}
}
suspend fun trustPackage(packageName: String) {
trustDao.upsertTrust(
TrustedDownloader(
packageName,
pm.getSignature(packageName).toByteArray()
)
)
reload()
prefs.edit {
acknowledgedPackageDownloader += packageName
}
}
suspend fun revokeTrustForPackage(packageName: String) =
trustDao.remove(packageName).also { reload() }
suspend fun acknowledgeAllNewDownloader() =
acknowledgedPackageDownloader.update(installedDownloaderPackageNames.value)
private suspend fun verify(packageName: String): Boolean {
val expectedSignature =
trustDao.getTrustedSignature(packageName) ?: return false
return pm.hasSignature(packageName, expectedSignature)
}
private companion object {
const val DOWNLOADER_FEATURE = "app.revanced.manager.downloader"
const val METADATA_DOWNLOADER_CLASSES = "app.revanced.manager.downloader.classes"
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
@Suppress("UNCHECKED_CAST")
fun Class<*>.getDownloaderBuilder() =
declaredMethods
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
?.let { it(null) as DownloaderBuilder<Parcelable> }
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
}
}

View File

@@ -286,28 +286,29 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap())
}
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
try {
createStream().use { patches -> replace(patches) }
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(tag, "Got exception while importing bundle", e)
withContext(Dispatchers.Main) {
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
suspend fun createLocal(createStream: suspend () -> InputStream) =
dispatchAction("Add bundle") {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
try {
createStream().use { patches -> replace(patches) }
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(tag, "Got exception while importing bundle", e)
withContext(Dispatchers.Main) {
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
}
deleteLocalFile()
}
deleteLocalFile()
}
}
doReload()
}
doReload()
}
suspend fun createRemote(url: String, autoUpdate: Boolean) =
dispatchAction("Add bundle ($url)") { state ->
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
update(src)
update(src, force = true)
state.copy(sources = state.sources.put(src.uid, src))
}
@@ -329,32 +330,38 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc))
}
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
suspend fun update(
vararg sources: RemotePatchBundle,
showToast: Boolean = false,
force: Boolean = false
) {
val uids = sources.map { it.uid }.toSet()
store.dispatch(Update(showToast = showToast) { it.uid in uids })
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
}
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
/**
* Updates all bundles that should be automatically updated.
*/
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
suspend fun updateCheck() =
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
private inner class Update(
private val force: Boolean = false,
private val redownload: Boolean = false,
private val showToast: Boolean = false,
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
) : Action<State> {
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
override suspend fun ActionContext.execute(
current: State
) = coroutineScope {
if (!networkInfo.isSafe()) {
if (!networkInfo.isSafe(force)) {
Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope current
}
@@ -367,7 +374,7 @@ class PatchBundleRepository(
Log.d(tag, "Updating patch bundle: ${it.name}")
val newVersion = with(it) {
if (force) downloadLatest() else update()
if (redownload) downloadLatest() else update()
} ?: return@async null
it to newVersion

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.network.downloader
sealed interface DownloaderPackageState {
data object Untrusted : DownloaderPackageState
data class Loaded(val downloader: List<LoadedDownloader>) : DownloaderPackageState
data class Failed(val throwable: Throwable) : DownloaderPackageState
}

View File

@@ -1,9 +0,0 @@
package app.revanced.manager.network.downloader
sealed interface DownloaderPluginState {
data object Untrusted : DownloaderPluginState
data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState
data class Failed(val throwable: Throwable) : DownloaderPluginState
}

View File

@@ -1,11 +1,11 @@
package app.revanced.manager.network.downloader
import android.os.Parcelable
import app.revanced.manager.plugin.downloader.OutputDownloadScope
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.downloader.OutputDownloadScope
import app.revanced.manager.downloader.GetScope
import java.io.OutputStream
class LoadedDownloaderPlugin(
class LoadedDownloader(
val packageName: String,
val name: String,
val version: String,

View File

@@ -10,20 +10,22 @@ import kotlinx.parcelize.Parcelize
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
*/
class ParceledDownloaderData private constructor(
val pluginPackageName: String,
val downloaderPackageName: String,
val downloaderName: String,
private val bundle: Bundle
) : Parcelable {
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
plugin.packageName,
constructor(downloader: LoadedDownloader, data: Parcelable) : this(
downloader.packageName,
downloader.name,
createBundle(data)
)
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
bundle.classLoader = plugin.classLoader
fun unwrapWith(downloader: LoadedDownloader): Parcelable {
bundle.classLoader = downloader.classLoader
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val className = bundle.getString(CLASS_NAME_KEY)!!
val clazz = plugin.classLoader.loadClass(className)
val clazz = downloader.classLoader.loadClass(className)
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!

View File

@@ -24,11 +24,11 @@ import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger
@@ -36,9 +36,9 @@ import app.revanced.manager.patcher.runStep
import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.patcher.toRemoteError
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
@@ -51,7 +51,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@OptIn(PluginHostApi::class)
@OptIn(DownloaderHostApi::class)
class PatcherWorker(
context: Context,
parameters: WorkerParameters
@@ -59,7 +59,7 @@ class PatcherWorker(
private val workerRepository: WorkerRepository by inject()
private val prefs: PreferencesManager by inject()
private val keystoreManager: KeystoreManager by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val downloaderRepository: DownloaderRepository by inject()
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val pm: PM by inject()
private val fs: Filesystem by inject()
@@ -72,7 +72,7 @@ class PatcherWorker(
val selectedPatches: PatchSelection,
val options: Options,
val logger: Logger,
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val handleStartActivityRequest: suspend (LoadedDownloader, Intent) -> ActivityResult,
val setInputFile: suspend (File) -> Unit,
val onEvent: (ProgressEvent) -> Unit,
) {
@@ -150,9 +150,9 @@ class PatcherWorker(
}
}
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
suspend fun download(downloader: LoadedDownloader, data: Parcelable) =
downloadedAppRepository.download(
plugin,
downloader,
data,
args.packageName,
args.input.version,
@@ -172,27 +172,27 @@ class PatcherWorker(
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
runStep(StepId.DownloadAPK, args.onEvent) {
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
val (downloader, data) = downloaderRepository.unwrapParceledData(
selectedApp.data
)
download(plugin, data)
download(downloader, data)
}
}
is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) {
downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin ->
downloaderRepository.loadedDownloaderPackageFlow.first()
.firstNotNullOfOrNull { downloader ->
try {
val getScope = object : GetScope {
override val pluginPackageName = plugin.packageName
override val downloaderPackageName = downloader.packageName
override val hostPackageName =
applicationContext.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? {
val result =
args.handleStartActivityRequest(plugin, intent)
args.handleStartActivityRequest(downloader, intent)
return when (result.resultCode) {
Activity.RESULT_OK -> result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
@@ -204,7 +204,7 @@ class PatcherWorker(
}
}
withContext(Dispatchers.IO) {
plugin.get(
downloader.get(
getScope,
selectedApp.packageName,
selectedApp.version
@@ -214,7 +214,7 @@ class PatcherWorker(
throw e
} catch (_: UserInteractionException) {
null
}?.let { (data, _) -> download(plugin, data) }
}?.let { (data, _) -> download(downloader, data) }
} ?: throw Exception("App is not available.")
}
}
@@ -250,11 +250,21 @@ class PatcherWorker(
tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
)
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure()
} catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e)
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure()
} finally {
patchedApk.delete()

View File

@@ -90,13 +90,13 @@ fun DashboardScreen(
onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit,
onDownloaderPluginClick: () -> Unit,
onDownloaderClick: () -> Unit,
onAppClick: (String) -> Unit
) {
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
val showNewDownloaderNotification by vm.newDownloaderAvailable.collectAsStateWithLifecycle(
false
)
val androidContext = LocalContext.current
@@ -307,14 +307,14 @@ fun DashboardScreen(
)
}
} else null,
if (showNewDownloaderPluginsNotification) {
if (showNewDownloaderNotification) {
{
NotificationCard(
text = stringResource(R.string.new_downloader_plugins_notification),
text = stringResource(R.string.new_downloader_notification),
icon = Icons.Outlined.Download,
modifier = Modifier.clickable(onClick = onDownloaderPluginClick),
modifier = Modifier.clickable(onClick = onDownloaderClick),
actions = {
TextButton(onClick = vm::ignoreNewDownloaderPlugins) {
TextButton(onClick = vm::ignoreNewDownloader) {
Text(stringResource(R.string.dismiss))
}
}

View File

@@ -148,7 +148,7 @@ fun PatcherScreen(
},
title = { Text(title) },
text = {
Text(stringResource(R.string.plugin_activity_dialog_body))
Text(stringResource(R.string.downloader_activity_dialog_body))
}
)
}

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.ui.screen
import android.R.attr.name
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
@@ -39,7 +40,7 @@ import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar
@@ -86,7 +87,7 @@ fun SelectedAppInfoScreen(
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = vm::handlePluginActivityResult
onResult = vm::handleDownloaderActivityResult
)
EventEffect(flow = vm.launchActivityFlow) { intent ->
launcher.launch(intent)
@@ -140,22 +141,22 @@ fun SelectedAppInfoScreen(
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
val downloader by vm.downloader.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
downloader = downloader,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
activeSearchJob = vm.activeDownloaderAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
onSelectDownloader = vm::searchUsingDownloader,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
@@ -201,8 +202,8 @@ fun SelectedAppInfoScreen(
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
plugins.find { it.packageName == app.data.pluginPackageName }?.name
?: app.data.pluginPackageName
downloader.find { it.packageName == app.data.downloaderPackageName && it.name == app.data.downloaderName }?.let { "${it.packageName} ${it.name}" }
?: app.data.downloaderPackageName
)
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
@@ -279,14 +280,14 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
@Composable
private fun AppSourceSelectorDialog(
plugins: List<LoadedDownloaderPlugin>,
downloader: List<LoadedDownloader>,
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
searchApp: SelectedApp.Search,
activeSearchJob: String?,
hasRoot: Boolean,
requiredVersion: String?,
onDismissRequest: () -> Unit,
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
onSelectDownloader: (LoadedDownloader) -> Unit,
onSelect: (SelectedApp) -> Unit,
) {
val canSelect = activeSearchJob == null
@@ -303,15 +304,15 @@ private fun AppSourceSelectorDialog(
text = {
LazyColumn {
item(key = "auto") {
val hasPlugins = plugins.isNotEmpty()
val hasDownloader = downloader.isNotEmpty()
ListItem(
modifier = Modifier
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
.enabled(hasPlugins),
.clickable(enabled = canSelect && hasDownloader) { onSelect(searchApp) }
.enabled(hasDownloader),
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
supportingContent = {
Text(
if (hasPlugins)
if (hasDownloader)
stringResource(R.string.app_source_dialog_option_auto_description)
else
stringResource(R.string.app_source_dialog_option_auto_unavailable)
@@ -349,11 +350,11 @@ private fun AppSourceSelectorDialog(
}
}
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
items(downloader, key = { "downloader_${it.packageName}" }) { downloader ->
ListItem(
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
headlineContent = { Text(plugin.name) },
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
modifier = Modifier.clickable(enabled = canSelect) { onSelectDownloader(downloader) },
headlineContent = { Text(downloader.name) },
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == downloader.packageName },
colors = transparentListItemColors
)
}

View File

@@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.network.downloader.DownloaderPluginState
import app.revanced.manager.network.downloader.DownloaderPackageState
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ConfirmDialog
@@ -60,7 +60,7 @@ fun DownloadsSettingsScreen(
) {
val context = LocalContext.current
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
val downloaderStates by viewModel.downloaderStates.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
@@ -68,8 +68,8 @@ fun DownloadsSettingsScreen(
ConfirmDialog(
onDismiss = { showDeleteConfirmationDialog = false },
onConfirm = { viewModel.deleteApps() },
title = stringResource(R.string.downloader_plugin_delete_apps_title),
description = stringResource(R.string.downloader_plugin_delete_apps_description),
title = stringResource(R.string.downloader_delete_apps_title),
description = stringResource(R.string.downloader_delete_apps_description),
icon = Icons.Outlined.Delete
)
}
@@ -92,111 +92,118 @@ fun DownloadsSettingsScreen(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
PullToRefreshBox(
onRefresh = viewModel::refreshPlugins,
isRefreshing = viewModel.isRefreshingPlugins,
onRefresh = viewModel::refreshDownloader,
isRefreshing = viewModel.isRefreshingDownloader,
modifier = Modifier.padding(paddingValues)
) {
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize()
) {
item {
GroupHeader(stringResource(R.string.downloader_plugins))
GroupHeader(stringResource(R.string.downloader))
}
pluginStates.forEach { (packageName, state) ->
item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
fun dismiss() {
showDialog = false
}
if (downloaderStates.isNotEmpty()) {
downloaderStates.forEach { (packageName, state) ->
item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
val packageInfo =
remember(packageName) {
viewModel.pm.getPackageInfo(
packageName
)
} ?: return@item
fun dismiss() {
showDialog = false
}
if (showDialog) {
val signature =
val packageInfo =
remember(packageName) {
val androidSignature =
viewModel.pm.getSignature(packageName)
val hash = MessageDigest.getInstance("SHA-256")
.digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase)
viewModel.pm.getPackageInfo(
packageName
)
} ?: return@item
if (showDialog) {
val signature =
remember(packageName) {
val androidSignature =
viewModel.pm.getSignature(packageName)
val hash = MessageDigest.getInstance("SHA-256")
.digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase)
}
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
when (state) {
is DownloaderPluginState.Loaded -> TrustDialog(
title = R.string.downloader_plugin_revoke_trust_dialog_title,
body = stringResource(
R.string.downloader_plugin_trust_dialog_body,
packageName,
signature
),
pluginName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.revokePluginTrust(packageName)
dismiss()
}
)
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
text = remember(state.throwable) {
state.throwable.stackTraceToString()
},
onDismiss = ::dismiss
)
is DownloaderPluginState.Untrusted -> TrustDialog(
title = R.string.downloader_plugin_trust_dialog_title,
body = stringResource(
R.string.downloader_plugin_trust_dialog_body
),
pluginName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustPlugin(packageName)
dismiss()
}
)
}
}
SettingsListItem(
modifier = Modifier.clickable { showDialog = true },
headlineContent = {
AppLabel(
packageInfo = packageInfo,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = stringResource(
when (state) {
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
is DownloaderPackageState.Loaded -> TrustDialog(
title = R.string.downloader_revoke_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body,
packageName,
signature
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.revokeDownloaderTrust(packageName)
dismiss()
}
)
is DownloaderPackageState.Failed -> ExceptionViewerDialog(
text = remember(state.throwable) {
state.throwable.stackTraceToString()
},
onDismiss = ::dismiss
)
is DownloaderPackageState.Untrusted -> TrustDialog(
title = R.string.downloader_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustDownloader(packageName)
dismiss()
}
)
}
),
trailingContent = { Text(packageInfo.versionName!!) }
)
}
SettingsListItem(
modifier = Modifier.clickable { showDialog = true },
headlineContent = {
AppLabel(
packageInfo = packageInfo,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = when (state) {
is DownloaderPackageState.Loaded -> {
val names = state.downloader.joinToString("\n") { it.name }
if (names.isNotEmpty())
stringResource(R.string.downloader_state_trusted, "\n\n$names")
else
stringResource(R.string.downloader_state_trusted)
}
is DownloaderPackageState.Failed -> stringResource(R.string.downloader_state_failed)
is DownloaderPackageState.Untrusted -> stringResource(R.string.downloader_state_untrusted)
},
trailingContent = { Text(packageInfo.versionName!!) }
)
}
}
}
if (pluginStates.isEmpty()) {
} else {
item {
Text(
stringResource(R.string.downloader_no_plugins_installed),
stringResource(R.string.no_downloader_installed),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
@@ -240,7 +247,7 @@ fun DownloadsSettingsScreen(
private fun TrustDialog(
@StringRes title: Int,
body: String,
pluginName: String,
downloaderName: String,
signature: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit
@@ -268,8 +275,8 @@ private fun TrustDialog(
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_plugin,
pluginName
R.string.downloader_trust_dialog_name,
downloaderName
),
)
OutlinedCard(
@@ -279,7 +286,7 @@ private fun TrustDialog(
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_signature,
R.string.downloader_trust_dialog_signature,
signature.chunked(2).joinToString(" ")
), modifier = Modifier.padding(12.dp)
)

View File

@@ -105,6 +105,14 @@ fun GeneralSettingsScreen(
description = R.string.pure_black_theme_description
)
}
GroupHeader(stringResource(R.string.networking))
BooleanItem(
preference = prefs.allowMeteredNetworks,
coroutineScope = coroutineScope,
headline = R.string.allow_metered_networks,
description = R.string.allow_metered_networks_description
)
}
}
}

View File

@@ -54,6 +54,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
patchBundleRepository.update(
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
showToast = true,
force = true
)
}
}
@@ -65,7 +66,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
fun update(src: PatchBundleSource) = viewModelScope.launch {
if (src !is RemotePatchBundle) return@launch
patchBundleRepository.update(src, showToast = true)
patchBundleRepository.update(src, showToast = true, force = true)
}
enum class Event {

View File

@@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Build
import android.os.PowerManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.getSystemService
@@ -15,15 +14,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.PM
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.first
@@ -34,7 +30,7 @@ import kotlinx.coroutines.launch
class DashboardViewModel(
private val app: Application,
private val patchBundleRepository: PatchBundleRepository,
private val downloaderPluginRepository: DownloaderPluginRepository,
private val downloaderRepository: DownloaderRepository,
private val reVancedAPI: ReVancedAPI,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager,
@@ -45,8 +41,8 @@ class DashboardViewModel(
private val contentResolver: ContentResolver = app.contentResolver
private val powerManager = app.getSystemService<PowerManager>()!!
val newDownloaderPluginsAvailable =
downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
val newDownloaderAvailable =
downloaderRepository.newDownloaderPackageNames.map { it.isNotEmpty() }
/**
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
@@ -71,8 +67,8 @@ class DashboardViewModel(
}
}
fun ignoreNewDownloaderPlugins() = viewModelScope.launch {
downloaderPluginRepository.acknowledgeAllNewPlugins()
fun ignoreNewDownloader() = viewModelScope.launch {
downloaderRepository.acknowledgeAllNewDownloader()
}
private suspend fun checkForManagerUpdates() {

View File

@@ -1,6 +1,5 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -8,7 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.util.PM
import app.revanced.manager.util.mutableStateSetOf
import kotlinx.coroutines.Dispatchers
@@ -19,10 +18,10 @@ import kotlinx.coroutines.withContext
class DownloadsViewModel(
private val downloadedAppRepository: DownloadedAppRepository,
private val downloaderPluginRepository: DownloaderPluginRepository,
private val downloaderRepository: DownloaderRepository,
val pm: PM
) : ViewModel() {
val downloaderPluginStates = downloaderPluginRepository.pluginStates
val downloaderStates = downloaderRepository.downloaderPackageStates
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
downloadedApps.sortedWith(
compareBy<DownloadedApp> {
@@ -32,7 +31,7 @@ class DownloadsViewModel(
}
val appSelection = mutableStateSetOf<DownloadedApp>()
var isRefreshingPlugins by mutableStateOf(false)
var isRefreshingDownloader by mutableStateOf(false)
private set
fun toggleApp(downloadedApp: DownloadedApp) {
@@ -52,17 +51,17 @@ class DownloadsViewModel(
}
}
fun refreshPlugins() = viewModelScope.launch {
isRefreshingPlugins = true
downloaderPluginRepository.reload()
isRefreshingPlugins = false
fun refreshDownloader() = viewModelScope.launch {
isRefreshingDownloader = true
downloaderRepository.reload()
isRefreshingDownloader = false
}
fun trustPlugin(packageName: String) = viewModelScope.launch {
downloaderPluginRepository.trustPackage(packageName)
fun trustDownloader(packageName: String) = viewModelScope.launch {
downloaderRepository.trustPackage(packageName)
}
fun revokePluginTrust(packageName: String) = viewModelScope.launch {
downloaderPluginRepository.revokeTrustForPackage(packageName)
fun revokeDownloaderTrust(packageName: String) = viewModelScope.launch {
downloaderRepository.revokeTrustForPackage(packageName)
}
}

View File

@@ -34,8 +34,8 @@ import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.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
@@ -79,7 +79,7 @@ import java.io.File
import java.nio.file.Files
import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class PatcherViewModel(
private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, InstallerModel {
@@ -184,13 +184,13 @@ class PatcherViewModel(
input.options,
logger,
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
handleStartActivityRequest = { plugin, intent ->
handleStartActivityRequest = { downloader, intent ->
withContext(Dispatchers.Main) {
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
try {
// Wait for the dialog interaction.
val accepted = with(CompletableDeferred<Boolean>()) {
currentActivityRequest = this to plugin.name
currentActivityRequest = this to downloader.name
await()
}

View File

@@ -22,19 +22,19 @@ import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
@@ -59,7 +59,7 @@ import kotlinx.parcelize.Parcelize
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent {
@@ -67,13 +67,13 @@ class SelectedAppInfoViewModel(
private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get()
private val pluginsRepository: DownloaderPluginRepository = get()
private val downloaderRepository: DownloaderRepository = get()
private val installedAppRepository: InstalledAppRepository = get()
private val rootInstaller: RootInstaller = get()
private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get()
val plugins = pluginsRepository.loadedPluginsFlow
val downloader = downloaderRepository.loadedDownloaderPackageFlow
val desiredVersion = input.app.version
val packageName = input.app.packageName
@@ -160,15 +160,15 @@ class SelectedAppInfoViewModel(
var showSourceSelector by mutableStateOf(false)
private set
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
val activePluginAction get() = pluginAction?.first?.packageName
private var downloaderAction: Pair<LoadedDownloader, Job>? by mutableStateOf(null)
val activeDownloaderAction get() = downloaderAction?.first?.packageName
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
private val launchActivityChannel = Channel<Intent>()
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
val errorFlow = combine(downloader, snapshotFlow { selectedApp }) { downloaderList, app ->
when {
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
app is SelectedApp.Search && downloaderList.isEmpty() -> Error.NoDownloader
else -> null
}
}
@@ -178,23 +178,23 @@ class SelectedAppInfoViewModel(
showSourceSelector = true
}
private fun cancelPluginAction() {
pluginAction?.second?.cancel()
pluginAction = null
private fun cancelDownloaderAction() {
downloaderAction?.second?.cancel()
downloaderAction = null
}
fun dismissSourceSelector() {
cancelPluginAction()
cancelDownloaderAction()
showSourceSelector = false
}
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
cancelPluginAction()
pluginAction = plugin to viewModelScope.launch {
fun searchUsingDownloader(downloader: LoadedDownloader) {
cancelDownloaderAction()
downloaderAction = downloader to viewModelScope.launch {
try {
val scope = object : GetScope {
override val hostPackageName = app.packageName
override val pluginPackageName = plugin.packageName
override val downloaderPackageName = downloader.packageName
override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished")
@@ -219,7 +219,7 @@ class SelectedAppInfoViewModel(
}
withContext(Dispatchers.IO) {
plugin.get(scope, packageName, desiredVersion)
downloader.get(scope, packageName, desiredVersion)
}?.let { (data, version) ->
if (desiredVersion != null && version != desiredVersion) {
app.toast(app.getString(R.string.downloader_invalid_version))
@@ -228,7 +228,7 @@ class SelectedAppInfoViewModel(
selectedApp = SelectedApp.Download(
packageName,
version,
ParceledDownloaderData(plugin, data)
ParceledDownloaderData(downloader, data)
)
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
} catch (e: UserInteractionException.Activity) {
@@ -239,13 +239,13 @@ class SelectedAppInfoViewModel(
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
Log.e(tag, "Downloader.get threw an exception", e)
} finally {
pluginAction = null
downloaderAction = null
dismissSourceSelector()
}
}
}
fun handlePluginActivityResult(result: ActivityResult) {
fun handleDownloaderActivityResult(result: ActivityResult) {
launchedActivity?.complete(result)
}
@@ -307,7 +307,7 @@ class SelectedAppInfoViewModel(
}
enum class Error(@param:StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available)
NoDownloader(R.string.no_downloader_available)
}
private companion object {

View File

@@ -82,7 +82,7 @@ class UpdateViewModel(
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!!
withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
showInternetCheckDialog = true
} else {
state = State.DOWNLOADING

View File

@@ -19,8 +19,8 @@ Second \"item\" text"</string>
<string name="cli">CLI</string>
<string name="manager">Manager</string>
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string>
<string name="downloader_host_permission_label">ReVanced Manager downloader host</string>
<string name="downloader_host_permission_description">Used to control access to ReVanced Manager downloader. Only ReVanced Manager has this.</string>
<string name="toast_copied_to_clipboard">Copied!</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
@@ -30,7 +30,7 @@ Second \"item\" text"</string>
<string name="select_app">Select an app</string>
<string name="patches_count_selected">%1$d/%2$d selected</string>
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
<string name="new_downloader_notification">New downloader available. Click here to configure them.</string>
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
<string name="import_">Import</string>
@@ -56,7 +56,7 @@ Second \"item\" text"</string>
<string name="app_source_dialog_title">Select source</string>
<string name="app_source_dialog_option_auto">Auto</string>
<string name="app_source_dialog_option_auto_description">Use all available downloader to download the app</string>
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string>
<string name="app_source_dialog_option_auto_unavailable">No downloader available</string>
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string>
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
@@ -87,7 +87,7 @@ Second \"item\" text"</string>
<string name="updates">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string>
<string name="downloads_description">Downloader plugins and downloaded apps</string>
<string name="downloads_description">Downloader and downloaded apps</string>
<string name="import_export">Import &amp; export</string>
<string name="import_export_description">Keystore, patch options and selection</string>
<string name="advanced">Advanced</string>
@@ -175,17 +175,17 @@ You will not be able to update the previously installed apps from this source."<
<string name="patch_options_reset_all">Reset patch options globally</string>
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
<string name="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader_plugins">Plugins</string>
<string name="downloader_plugin_state_trusted">Trusted</string>
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string>
<string name="downloader_plugin_state_untrusted">Untrusted</string>
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_plugin_trust_dialog_body">Continuing will allow this plugin to run on your system.\n\nOnly enable this plugin if you trust it. Plugins can execute arbitrary code and may compromise your device.</string>
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader">Downloader</string>
<string name="downloader_state_trusted">Trusted%s</string>
<string name="downloader_state_failed">Failed to load. Click for more details</string>
<string name="downloader_state_untrusted">Untrusted</string>
<string name="downloader_trust_dialog_title">Trust downloader?</string>
<string name="downloader_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_trust_dialog_body">Continuing will allow this downloader to run on your system.\n\nOnly enable this downloader if you trust it. Downloader can execute arbitrary code and may compromise your device.</string>
<string name="downloader_trust_dialog_signature">Signature:\n\n%s</string>
<string name="downloader_trust_dialog_name">Downloader:\n%s</string>
<string name="downloader_delete_apps_title">Delete selected apps</string>
<string name="downloader_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found.</string>
<string name="search_apps">Search apps…</string>
@@ -217,6 +217,10 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="appearance">Appearance</string>
<string name="networking">Networking</string>
<string name="allow_metered_networks">Allow metered networks</string>
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
The application might still warn about metered networks for manual operations.</string>
<string name="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
@@ -312,8 +316,8 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="downloader_invalid_version">Downloader did not fetch the correct version</string>
<string name="downloader_app_not_found">Downloader did not find the app</string>
<string name="downloader_error">Downloader error: %s</string>
<string name="downloader_no_plugins_installed">No downloader installed.</string>
<string name="downloader_no_plugins_available">There are downloaders installed but none are trusted. Check your settings.</string>
<string name="no_downloader_installed">No downloader installed.</string>
<string name="no_downloader_available">There are downloader installed but none are trusted. Check your settings.</string>
<string name="already_patched">Already patched</string>
<string name="patch_selector_sheet_filter_title">Filter</string>
@@ -341,7 +345,7 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="save_apk_success">APK Saved</string>
<string name="sign_fail">Failed to sign APK: %s</string>
<string name="save_logs">Save logs</string>
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string>
<string name="downloader_activity_dialog_body">User interaction is required in order to proceed with this downloader.</string>
<string name="select_install_type">Select installation type</string>
<string name="patcher_step_group_preparing">Preparing</string>

View File

@@ -4,3 +4,4 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
android.nonFinalResIds=false
org.gradle.caching=true
version=1.0.0