mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-18 00:33:58 +00:00
Compare commits
1 Commits
feat/impro
...
feat/root-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff70a77afb |
@@ -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 system
|
||||
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||
- 💉 **Patch**: Select and apply patches to any Android app
|
||||
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||
|
||||
|
||||
104
api/api/api.api
104
api/api/api.api
@@ -1,19 +1,19 @@
|
||||
public abstract interface class app/revanced/manager/downloader/BaseDownloadScope : app/revanced/manager/downloader/Scope {
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
}
|
||||
|
||||
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/ConstantsKt {
|
||||
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/DownloadUrl : android/os/Parcelable {
|
||||
public final class app/revanced/manager/plugin/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/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 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 describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getHeaders ()Ljava/util/Map;
|
||||
@@ -24,61 +24,58 @@ public final class app/revanced/manager/downloader/DownloadUrl : android/os/Parc
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/DownloadUrl;
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/DownloadUrl;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/Downloader {
|
||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/DownloaderBuilder {
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public abstract interface annotation class app/revanced/manager/downloader/DownloaderHostApi : java/lang/annotation/Annotation {
|
||||
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 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 final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/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/downloader/ExtensionsKt {
|
||||
public static final fun download (Lapp/revanced/manager/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
|
||||
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 abstract interface class app/revanced/manager/downloader/GetScope : app/revanced/manager/downloader/Scope {
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/downloader/InputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/downloader/OutputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/Package : android/os/Parcelable {
|
||||
public final class app/revanced/manager/plugin/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/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 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 describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getName ()Ljava/lang/String;
|
||||
@@ -88,95 +85,98 @@ public final class app/revanced/manager/downloader/Package : android/os/Parcelab
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/Package;
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/Package;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/downloader/Scope {
|
||||
public abstract fun getDownloaderPackageName ()Ljava/lang/String;
|
||||
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 fun getHostPackageName ()Ljava/lang/String;
|
||||
public abstract fun getPluginPackageName ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/downloader/UserInteractionException : java/lang/Exception {
|
||||
public abstract class app/revanced/manager/plugin/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/downloader/UserInteractionException$Activity : app/revanced/manager/downloader/UserInteractionException {
|
||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/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/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/downloader/UserInteractionException$Activity {
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/downloader/UserInteractionException$Activity {
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/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/downloader/UserInteractionException$RequestDenied : app/revanced/manager/downloader/UserInteractionException {
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
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 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 class app/revanced/manager/downloader/webview/IWebView$Default : app/revanced/manager/downloader/webview/IWebView {
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/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/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebView {
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebView;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public class app/revanced/manager/downloader/webview/IWebViewEvents$Default : app/revanced/manager/downloader/webview/IWebViewEvents {
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/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/downloader/webview/IWebView;)V
|
||||
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebViewEvents {
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebViewEvents;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/downloader/webview/WebViewCallbackScope : app/revanced/manager/downloader/Scope {
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/downloader/webview/WebViewScope : app/revanced/manager/downloader/Scope {
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/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
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ dependencies {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.downloader"
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
@@ -52,7 +52,7 @@ android {
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
nonPublicMarkers += "app.revanced.manager.downloader.DownloaderHostApi"
|
||||
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// IWebView.aidl
|
||||
package app.revanced.manager.downloader.webview;
|
||||
|
||||
@JavaPassthrough(annotation="@app.revanced.manager.downloader.DownloaderHostApi")
|
||||
oneway interface IWebView {
|
||||
void load(String url);
|
||||
void finish();
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
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"
|
||||
@@ -0,0 +1,7 @@
|
||||
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"
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.downloader
|
||||
package app.revanced.manager.plugin.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 downloader hosts, don't use it in a downloader.",
|
||||
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
|
||||
)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DownloaderHostApi
|
||||
annotation class PluginHostApi
|
||||
|
||||
/**
|
||||
* The base interface for all DSL scopes.
|
||||
@@ -30,9 +30,9 @@ interface Scope {
|
||||
val hostPackageName: String
|
||||
|
||||
/**
|
||||
* The package name of the downloader.
|
||||
* The package name of the plugin.
|
||||
*/
|
||||
val downloaderPackageName: String
|
||||
val pluginPackageName: 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 downloader.
|
||||
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
|
||||
* @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 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.
|
||||
// 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.
|
||||
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 downloader.
|
||||
* Define the download block of the plugin.
|
||||
*/
|
||||
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 downloader.
|
||||
* Define the get block of the plugin.
|
||||
* 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) {
|
||||
@DownloaderHostApi
|
||||
@PluginHostApi
|
||||
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:DownloaderHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
||||
@property:DownloaderHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
||||
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
||||
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
||||
)
|
||||
|
||||
/**
|
||||
* Define a downloader.
|
||||
* Define a downloader plugin.
|
||||
*/
|
||||
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 @DownloaderHostApi constructor() :
|
||||
class RequestDenied @PluginHostApi constructor() :
|
||||
UserInteractionException("Request denied by user")
|
||||
|
||||
sealed class Activity(message: String) : UserInteractionException(message) {
|
||||
class Cancelled @DownloaderHostApi constructor() : Activity("Interaction cancelled")
|
||||
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
|
||||
|
||||
/**
|
||||
* @param resultCode The result code of the activity.
|
||||
* @param intent The [Intent] of the activity.
|
||||
*/
|
||||
class NotCompleted @DownloaderHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
||||
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
||||
Activity("Unexpected activity result code: $resultCode")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.downloader
|
||||
package app.revanced.manager.plugin.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(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
|
||||
Intent().apply { setClassName(pluginPackageName, 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(downloaderPackageName, SERVICE::class.qualifiedName!!) }, block
|
||||
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.downloader
|
||||
package app.revanced.manager.plugin.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 downloader that only need a name and version to implement their [DownloaderScope.download] function.
|
||||
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
|
||||
*
|
||||
* @param name The package name.
|
||||
* @param version The version.
|
||||
@@ -1,12 +1,12 @@
|
||||
package app.revanced.manager.downloader.webview
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.content.Intent
|
||||
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 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 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(DownloaderHostApi::class)
|
||||
@OptIn(PluginHostApi::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(DownloaderHostApi::class)
|
||||
@OptIn(PluginHostApi::class)
|
||||
suspend fun <T> GetScope.runWebView(
|
||||
title: String,
|
||||
block: suspend WebViewScope<T>.() -> InitialUrl
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.downloader.webview
|
||||
package app.revanced.manager.plugin.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.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.R
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.R
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
@DownloaderHostApi
|
||||
@OptIn(PluginHostApi::class)
|
||||
@PluginHostApi
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -110,7 +110,7 @@ class WebViewActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
@OptIn(PluginHostApi::class)
|
||||
internal class WebViewModel : ViewModel() {
|
||||
init {
|
||||
CookieManager.getInstance().apply {
|
||||
@@ -1,31 +1,3 @@
|
||||
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
|
||||
|
||||
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
|
||||
|
||||
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
||||
|
||||
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||
|
||||
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||
|
||||
|
||||
|
||||
@@ -108,10 +108,6 @@ dependencies {
|
||||
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
|
||||
// Ackpine
|
||||
implementation(libs.ackpine.core)
|
||||
implementation(libs.ackpine.ktx)
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.17
|
||||
version = 1.26.0-dev.13
|
||||
|
||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -1,7 +1,7 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
||||
-keep class app.revanced.manager.downloader.** { *; }
|
||||
-keep class app.revanced.manager.plugin.** { *; }
|
||||
-keep class app.revanced.patcher.** { *; }
|
||||
-keep class com.android.tools.smali.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "9a937123afc782978d185d00c35d20e0",
|
||||
"identityHash": "d0119047505da435972c5247181de675",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "patch_bundles",
|
||||
@@ -21,9 +21,10 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionHash",
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT"
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
@@ -43,7 +44,9 @@
|
||||
"columnNames": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "patch_selections",
|
||||
@@ -124,6 +127,7 @@
|
||||
"patch_name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "patch_selections",
|
||||
@@ -173,7 +177,9 @@
|
||||
"package_name",
|
||||
"version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "installed_app",
|
||||
@@ -209,7 +215,9 @@
|
||||
"columnNames": [
|
||||
"current_package_name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "applied_patch",
|
||||
@@ -370,6 +378,7 @@
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "option_groups",
|
||||
@@ -385,7 +394,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "trusted_downloader",
|
||||
"tableName": "trusted_downloader_plugins",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
||||
"fields": [
|
||||
{
|
||||
@@ -406,12 +415,15 @@
|
||||
"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, '9a937123afc782978d185d00c35d20e0')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission
|
||||
android:name="app.revanced.manager.permission.DOWNLOADER_HOST"
|
||||
android:name="app.revanced.manager.permission.PLUGIN_HOST"
|
||||
android:protectionLevel="signature"
|
||||
android:label="@string/downloader_host_permission_label"
|
||||
android:description="@string/downloader_host_permission_description"
|
||||
android:label="@string/plugin_host_permission_label"
|
||||
android:description="@string/plugin_host_permission_description"
|
||||
/>
|
||||
|
||||
<uses-permission android:name="app.revanced.manager.permission.DOWNLOADER_HOST" />
|
||||
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_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,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
@@ -72,15 +75,5 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,4 +0,0 @@
|
||||
// ProgressEventParcel.aidl
|
||||
package app.revanced.manager.patcher;
|
||||
|
||||
parcelable ProgressEventParcel;
|
||||
@@ -1,12 +1,11 @@
|
||||
// IPatcherEvents.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||
|
||||
// Interface for sending events back to the main app process.
|
||||
oneway interface IPatcherEvents {
|
||||
void log(String level, String msg);
|
||||
void event(in ProgressEventParcel event);
|
||||
void patchSucceeded();
|
||||
void progress(String name, String state, String msg);
|
||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||
void finished(String exceptionStackTrace);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
onUpdateClick = {
|
||||
navController.navigate(Update())
|
||||
},
|
||||
onDownloaderClick = {
|
||||
onDownloaderPluginClick = {
|
||||
navController.navigate(Settings.Downloads)
|
||||
},
|
||||
onAppClick = { packageName ->
|
||||
|
||||
@@ -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.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
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 downloaderRepository: DownloaderRepository by inject()
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -48,8 +48,7 @@ class ManagerApplication : Application() {
|
||||
workerModule,
|
||||
viewModelModule,
|
||||
databaseModule,
|
||||
rootModule,
|
||||
ackpineModule
|
||||
rootModule
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ class ManagerApplication : Application() {
|
||||
prefs.preload()
|
||||
}
|
||||
scope.launch(Dispatchers.Default) {
|
||||
downloaderRepository.reload()
|
||||
downloaderPluginRepository.reload()
|
||||
}
|
||||
scope.launch(Dispatchers.Default) {
|
||||
with(patchBundleRepository) {
|
||||
|
||||
@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
|
||||
/**
|
||||
* Returns true if it is safe to download large files.
|
||||
*/
|
||||
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
|
||||
fun isSafe() = isConnected() && isUnmetered()
|
||||
}
|
||||
@@ -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.downloader.TrustedDownloader
|
||||
import app.revanced.manager.data.room.downloader.TrustedDownloaderDao
|
||||
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
|
||||
import kotlin.random.Random
|
||||
|
||||
@Database(
|
||||
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloader::class],
|
||||
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::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 trustedDownloaderDao(): TrustedDownloaderDao
|
||||
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||
|
||||
companion object {
|
||||
fun generateUid() = Random.Default.nextInt()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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>)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package app.revanced.manager.data.room.downloader
|
||||
package app.revanced.manager.data.room.plugins
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "trusted_downloader")
|
||||
class TrustedDownloader(
|
||||
@Entity(tableName = "trusted_downloader_plugins")
|
||||
class TrustedDownloaderPlugin(
|
||||
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
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>)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
|
||||
val ackpineModule = module {
|
||||
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||
|
||||
single {
|
||||
provideInstaller(androidContext())
|
||||
}
|
||||
single {
|
||||
provideUninstaller(androidContext())
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ val repositoryModule = module {
|
||||
// It is best to load patch bundles ASAP
|
||||
createdAtStart()
|
||||
}
|
||||
singleOf(::DownloaderRepository)
|
||||
singleOf(::DownloaderPluginRepository)
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
singleOf(::InstalledAppRepository)
|
||||
|
||||
@@ -54,7 +54,11 @@ class RootInstaller(
|
||||
await()
|
||||
}
|
||||
|
||||
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
|
||||
suspend fun execute(vararg commands: String): Shell.Result {
|
||||
val stdout = mutableListOf<String>()
|
||||
val stderr = mutableListOf<String>()
|
||||
return getShell().newJob().add(*commands).to(stdout, stderr).exec()
|
||||
}
|
||||
|
||||
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||
|
||||
@@ -108,20 +112,15 @@ class RootInstaller(
|
||||
unmount(packageName)
|
||||
|
||||
stockAPK?.let { stockApp ->
|
||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||
// TODO: get user id programmatically
|
||||
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
|
||||
pm.getPackageInfo(patchedAPK)
|
||||
?: error("Failed to get package info for patched app")
|
||||
)
|
||||
)
|
||||
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
||||
}
|
||||
// TODO: get user id programmatically
|
||||
execute("pm uninstall -k --user 0 $packageName")
|
||||
|
||||
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
|
||||
execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"")
|
||||
.assertSuccess("Failed to install stock app")
|
||||
}
|
||||
|
||||
remoteFS.getFile(modulePath).mkdir()
|
||||
remoteFS.getFile(modulePath).mkdirs()
|
||||
.also { if (!it) throw Exception("Failed to create module directory") }
|
||||
|
||||
listOf(
|
||||
"service.sh",
|
||||
@@ -142,7 +141,6 @@ class RootInstaller(
|
||||
}
|
||||
|
||||
"$modulePath/$packageName.apk".let { apkPath ->
|
||||
|
||||
remoteFS.getFile(patchedAPK.absolutePath)
|
||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||
.newInputStream().use { inputStream ->
|
||||
@@ -173,9 +171,43 @@ class RootInstaller(
|
||||
const val modulesPath = "/data/adb/modules"
|
||||
|
||||
private fun Shell.Result.assertSuccess(errorMessage: String) {
|
||||
if (!isSuccess) throw Exception(errorMessage)
|
||||
if (!isSuccess) {
|
||||
throw ShellCommandException(
|
||||
errorMessage,
|
||||
code,
|
||||
out,
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShellCommandException(
|
||||
val userMessage: String,
|
||||
val exitCode: Int,
|
||||
val stdout: List<String>,
|
||||
val stderr: List<String>
|
||||
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
|
||||
companion object {
|
||||
private fun format(message: String, exitCode: Int, stdout: List<String>, stderr: List<String>): String =
|
||||
buildString {
|
||||
appendLine(message)
|
||||
appendLine("Exit code: $exitCode")
|
||||
|
||||
val output = stdout.filter { it.isNotBlank() }
|
||||
val errors = stderr.filter { it.isNotBlank() }
|
||||
|
||||
if (output.isNotEmpty()) {
|
||||
appendLine("stdout:")
|
||||
output.forEach(::appendLine)
|
||||
}
|
||||
if (errors.isNotEmpty()) {
|
||||
appendLine("stderr:")
|
||||
errors.forEach(::appendLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RootServiceException : Exception("Root not available")
|
||||
@@ -31,9 +31,7 @@ class PreferencesManager(
|
||||
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||
|
||||
val acknowledgedDownloader = stringSetPreference("acknowledged_downloader", emptySet())
|
||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||
|
||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||
|
||||
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
|
||||
}
|
||||
|
||||
@@ -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.LoadedDownloader
|
||||
import app.revanced.manager.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.plugin.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(
|
||||
downloader: LoadedDownloader,
|
||||
plugin: LoadedDownloaderPlugin,
|
||||
data: Parcelable,
|
||||
expectedPackageName: String,
|
||||
expectedVersion: String?,
|
||||
@@ -55,7 +55,7 @@ class DownloadedAppRepository(
|
||||
|
||||
channelFlow {
|
||||
val scope = object : OutputDownloadScope {
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
override val pluginPackageName = plugin.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(
|
||||
)
|
||||
}
|
||||
}
|
||||
downloader.download(scope, data, stream)
|
||||
plugin.download(scope, data, stream)
|
||||
}
|
||||
}
|
||||
.conflate()
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,173 +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.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")
|
||||
}
|
||||
}
|
||||
@@ -286,29 +286,28 @@ 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()))
|
||||
}
|
||||
|
||||
deleteLocalFile()
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
doReload()
|
||||
deleteLocalFile()
|
||||
}
|
||||
}
|
||||
|
||||
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, force = true)
|
||||
update(src)
|
||||
state.copy(sources = state.sources.put(src.uid, src))
|
||||
}
|
||||
|
||||
@@ -330,38 +329,32 @@ class PatchBundleRepository(
|
||||
state.copy(sources = state.sources.put(uid, newSrc))
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
vararg sources: RemotePatchBundle,
|
||||
showToast: Boolean = false,
|
||||
force: Boolean = false
|
||||
) {
|
||||
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||
val uids = sources.map { it.uid }.toSet()
|
||||
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
|
||||
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||
}
|
||||
|
||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
|
||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||
|
||||
/**
|
||||
* Updates all bundles that should be automatically updated.
|
||||
*/
|
||||
suspend fun updateCheck() =
|
||||
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
|
||||
suspend fun updateCheck() = store.dispatch(Update { 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 (redownload) "Redownload remote bundles" else "Update check"
|
||||
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||
|
||||
override suspend fun ActionContext.execute(
|
||||
current: State
|
||||
) = coroutineScope {
|
||||
if (!networkInfo.isSafe(force)) {
|
||||
if (!networkInfo.isSafe()) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@coroutineScope current
|
||||
}
|
||||
@@ -374,7 +367,7 @@ class PatchBundleRepository(
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
|
||||
val newVersion = with(it) {
|
||||
if (redownload) downloadLatest() else update()
|
||||
if (force) downloadLatest() else update()
|
||||
} ?: return@async null
|
||||
|
||||
it to newVersion
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import java.io.OutputStream
|
||||
|
||||
class LoadedDownloader(
|
||||
class LoadedDownloaderPlugin(
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
val version: String,
|
||||
@@ -10,22 +10,20 @@ 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 downloaderPackageName: String,
|
||||
val downloaderName: String,
|
||||
val pluginPackageName: String,
|
||||
private val bundle: Bundle
|
||||
) : Parcelable {
|
||||
constructor(downloader: LoadedDownloader, data: Parcelable) : this(
|
||||
downloader.packageName,
|
||||
downloader.name,
|
||||
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
|
||||
plugin.packageName,
|
||||
createBundle(data)
|
||||
)
|
||||
|
||||
fun unwrapWith(downloader: LoadedDownloader): Parcelable {
|
||||
bundle.classLoader = downloader.classLoader
|
||||
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
|
||||
bundle.classLoader = plugin.classLoader
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val className = bundle.getString(CLASS_NAME_KEY)!!
|
||||
val clazz = downloader.classLoader.loadClass(className)
|
||||
val clazz = plugin.classLoader.loadClass(className)
|
||||
|
||||
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
|
||||
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
sealed class ProgressEvent : Parcelable {
|
||||
abstract val stepId: StepId?
|
||||
|
||||
data class Started(override val stepId: StepId) : ProgressEvent()
|
||||
|
||||
data class Progress(
|
||||
override val stepId: StepId,
|
||||
val current: Long? = null,
|
||||
val total: Long? = null,
|
||||
val message: String? = null,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Completed(
|
||||
override val stepId: StepId,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Failed(
|
||||
override val stepId: StepId?,
|
||||
val error: RemoteError,
|
||||
) : ProgressEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable wrapper for [ProgressEvent].
|
||||
*
|
||||
* Required because AIDL does not support sealed classes.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
|
||||
|
||||
fun ProgressEventParcel.toEvent(): ProgressEvent = event
|
||||
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
|
||||
|
||||
@Parcelize
|
||||
sealed class StepId : Parcelable {
|
||||
data object DownloadAPK : StepId()
|
||||
data object LoadPatches : StepId()
|
||||
data object ReadAPK : StepId()
|
||||
data object ExecutePatches : StepId()
|
||||
data class ExecutePatch(val index: Int) : StepId()
|
||||
data object WriteAPK : StepId()
|
||||
data object SignAPK : StepId()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class RemoteError(
|
||||
val type: String,
|
||||
val message: String?,
|
||||
val stackTrace: String,
|
||||
) : Parcelable
|
||||
|
||||
fun Exception.toRemoteError() = RemoteError(
|
||||
type = this::class.java.name,
|
||||
message = this.message,
|
||||
stackTrace = this.stackTraceToString(),
|
||||
)
|
||||
|
||||
|
||||
inline fun <T> runStep(
|
||||
stepId: StepId,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
block: () -> T,
|
||||
): T = try {
|
||||
onEvent(ProgressEvent.Started(stepId))
|
||||
val value = block()
|
||||
onEvent(ProgressEvent.Completed(stepId))
|
||||
value
|
||||
} catch (error: Exception) {
|
||||
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
|
||||
throw error
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.patcher.Session.Companion.component1
|
||||
import app.revanced.manager.patcher.Session.Companion.component2
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
@@ -21,10 +22,15 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
private val androidContext: Context,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onEvent: (ProgressEvent) -> Unit,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
onProgress(name, state, message)
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
@@ -36,68 +42,86 @@ class Session(
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
this().collect { (patch, exception) ->
|
||||
val index = selectedPatches.indexOf(patch)
|
||||
if (index == -1) return@collect
|
||||
if (patch !in selectedPatches) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
)
|
||||
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
nextPatchIndex++
|
||||
|
||||
onPatchCompleted()
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_executed,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
runStep(StepId.ExecutePatches, onEvent) {
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
|
||||
addHandler(logger.handler)
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
runStep(StepId.WriteAPK, onEvent) {
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import java.io.File
|
||||
@@ -14,7 +13,7 @@ import java.io.File
|
||||
/**
|
||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||
*/
|
||||
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
@@ -22,50 +21,47 @@ class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
patchList
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onEvent,
|
||||
)
|
||||
}
|
||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||
|
||||
session.use { s ->
|
||||
s.run(
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
context,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onPatchCompleted = onPatchCompleted,
|
||||
onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
File(outputFile),
|
||||
patchList
|
||||
)
|
||||
|
||||
@@ -10,13 +10,12 @@ import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||
import app.revanced.manager.patcher.LibraryResolver
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.ProgressEventParcel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||
import app.revanced.manager.patcher.toEvent
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -67,7 +66,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||
@@ -111,6 +111,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
@@ -123,10 +124,13 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun event(event: ProgressEventParcel?) {
|
||||
event?.let { onEvent(it.toEvent()) }
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
||||
override fun finished(exceptionStackTrace: String?) {
|
||||
binder.exit()
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.content.Context
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -34,6 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
||||
@@ -8,15 +8,12 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toParcel
|
||||
import app.revanced.manager.ui.model.State
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +24,7 @@ import kotlin.system.exitProcess
|
||||
/**
|
||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||
*/
|
||||
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
private var eventBinder: IPatcherEvents? = null
|
||||
|
||||
private val scope =
|
||||
@@ -49,8 +46,6 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
override fun exit() = exitProcess(0)
|
||||
|
||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||
|
||||
eventBinder = events
|
||||
|
||||
scope.launch {
|
||||
@@ -61,42 +56,38 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||
val allPatches = PatchBundle.Loader.patches(
|
||||
parameters.configurations.map { it.bundle },
|
||||
parameters.packageName
|
||||
)
|
||||
|
||||
parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
.filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onEvent = ::onEvent,
|
||||
)
|
||||
}
|
||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||
|
||||
session.use {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
androidContext = context,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onPatchCompleted = { events.patchSucceeded() },
|
||||
onProgress = { name, state, message ->
|
||||
events.progress(name, state?.name, message)
|
||||
}
|
||||
).use {
|
||||
it.run(File(parameters.outputFile), patchList)
|
||||
}
|
||||
|
||||
@@ -128,7 +119,7 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess()
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||
|
||||
@@ -24,22 +24,19 @@ 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.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
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.LoadedDownloader
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toRemoteError
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -51,7 +48,9 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class PatcherWorker(
|
||||
context: Context,
|
||||
parameters: WorkerParameters
|
||||
@@ -59,7 +58,7 @@ class PatcherWorker(
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val downloaderRepository: DownloaderRepository by inject()
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||
private val pm: PM by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
@@ -72,9 +71,11 @@ class PatcherWorker(
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloader, Intent) -> ActivityResult,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onEvent: (ProgressEvent) -> Unit,
|
||||
val onProgress: ProgressEventHandler
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
@@ -139,6 +140,10 @@ class PatcherWorker(
|
||||
}
|
||||
|
||||
private suspend fun runPatcher(args: Args): Result {
|
||||
|
||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
args.onProgress(name, state, message)
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
@@ -150,73 +155,59 @@ class PatcherWorker(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun download(downloader: LoadedDownloader, data: Parcelable) =
|
||||
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
|
||||
downloadedAppRepository.download(
|
||||
downloader,
|
||||
plugin,
|
||||
data,
|
||||
args.packageName,
|
||||
args.input.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
onDownload = { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
)
|
||||
}
|
||||
).also { args.setInputFile(it) }
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
val (downloader, data) = downloaderRepository.unwrapParceledData(
|
||||
selectedApp.data
|
||||
)
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||
|
||||
download(downloader, data)
|
||||
}
|
||||
download(plugin, data)
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
downloaderRepository.loadedDownloaderPackageFlow.first()
|
||||
.firstNotNullOfOrNull { downloader ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
override val hostPackageName =
|
||||
applicationContext.packageName
|
||||
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result =
|
||||
args.handleStartActivityRequest(downloader, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = applicationContext.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result = args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(downloader, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
@@ -236,12 +227,12 @@ class PatcherWorker(
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
args.onEvent,
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
)
|
||||
|
||||
runStep(StepId.SignAPK, args.onEvent) {
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
}
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
Result.success()
|
||||
@@ -250,21 +241,11 @@ 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
|
||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||
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
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class InstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
when (extraStatus) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}.apply {
|
||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||
|
||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
@@ -80,7 +79,7 @@ private fun installerStatusDialogButton(
|
||||
enum class DialogKind(
|
||||
val flag: Int,
|
||||
val title: Int,
|
||||
@param:StringRes val contentStringResId: Int,
|
||||
@StringRes val contentStringResId: Int,
|
||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||
val dismissButton: InstallerStatusDialogButton? = null,
|
||||
@@ -134,8 +133,10 @@ enum class DialogKind(
|
||||
title = R.string.installation_storage_issue_dialog_title,
|
||||
contentStringResId = R.string.installation_storage_issue_description,
|
||||
),
|
||||
|
||||
@RequiresApi(34)
|
||||
FAILURE_TIMEOUT(
|
||||
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
title = R.string.installation_timeout_dialog_title,
|
||||
contentStringResId = R.string.installation_timeout_description,
|
||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||
|
||||
@@ -39,9 +39,11 @@ import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -50,6 +52,8 @@ import kotlin.math.floor
|
||||
fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
stepProgressProvider: StepProgressProvider,
|
||||
isExpanded: Boolean = false,
|
||||
onExpand: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
@@ -63,17 +67,8 @@ fun Steps(
|
||||
}
|
||||
}
|
||||
|
||||
val filteredSteps = remember(steps) {
|
||||
val failedCount = steps.count { it.state == State.FAILED }
|
||||
|
||||
steps.filter { step ->
|
||||
// Show hidden steps if it's the only failed step.
|
||||
!step.hide || (step.state == State.FAILED && failedCount == 1)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state == State.RUNNING || state == State.FAILED)
|
||||
if (state == State.RUNNING)
|
||||
onExpand()
|
||||
}
|
||||
|
||||
@@ -97,8 +92,13 @@ fun Steps(
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
val stepProgress = remember(stepCount, steps) {
|
||||
stepCount?.let { (current, total) -> "$current/$total" }
|
||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||
text = stepProgress,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
@@ -112,20 +112,23 @@ fun Steps(
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
) {
|
||||
filteredSteps.forEachIndexed { index, step ->
|
||||
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${current.megaBytes} MB"
|
||||
steps.forEachIndexed { index, step ->
|
||||
val (progress, progressText) = when (step.progressKey) {
|
||||
null -> null
|
||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${downloaded.megaBytes} MB"
|
||||
}
|
||||
} ?: (null to null)
|
||||
|
||||
SubStep(
|
||||
name = step.title,
|
||||
name = step.name,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
progress = progress,
|
||||
progressText = progressText,
|
||||
isFirst = index == 0,
|
||||
isLast = index == filteredSteps.lastIndex,
|
||||
isLast = index == steps.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.model
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
@@ -16,20 +15,19 @@ enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
enum class ProgressKey {
|
||||
DOWNLOAD
|
||||
}
|
||||
|
||||
interface StepProgressProvider {
|
||||
val downloadProgress: Pair<Long, Long?>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val id: StepId,
|
||||
val title: String,
|
||||
val name: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val progress: Pair<Long, Long?>? = null,
|
||||
val hide: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
|
||||
fun Step.withState(
|
||||
state: State = this.state,
|
||||
message: String? = this.message,
|
||||
progress: Pair<Long, Long?>? = this.progress
|
||||
) = copy(state = state, message = message, progress = progress)
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
||||
@@ -90,13 +90,13 @@ fun DashboardScreen(
|
||||
onAppSelectorClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
onDownloaderClick: () -> Unit,
|
||||
onDownloaderPluginClick: () -> 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 showNewDownloaderNotification by vm.newDownloaderAvailable.collectAsStateWithLifecycle(
|
||||
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||
false
|
||||
)
|
||||
val androidContext = LocalContext.current
|
||||
@@ -307,14 +307,14 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
if (showNewDownloaderNotification) {
|
||||
if (showNewDownloaderPluginsNotification) {
|
||||
{
|
||||
NotificationCard(
|
||||
text = stringResource(R.string.new_downloader_notification),
|
||||
text = stringResource(R.string.new_downloader_plugins_notification),
|
||||
icon = Icons.Outlined.Download,
|
||||
modifier = Modifier.clickable(onClick = onDownloaderClick),
|
||||
modifier = Modifier.clickable(onClick = onDownloaderPluginClick),
|
||||
actions = {
|
||||
TextButton(onClick = vm::ignoreNewDownloader) {
|
||||
TextButton(onClick = vm::ignoreNewDownloaderPlugins) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ fun PatcherScreen(
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
viewModel.steps.groupBy { it.category }.toList()
|
||||
viewModel.steps.groupBy { it.category }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ fun PatcherScreen(
|
||||
},
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Text(stringResource(R.string.downloader_activity_dialog_body))
|
||||
Text(stringResource(R.string.plugin_activity_dialog_body))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -230,12 +230,14 @@ fun PatcherScreen(
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = steps,
|
||||
items = steps.toList(),
|
||||
key = { it.first }
|
||||
) { (category, steps) ->
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
||||
stepProgressProvider = viewModel,
|
||||
isExpanded = expandedCategory == category,
|
||||
onExpand = { expandCategory(category) },
|
||||
onClick = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -26,7 +25,6 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -40,7 +38,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.LoadedDownloader
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@@ -78,16 +76,16 @@ fun SelectedAppInfoScreen(
|
||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||
val patches by remember {
|
||||
derivedStateOf {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
val selectedPatchCount = remember(patches) {
|
||||
patches.values.sumOf { it.size }
|
||||
}
|
||||
val selectedPatchCount = patches.values.sumOf { it.size }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::handleDownloaderActivityResult
|
||||
onResult = vm::handlePluginActivityResult
|
||||
)
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
launcher.launch(intent)
|
||||
@@ -141,22 +139,22 @@ fun SelectedAppInfoScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
val downloader by vm.downloader.collectAsStateWithLifecycle(emptyList())
|
||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
downloader = downloader,
|
||||
plugins = plugins,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activeDownloaderAction,
|
||||
activeSearchJob = vm.activePluginAction,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectDownloader = vm::searchUsingDownloader,
|
||||
onSelectPlugin = vm::searchUsingPlugin,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
@@ -202,8 +200,8 @@ fun SelectedAppInfoScreen(
|
||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||
is SelectedApp.Download -> stringResource(
|
||||
R.string.apk_source_downloader,
|
||||
downloader.find { it.packageName == app.data.downloaderPackageName && it.name == app.data.downloaderName }?.let { "${it.packageName} ${it.name}" }
|
||||
?: app.data.downloaderPackageName
|
||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||
?: app.data.pluginPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
@@ -280,14 +278,14 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
||||
|
||||
@Composable
|
||||
private fun AppSourceSelectorDialog(
|
||||
downloader: List<LoadedDownloader>,
|
||||
plugins: List<LoadedDownloaderPlugin>,
|
||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||
searchApp: SelectedApp.Search,
|
||||
activeSearchJob: String?,
|
||||
hasRoot: Boolean,
|
||||
requiredVersion: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectDownloader: (LoadedDownloader) -> Unit,
|
||||
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
|
||||
onSelect: (SelectedApp) -> Unit,
|
||||
) {
|
||||
val canSelect = activeSearchJob == null
|
||||
@@ -304,15 +302,15 @@ private fun AppSourceSelectorDialog(
|
||||
text = {
|
||||
LazyColumn {
|
||||
item(key = "auto") {
|
||||
val hasDownloader = downloader.isNotEmpty()
|
||||
val hasPlugins = plugins.isNotEmpty()
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && hasDownloader) { onSelect(searchApp) }
|
||||
.enabled(hasDownloader),
|
||||
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
||||
.enabled(hasPlugins),
|
||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (hasDownloader)
|
||||
if (hasPlugins)
|
||||
stringResource(R.string.app_source_dialog_option_auto_description)
|
||||
else
|
||||
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
||||
@@ -350,11 +348,11 @@ private fun AppSourceSelectorDialog(
|
||||
}
|
||||
}
|
||||
|
||||
items(downloader, key = { "downloader_${it.packageName}" }) { downloader ->
|
||||
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectDownloader(downloader) },
|
||||
headlineContent = { Text(downloader.name) },
|
||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == downloader.packageName },
|
||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
||||
headlineContent = { Text(plugin.name) },
|
||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.DownloaderPackageState
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
@@ -60,7 +60,7 @@ fun DownloadsSettingsScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val downloaderStates by viewModel.downloaderStates.collectAsStateWithLifecycle()
|
||||
val pluginStates by viewModel.downloaderPluginStates.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_delete_apps_title),
|
||||
description = stringResource(R.string.downloader_delete_apps_description),
|
||||
title = stringResource(R.string.downloader_plugin_delete_apps_title),
|
||||
description = stringResource(R.string.downloader_plugin_delete_apps_description),
|
||||
icon = Icons.Outlined.Delete
|
||||
)
|
||||
}
|
||||
@@ -92,118 +92,111 @@ fun DownloadsSettingsScreen(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
onRefresh = viewModel::refreshDownloader,
|
||||
isRefreshing = viewModel.isRefreshingDownloader,
|
||||
onRefresh = viewModel::refreshPlugins,
|
||||
isRefreshing = viewModel.isRefreshingPlugins,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
GroupHeader(stringResource(R.string.downloader))
|
||||
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||
}
|
||||
|
||||
if (downloaderStates.isNotEmpty()) {
|
||||
downloaderStates.forEach { (packageName, state) ->
|
||||
item(key = packageName) {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
showDialog = false
|
||||
}
|
||||
|
||||
val packageInfo =
|
||||
remember(packageName) {
|
||||
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
|
||||
}
|
||||
|
||||
when (state) {
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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!!) }
|
||||
)
|
||||
pluginStates.forEach { (packageName, state) ->
|
||||
item(key = packageName) {
|
||||
var showDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
showDialog = false
|
||||
}
|
||||
|
||||
val packageInfo =
|
||||
remember(packageName) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
),
|
||||
trailingContent = { Text(packageInfo.versionName!!) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if (pluginStates.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.no_downloader_installed),
|
||||
stringResource(R.string.downloader_no_plugins_installed),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
@@ -247,7 +240,7 @@ fun DownloadsSettingsScreen(
|
||||
private fun TrustDialog(
|
||||
@StringRes title: Int,
|
||||
body: String,
|
||||
downloaderName: String,
|
||||
pluginName: String,
|
||||
signature: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
@@ -275,8 +268,8 @@ private fun TrustDialog(
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_trust_dialog_name,
|
||||
downloaderName
|
||||
R.string.downloader_plugin_trust_dialog_plugin,
|
||||
pluginName
|
||||
),
|
||||
)
|
||||
OutlinedCard(
|
||||
@@ -286,7 +279,7 @@ private fun TrustDialog(
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_trust_dialog_signature,
|
||||
R.string.downloader_plugin_trust_dialog_signature,
|
||||
signature.chunked(2).joinToString(" ")
|
||||
), modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
|
||||
@@ -105,14 +105,6 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
||||
patchBundleRepository.update(
|
||||
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||
showToast = true,
|
||||
force = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,7 +65,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
||||
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||
if (src !is RemotePatchBundle) return@launch
|
||||
|
||||
patchBundleRepository.update(src, showToast = true, force = true)
|
||||
patchBundleRepository.update(src, showToast = true)
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -14,12 +15,15 @@ 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.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
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
|
||||
@@ -30,7 +34,7 @@ import kotlinx.coroutines.launch
|
||||
class DashboardViewModel(
|
||||
private val app: Application,
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
private val downloaderRepository: DownloaderRepository,
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val networkInfo: NetworkInfo,
|
||||
val prefs: PreferencesManager,
|
||||
@@ -41,8 +45,8 @@ class DashboardViewModel(
|
||||
private val contentResolver: ContentResolver = app.contentResolver
|
||||
private val powerManager = app.getSystemService<PowerManager>()!!
|
||||
|
||||
val newDownloaderAvailable =
|
||||
downloaderRepository.newDownloaderPackageNames.map { it.isNotEmpty() }
|
||||
val newDownloaderPluginsAvailable =
|
||||
downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||
|
||||
/**
|
||||
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
||||
@@ -67,8 +71,8 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun ignoreNewDownloader() = viewModelScope.launch {
|
||||
downloaderRepository.acknowledgeAllNewDownloader()
|
||||
fun ignoreNewDownloaderPlugins() = viewModelScope.launch {
|
||||
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
||||
}
|
||||
|
||||
private suspend fun checkForManagerUpdates() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -7,7 +8,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.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.mutableStateSetOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,10 +19,10 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadsViewModel(
|
||||
private val downloadedAppRepository: DownloadedAppRepository,
|
||||
private val downloaderRepository: DownloaderRepository,
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||
val pm: PM
|
||||
) : ViewModel() {
|
||||
val downloaderStates = downloaderRepository.downloaderPackageStates
|
||||
val downloaderPluginStates = downloaderPluginRepository.pluginStates
|
||||
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
|
||||
downloadedApps.sortedWith(
|
||||
compareBy<DownloadedApp> {
|
||||
@@ -31,7 +32,7 @@ class DownloadsViewModel(
|
||||
}
|
||||
val appSelection = mutableStateSetOf<DownloadedApp>()
|
||||
|
||||
var isRefreshingDownloader by mutableStateOf(false)
|
||||
var isRefreshingPlugins by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun toggleApp(downloadedApp: DownloadedApp) {
|
||||
@@ -51,17 +52,17 @@ class DownloadsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshDownloader() = viewModelScope.launch {
|
||||
isRefreshingDownloader = true
|
||||
downloaderRepository.reload()
|
||||
isRefreshingDownloader = false
|
||||
fun refreshPlugins() = viewModelScope.launch {
|
||||
isRefreshingPlugins = true
|
||||
downloaderPluginRepository.reload()
|
||||
isRefreshingPlugins = false
|
||||
}
|
||||
|
||||
fun trustDownloader(packageName: String) = viewModelScope.launch {
|
||||
downloaderRepository.trustPackage(packageName)
|
||||
fun trustPlugin(packageName: String) = viewModelScope.launch {
|
||||
downloaderPluginRepository.trustPackage(packageName)
|
||||
}
|
||||
|
||||
fun revokeDownloaderTrust(packageName: String) = viewModelScope.launch {
|
||||
downloaderRepository.revokeTrustForPackage(packageName)
|
||||
fun revokePluginTrust(packageName: String) = viewModelScope.launch {
|
||||
downloaderPluginRepository.revokeTrustForPackage(packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -13,6 +19,7 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
@@ -23,8 +30,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
class InstalledAppInfoViewModel(
|
||||
packageName: String
|
||||
@@ -82,28 +87,51 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
fun uninstall() {
|
||||
val app = installedApp ?: return
|
||||
viewModelScope.launch {
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
val msg = result.failure.message.orEmpty()
|
||||
context.toast(
|
||||
this@InstalledAppInfoViewModel.context.getString(
|
||||
R.string.uninstall_app_fail,
|
||||
msg
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
}
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
||||
|
||||
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||
InstallType.MOUNT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(app.currentPackageName)
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val extraStatus =
|
||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||
val extraStatusMessage =
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
installedApp?.let {
|
||||
installedAppRepository.delete(it)
|
||||
}
|
||||
onBackClick()
|
||||
}
|
||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
it,
|
||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
@@ -14,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
@@ -29,35 +32,32 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.withState
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.asCode
|
||||
import app.revanced.manager.util.saveableVar
|
||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,23 +66,14 @@ import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.installer.getSession
|
||||
import ru.solrudev.ackpine.session.ProgressSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class PatcherViewModel(
|
||||
private val input: Patcher.ViewModelParams
|
||||
) : ViewModel(), KoinComponent, InstallerModel {
|
||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val pm: PM by inject()
|
||||
@@ -90,7 +81,6 @@ class PatcherViewModel(
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
private val selectedApp = input.selectedApp
|
||||
@@ -105,6 +95,7 @@ class PatcherViewModel(
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
private set
|
||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||
key = "packageInstallerStatus",
|
||||
stateSaver = autoSaver()
|
||||
@@ -113,7 +104,7 @@ class PatcherViewModel(
|
||||
}
|
||||
private set
|
||||
|
||||
var isInstalling by mutableStateOf(false)
|
||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
||||
private set
|
||||
|
||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||
@@ -132,18 +123,6 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This coroutine scope is used to await installations.
|
||||
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
|
||||
*/
|
||||
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* Holds the package name of the Apk we are trying to install.
|
||||
*/
|
||||
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
|
||||
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
|
||||
|
||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
|
||||
@@ -159,15 +138,35 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
||||
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
||||
private var completedPatchCount by savedStateHandle.saveable {
|
||||
// SavedStateHandle.saveable only supports the boxed version.
|
||||
@Suppress("AutoboxingStateCreation") mutableStateOf(
|
||||
0
|
||||
)
|
||||
}
|
||||
val patchesProgress get() = completedPatchCount to patchCount
|
||||
override var downloadProgress by savedStateHandle.saveable(
|
||||
key = "downloadProgress",
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<Pair<Long, Long?>?>(null)
|
||||
}
|
||||
private set
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(
|
||||
app,
|
||||
input.selectedApp
|
||||
).toMutableStateList()
|
||||
}
|
||||
private var currentStepIndex = 0
|
||||
|
||||
val progress by derivedStateOf {
|
||||
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||
val current = steps.count {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
} + completedPatchCount
|
||||
|
||||
val current = steps.count { it.state == State.COMPLETED }
|
||||
val total = steps.size
|
||||
val total = steps.size - 1 + patchCount
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
@@ -175,46 +174,67 @@ class PatcherViewModel(
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||
ParcelUuid(
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
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 downloader.name
|
||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
onDownloadProgress = {
|
||||
withContext(Dispatchers.Main) {
|
||||
downloadProgress = it
|
||||
}
|
||||
},
|
||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
try {
|
||||
// Wait for the dialog interaction.
|
||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||
currentActivityRequest = this to plugin.name
|
||||
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
} finally {
|
||||
launchedActivity = null
|
||||
}
|
||||
} finally {
|
||||
currentActivityRequest = null
|
||||
launchedActivity = null
|
||||
}
|
||||
} finally {
|
||||
currentActivityRequest = null
|
||||
}
|
||||
},
|
||||
onEvent = ::handleProgressEvent,
|
||||
)
|
||||
}
|
||||
},
|
||||
onProgress = { name, state, message ->
|
||||
viewModelScope.launch {
|
||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||
copy(
|
||||
name = name ?: this.name,
|
||||
state = state ?: this.state,
|
||||
message = message ?: this.message
|
||||
)
|
||||
}
|
||||
|
||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||
currentStepIndex++
|
||||
|
||||
steps[currentStepIndex] =
|
||||
steps[currentStepIndex].copy(state = State.RUNNING)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
val patcherSucceeded =
|
||||
@@ -226,26 +246,64 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
InstallService.APP_INSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
InstallService.EXTRA_INSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
installerSessionId?.uuid?.let { id ->
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
// The process was killed during installation. Await the session again.
|
||||
withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.getSession(id)
|
||||
}?.let {
|
||||
awaitInstallation(it)
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
installedAppRepository.addOrUpdate(
|
||||
installedPackageName!!,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
} else packageInstallerStatus = pmStatus
|
||||
|
||||
isInstalling = false
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
||||
packageInstallerStatus = pmStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
installerBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
},
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
@@ -255,6 +313,7 @@ class PatcherViewModel(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installerBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
@@ -268,37 +327,7 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
|
||||
val stepIndex = steps.indexOfFirst {
|
||||
event.stepId?.let { id -> id == it.id }
|
||||
?: (it.state == State.RUNNING || it.state == State.WAITING)
|
||||
}
|
||||
|
||||
if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
|
||||
when (event) {
|
||||
is ProgressEvent.Started -> withState(State.RUNNING)
|
||||
|
||||
is ProgressEvent.Progress -> withState(
|
||||
message = event.message ?: message,
|
||||
progress = event.current?.let { event.current to event.total } ?: progress
|
||||
)
|
||||
|
||||
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
|
||||
|
||||
is ProgressEvent.Failed -> {
|
||||
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
|
||||
withState(
|
||||
State.FAILED,
|
||||
message = event.error.stackTrace,
|
||||
progress = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
installerCoroutineScope.cancel()
|
||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
@@ -343,111 +372,50 @@ class PatcherViewModel(
|
||||
|
||||
fun open() = installedPackageName?.let(pm::launch)
|
||||
|
||||
private suspend fun startInstallation(file: File, packageName: String) {
|
||||
val session = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
installerPkgName = packageName
|
||||
}
|
||||
awaitInstallation(session)
|
||||
}
|
||||
|
||||
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||
Dispatchers.Main
|
||||
) {
|
||||
val result = installerCoroutineScope.async {
|
||||
try {
|
||||
installerSessionId = ParcelUuid(session.id)
|
||||
withContext(Dispatchers.IO) {
|
||||
session.await()
|
||||
}
|
||||
} finally {
|
||||
installerSessionId = null
|
||||
}
|
||||
}.await()
|
||||
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName = installerPkgName
|
||||
installedAppRepository.addOrUpdate(
|
||||
installerPkgName,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
var needsRootUninstall = false
|
||||
var pmInstallStarted = false
|
||||
try {
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
val currentPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||
isInstalling = true
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (
|
||||
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
|
||||
existingPackageInfo
|
||||
)
|
||||
) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
pm.installApp(listOf(outputFile))
|
||||
pmInstallStarted = true
|
||||
}
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||
}
|
||||
|
||||
InstallType.MOUNT -> {
|
||||
InstallType.MOUNT -> {
|
||||
try {
|
||||
val packageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
val label = with(pm) {
|
||||
currentPackageInfo.label()
|
||||
}
|
||||
|
||||
// Check for base APK, first check if the app is already installed
|
||||
if (existingPackageInfo == null) {
|
||||
// If the app is not installed, check if the output file is a base apk
|
||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||
// Exit if there is no base APK package
|
||||
packageInstallerStatus =
|
||||
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
packageInfo.label()
|
||||
}
|
||||
|
||||
val inputVersion = input.selectedApp.version
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
|
||||
needsRootUninstall = true
|
||||
// Install as root
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
@@ -458,7 +426,7 @@ class PatcherViewModel(
|
||||
)
|
||||
|
||||
installedAppRepository.addOrUpdate(
|
||||
currentPackageInfo.packageName,
|
||||
packageInfo.packageName,
|
||||
packageName,
|
||||
inputVersion,
|
||||
InstallType.MOUNT,
|
||||
@@ -470,20 +438,21 @@ class PatcherViewModel(
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
needsRootUninstall = false
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
} finally {
|
||||
isInstalling = false
|
||||
if (needsRootUninstall) {
|
||||
try {
|
||||
withContext(NonCancellable) {
|
||||
rootInstaller.uninstall(packageName)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
if (!pmInstallStarted) isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,27 +463,12 @@ class PatcherViewModel(
|
||||
|
||||
override fun reinstall() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
val pkgName = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(outputFile)?.packageName
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
when (val result = pm.uninstallPackage(pkgName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
return@launch
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
startInstallation(outputFile, pkgName)
|
||||
}
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,66 +487,34 @@ class PatcherViewModel(
|
||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||
}
|
||||
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedApp: SelectedApp,
|
||||
selectedPatches: PatchSelection
|
||||
): List<Step> = buildList {
|
||||
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
||||
add(
|
||||
Step(
|
||||
StepId.DownloadAPK,
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||
val needsDownload =
|
||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
||||
|
||||
add(
|
||||
return listOfNotNull(
|
||||
Step(
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING,
|
||||
state = State.RUNNING,
|
||||
progressKey = ProgressKey.DOWNLOAD,
|
||||
).takeIf { needsDownload },
|
||||
Step(
|
||||
StepId.LoadPatches,
|
||||
context.getString(R.string.patcher_step_load_patches),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
add(
|
||||
StepCategory.PREPARING,
|
||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
||||
),
|
||||
Step(
|
||||
StepId.ReadAPK,
|
||||
context.getString(R.string.patcher_step_unpack),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
add(
|
||||
),
|
||||
|
||||
Step(
|
||||
StepId.ExecutePatches,
|
||||
context.getString(R.string.execute_patches),
|
||||
StepCategory.PATCHING,
|
||||
hide = true
|
||||
)
|
||||
)
|
||||
StepCategory.PATCHING
|
||||
),
|
||||
|
||||
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||
add(
|
||||
Step(
|
||||
StepId.ExecutePatch(index),
|
||||
name,
|
||||
StepCategory.PATCHING
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
Step(
|
||||
StepId.WriteAPK,
|
||||
context.getString(R.string.patcher_step_write_patched),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
)
|
||||
add(
|
||||
Step(
|
||||
StepId.SignAPK,
|
||||
context.getString(R.string.patcher_step_sign_apk),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -22,19 +23,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.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
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.LoadedDownloader
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
@@ -59,7 +60,7 @@ import kotlinx.parcelize.Parcelize
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class SelectedAppInfoViewModel(
|
||||
input: SelectedApplicationInfo.ViewModelParams
|
||||
) : ViewModel(), KoinComponent {
|
||||
@@ -67,13 +68,13 @@ class SelectedAppInfoViewModel(
|
||||
private val bundleRepository: PatchBundleRepository = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val optionsRepository: PatchOptionsRepository = get()
|
||||
private val downloaderRepository: DownloaderRepository = get()
|
||||
private val pluginsRepository: DownloaderPluginRepository = 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 downloader = downloaderRepository.loadedDownloaderPackageFlow
|
||||
val plugins = pluginsRepository.loadedPluginsFlow
|
||||
val desiredVersion = input.app.version
|
||||
val packageName = input.app.packageName
|
||||
|
||||
@@ -128,6 +129,8 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
val state = mutableStateOf<Options>(emptyMap())
|
||||
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
@@ -138,7 +141,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
mutableStateOf(emptyMap())
|
||||
state
|
||||
}
|
||||
private set
|
||||
|
||||
@@ -146,6 +149,8 @@ class SelectedAppInfoViewModel(
|
||||
if (input.patches != null)
|
||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||
|
||||
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
||||
|
||||
// Try to get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||
@@ -155,20 +160,20 @@ class SelectedAppInfoViewModel(
|
||||
selectionState = SelectionState.Customized(previous)
|
||||
}
|
||||
|
||||
mutableStateOf(SelectionState.Default)
|
||||
selection
|
||||
}
|
||||
|
||||
var showSourceSelector by mutableStateOf(false)
|
||||
private set
|
||||
private var downloaderAction: Pair<LoadedDownloader, Job>? by mutableStateOf(null)
|
||||
val activeDownloaderAction get() = downloaderAction?.first?.packageName
|
||||
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
|
||||
val activePluginAction get() = pluginAction?.first?.packageName
|
||||
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
||||
private val launchActivityChannel = Channel<Intent>()
|
||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||
|
||||
val errorFlow = combine(downloader, snapshotFlow { selectedApp }) { downloaderList, app ->
|
||||
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
|
||||
when {
|
||||
app is SelectedApp.Search && downloaderList.isEmpty() -> Error.NoDownloader
|
||||
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -178,23 +183,23 @@ class SelectedAppInfoViewModel(
|
||||
showSourceSelector = true
|
||||
}
|
||||
|
||||
private fun cancelDownloaderAction() {
|
||||
downloaderAction?.second?.cancel()
|
||||
downloaderAction = null
|
||||
private fun cancelPluginAction() {
|
||||
pluginAction?.second?.cancel()
|
||||
pluginAction = null
|
||||
}
|
||||
|
||||
fun dismissSourceSelector() {
|
||||
cancelDownloaderAction()
|
||||
cancelPluginAction()
|
||||
showSourceSelector = false
|
||||
}
|
||||
|
||||
fun searchUsingDownloader(downloader: LoadedDownloader) {
|
||||
cancelDownloaderAction()
|
||||
downloaderAction = downloader to viewModelScope.launch {
|
||||
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
|
||||
cancelPluginAction()
|
||||
pluginAction = plugin to viewModelScope.launch {
|
||||
try {
|
||||
val scope = object : GetScope {
|
||||
override val hostPackageName = app.packageName
|
||||
override val downloaderPackageName = downloader.packageName
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent) =
|
||||
withContext(Dispatchers.Main) {
|
||||
if (launchedActivity != null) error("Previous activity has not finished")
|
||||
@@ -219,7 +224,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.get(scope, packageName, desiredVersion)
|
||||
plugin.get(scope, packageName, desiredVersion)
|
||||
}?.let { (data, version) ->
|
||||
if (desiredVersion != null && version != desiredVersion) {
|
||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
||||
@@ -228,7 +233,7 @@ class SelectedAppInfoViewModel(
|
||||
selectedApp = SelectedApp.Download(
|
||||
packageName,
|
||||
version,
|
||||
ParceledDownloaderData(downloader, data)
|
||||
ParceledDownloaderData(plugin, data)
|
||||
)
|
||||
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
|
||||
} catch (e: UserInteractionException.Activity) {
|
||||
@@ -239,13 +244,13 @@ class SelectedAppInfoViewModel(
|
||||
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
|
||||
Log.e(tag, "Downloader.get threw an exception", e)
|
||||
} finally {
|
||||
downloaderAction = null
|
||||
pluginAction = null
|
||||
dismissSourceSelector()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDownloaderActivityResult(result: ActivityResult) {
|
||||
fun handlePluginActivityResult(result: ActivityResult) {
|
||||
launchedActivity?.complete(result)
|
||||
}
|
||||
|
||||
@@ -306,8 +311,8 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
enum class Error(@param:StringRes val resourceId: Int) {
|
||||
NoDownloader(R.string.no_downloader_available)
|
||||
enum class Error(@StringRes val resourceId: Int) {
|
||||
NoPlugins(R.string.downloader_no_plugins_available)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -16,6 +21,8 @@ import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import io.ktor.client.plugins.onDownload
|
||||
@@ -24,14 +31,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
|
||||
class UpdateViewModel(
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
@@ -39,11 +39,10 @@ class UpdateViewModel(
|
||||
private val app: Application by inject()
|
||||
private val reVancedAPI: ReVancedAPI by inject()
|
||||
private val http: HttpService by inject()
|
||||
private val pm: PM by inject()
|
||||
private val networkInfo: NetworkInfo by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
// TODO: save state to handle process death.
|
||||
var downloadedSize by mutableLongStateOf(0L)
|
||||
private set
|
||||
var totalSize by mutableLongStateOf(0L)
|
||||
@@ -63,17 +62,14 @@ class UpdateViewModel(
|
||||
private set
|
||||
|
||||
private val location = fs.tempDir.resolve("updater.apk")
|
||||
private val job = viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +78,7 @@ class UpdateViewModel(
|
||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||
val release = releaseInfo!!
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
|
||||
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
||||
showInternetCheckDialog = true
|
||||
} else {
|
||||
state = State.DOWNLOADING
|
||||
@@ -102,36 +98,50 @@ class UpdateViewModel(
|
||||
|
||||
fun installUpdate() = viewModelScope.launch {
|
||||
state = State.INSTALLING
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}.await()
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||
else -> {
|
||||
val msg = failure.message.orEmpty()
|
||||
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||
installError = msg
|
||||
state = State.FAILED
|
||||
pm.installApp(listOf(location))
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let {
|
||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||
val extra =
|
||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
|
||||
when(pmStatus) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
else -> {
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
installError = extra
|
||||
state = State.FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installBroadcastReceiver)
|
||||
|
||||
job.cancel()
|
||||
location.delete()
|
||||
}
|
||||
|
||||
enum class State(@param:StringRes val title: Int) {
|
||||
enum class State(@StringRes val title: Int) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update),
|
||||
CAN_INSTALL(R.string.ready_to_install_update),
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
/**
|
||||
* Converts an Ackpine installation failure into a PM status code
|
||||
*/
|
||||
fun InstallFailure.asCode() = when (this) {
|
||||
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
|
||||
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
|
||||
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
|
||||
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an Ackpine uninstallation failure into a PM status code
|
||||
*/
|
||||
fun UninstallFailure.asCode() = when (this) {
|
||||
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
@@ -2,8 +2,11 @@ package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.PackageInfoFlags
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
@@ -13,6 +16,8 @@ import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -20,13 +25,10 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
import ru.solrudev.ackpine.uninstaller.createSession
|
||||
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
@@ -38,8 +40,7 @@ data class AppInfo(
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
class PM(
|
||||
private val app: Application,
|
||||
patchBundleRepository: PatchBundleRepository,
|
||||
private val uninstaller: PackageUninstaller
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -144,11 +145,17 @@ class PM(
|
||||
false
|
||||
)
|
||||
|
||||
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||
uninstaller.createSession(pkg) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
config()
|
||||
}.await()
|
||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
apks.forEach { apk -> session.writeApk(apk) }
|
||||
session.commit(app.installIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
||||
}
|
||||
|
||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||
@@ -157,4 +164,44 @@ class PM(
|
||||
}
|
||||
|
||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
setRequestUpdateOwnership(true)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -83,8 +82,6 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (error: Exception) {
|
||||
// You can only toast on the main thread.
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
|
||||
@@ -19,8 +19,8 @@ Second \"item\" text"</string>
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</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="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="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_notification">New downloader available. Click here to configure them.</string>
|
||||
<string name="new_downloader_plugins_notification">New downloader plugins 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 downloader available</string>
|
||||
<string name="app_source_dialog_option_auto_unavailable">No plugins 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 and downloaded apps</string>
|
||||
<string name="downloads_description">Downloader plugins and downloaded apps</string>
|
||||
<string name="import_export">Import & 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">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_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_settings_no_apps">No downloaded apps found.</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
@@ -217,10 +217,6 @@ 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>
|
||||
@@ -316,8 +312,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="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="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="already_patched">Already patched</string>
|
||||
|
||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
@@ -345,7 +341,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="downloader_activity_dialog_body">User interaction is required in order to proceed with this downloader.</string>
|
||||
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string>
|
||||
<string name="select_install_type">Select installation type</string>
|
||||
|
||||
<string name="patcher_step_group_preparing">Preparing</string>
|
||||
|
||||
@@ -3,4 +3,4 @@ android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.caching=true
|
||||
|
||||
@@ -37,7 +37,6 @@ kotlin-process = "1.5.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
binary-compatibility-validator = "0.17.0"
|
||||
semver-parser = "3.0.0"
|
||||
ackpine = "0.18.5"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@@ -134,10 +133,6 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
||||
# Semantic versioning parser
|
||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
|
||||
|
||||
# Ackpine
|
||||
ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
|
||||
ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
|
||||
Reference in New Issue
Block a user