Compare commits

...

13 Commits

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

### Bug Fixes

* allow updating patches on metered networks ([9d9a0e8](9d9a0e81db))
2026-01-06 21:45:09 +00:00
Ax333l
9d9a0e81db fix: allow updating patches on metered networks 2026-01-06 22:37:25 +01:00
semantic-release-bot
ffa42099e3 chore: Release v1.26.0-dev.16 [skip ci]
# 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](11dd6e4064))
2025-12-30 00:16:11 +00:00
Robert
11dd6e4064 feat: Show patches as individual steps in patcher screen (#2889)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-12-30 01:08:54 +01:00
semantic-release-bot
35fb59b31d chore: Release v1.26.0-dev.15 [skip ci]
# 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](18a4df9af9))
2025-12-29 22:49:24 +00:00
Ax333l
18a4df9af9 fix: install dialog getting stuck (#2900) 2025-12-29 23:42:14 +01:00
semantic-release-bot
bd69b45a69 chore: Release v1.26.0-dev.14 [skip ci]
# 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](0d26df03f4))
2025-12-28 18:20:22 +00:00
Robert
0d26df03f4 fix: Update selected patch count when SelectionState changes (#2896) 2025-12-28 19:13:00 +01:00
semantic-release-bot
c436a7a100 chore: Release v1.26.0-dev.13 [skip ci]
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)

### Features

* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](dbb6c01e89))
2025-12-17 20:05:47 +00:00
Ushie
dbb6c01e89 feat: Make patcher screen design more consistent with inspiration (#2805) 2025-12-17 22:58:02 +03:00
69 changed files with 1495 additions and 1349 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,38 @@
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
### Bug Fixes
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
### Features
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
### Bug Fixes
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
### Bug Fixes
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
### Features
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17) # app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)

View File

@@ -108,6 +108,10 @@ dependencies {
// Compose Icons // Compose Icons
implementation(libs.compose.icons.fontawesome) implementation(libs.compose.icons.fontawesome)
// Ackpine
implementation(libs.ackpine.core)
implementation(libs.ackpine.ktx)
} }
buildscript { buildscript {

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<permission <permission
android:name="app.revanced.manager.permission.PLUGIN_HOST" android:name="app.revanced.manager.permission.DOWNLOADER_HOST"
android:protectionLevel="signature" android:protectionLevel="signature"
android:label="@string/plugin_host_permission_label" android:label="@string/downloader_host_permission_label"
android:description="@string/plugin_host_permission_description" android:description="@string/downloader_host_permission_description"
/> />
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" /> <uses-permission android:name="app.revanced.manager.permission.DOWNLOADER_HOST" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -49,10 +49,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" /> <activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<service android:name=".service.InstallService" />
<service android:name=".service.UninstallService" />
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"
@@ -75,5 +72,15 @@
android:value="androidx.startup" android:value="androidx.startup"
tools:node="remove" /> tools:node="remove" />
</provider> </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> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,4 @@
// ProgressEventParcel.aidl
package app.revanced.manager.patcher;
parcelable ProgressEventParcel;

View File

@@ -1,11 +1,12 @@
// IPatcherEvents.aidl // IPatcherEvents.aidl
package app.revanced.manager.patcher.runtime.process; package app.revanced.manager.patcher.runtime.process;
import app.revanced.manager.patcher.ProgressEventParcel;
// Interface for sending events back to the main app process. // Interface for sending events back to the main app process.
oneway interface IPatcherEvents { oneway interface IPatcherEvents {
void log(String level, String msg); void log(String level, String msg);
void patchSucceeded(); void event(in ProgressEventParcel event);
void progress(String name, String state, String msg);
// The patching process has ended. The exceptionStackTrace is null if it finished successfully. // The patching process has ended. The exceptionStackTrace is null if it finished successfully.
void finished(String exceptionStackTrace); void finished(String exceptionStackTrace);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
package app.revanced.manager.di
import android.content.Context
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import ru.solrudev.ackpine.installer.PackageInstaller
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
val ackpineModule = module {
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
single {
provideInstaller(androidContext())
}
single {
provideUninstaller(androidContext())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
package app.revanced.manager.patcher
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class ProgressEvent : Parcelable {
abstract val stepId: StepId?
data class Started(override val stepId: StepId) : ProgressEvent()
data class Progress(
override val stepId: StepId,
val current: Long? = null,
val total: Long? = null,
val message: String? = null,
) : ProgressEvent()
data class Completed(
override val stepId: StepId,
) : ProgressEvent()
data class Failed(
override val stepId: StepId?,
val error: RemoteError,
) : ProgressEvent()
}
/**
* Parcelable wrapper for [ProgressEvent].
*
* Required because AIDL does not support sealed classes.
*/
@Parcelize
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
fun ProgressEventParcel.toEvent(): ProgressEvent = event
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
@Parcelize
sealed class StepId : Parcelable {
data object DownloadAPK : StepId()
data object LoadPatches : StepId()
data object ReadAPK : StepId()
data object ExecutePatches : StepId()
data class ExecutePatch(val index: Int) : StepId()
data object WriteAPK : StepId()
data object SignAPK : StepId()
}
@Parcelize
data class RemoteError(
val type: String,
val message: String?,
val stackTrace: String,
) : Parcelable
fun Exception.toRemoteError() = RemoteError(
type = this::class.java.name,
message = this.message,
stackTrace = this.stackTraceToString(),
)
inline fun <T> runStep(
stepId: StepId,
onEvent: (ProgressEvent) -> Unit,
block: () -> T,
): T = try {
onEvent(ProgressEvent.Started(stepId))
val value = block()
onEvent(ProgressEvent.Completed(stepId))
value
} catch (error: Exception) {
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
throw error
}

View File

@@ -1,10 +1,9 @@
package app.revanced.manager.patcher package app.revanced.manager.patcher
import android.content.Context
import app.revanced.library.ApkUtils.applyTo import app.revanced.library.ApkUtils.applyTo
import app.revanced.manager.R import app.revanced.manager.patcher.Session.Companion.component1
import app.revanced.manager.patcher.Session.Companion.component2
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.ui.model.State
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
@@ -22,15 +21,10 @@ class Session(
cacheDir: String, cacheDir: String,
frameworkDir: String, frameworkDir: String,
aaptPath: String, aaptPath: String,
private val androidContext: Context,
private val logger: Logger, private val logger: Logger,
private val input: File, private val input: File,
private val onPatchCompleted: suspend () -> Unit, private val onEvent: (ProgressEvent) -> Unit,
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
) : Closeable { ) : 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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher( private val patcher = Patcher(
PatcherConfig( PatcherConfig(
@@ -42,86 +36,68 @@ class Session(
) )
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) { 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) -> this().collect { (patch, exception) ->
if (patch !in selectedPatches) return@collect val index = selectedPatches.indexOf(patch)
if (index == -1) return@collect
if (exception != null) { if (exception != null) {
updateProgress( onEvent(
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name), ProgressEvent.Failed(
state = State.FAILED, StepId.ExecutePatch(index),
message = exception.stackTraceToString() exception.toRemoteError(),
)
) )
logger.error("${patch.name} failed:") logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString()) logger.error(exception.stackTraceToString())
throw exception throw exception
} }
nextPatchIndex++ onEvent(
ProgressEvent.Completed(
onPatchCompleted() StepId.ExecutePatch(index),
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
updateProgress(
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
) )
} )
logger.info("${patch.name} succeeded") 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) { suspend fun run(output: File, selectedPatches: PatchList) {
updateProgress(state = State.COMPLETED) // Unpacking runStep(StepId.ExecutePatches, onEvent) {
java.util.logging.Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
java.util.logging.Logger.getLogger("").apply { addHandler(logger.handler)
handlers.forEach {
it.close()
removeHandler(it)
} }
addHandler(logger.handler) with(patcher) {
logger.info("Merging integrations")
this += selectedPatches.toSet()
logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
}
} }
with(patcher) { runStep(StepId.WriteAPK, onEvent) {
logger.info("Merging integrations") logger.info("Writing patched files...")
this += selectedPatches.toSet() val result = patcher.get()
logger.info("Applying patches...") val patched = tempDir.resolve("result.apk")
applyPatchesVerbose(selectedPatches.sortedBy { it.name }) withContext(Dispatchers.IO) {
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
result.applyTo(patched)
logger.info("Patched apk saved to $patched")
withContext(Dispatchers.IO) {
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
} }
logger.info("Writing patched files...")
val result = patcher.get()
val patched = tempDir.resolve("result.apk")
withContext(Dispatchers.IO) {
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
result.applyTo(patched)
logger.info("Patched apk saved to $patched")
withContext(Dispatchers.IO) {
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
updateProgress(state = State.COMPLETED) // Saving
} }
override fun close() { override fun close() {

View File

@@ -1,11 +1,12 @@
package app.revanced.manager.patcher.runtime package app.revanced.manager.patcher.runtime
import android.content.Context import android.content.Context
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.patcher.runStep
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import java.io.File import java.io.File
@@ -13,7 +14,7 @@ import java.io.File
/** /**
* Simple [Runtime] implementation that runs the patcher using coroutines. * Simple [Runtime] implementation that runs the patcher using coroutines.
*/ */
class CoroutineRuntime(private val context: Context) : Runtime(context) { class CoroutineRuntime(context: Context) : Runtime(context) {
override suspend fun execute( override suspend fun execute(
inputFile: String, inputFile: String,
outputFile: String, outputFile: String,
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) { ) {
val selectedBundles = selectedPatches.keys val patchList = runStep(StepId.LoadPatches, onEvent) {
val bundles = bundles() val selectedBundles = selectedPatches.keys
val uids = bundles.entries.associate { (key, value) -> value to key } val bundles = bundles()
val uids = bundles.entries.associate { (key, value) -> value to key }
val allPatches = val allPatches =
PatchBundle.Loader.patches(bundles.values, packageName) PatchBundle.Loader.patches(bundles.values, packageName)
.mapKeys { (b, _) -> uids[b]!! } .mapKeys { (b, _) -> uids[b]!! }
.filterKeys { it in selectedBundles } .filterKeys { it in selectedBundles }
val patchList = selectedPatches.flatMap { (bundle, selected) -> val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { it.name in selected } allPatches[bundle]?.filter { it.name in selected }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist") ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
} }
// Set all patch options. // Set all patch options.
options.forEach { (bundle, bundlePatchOptions) -> options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach val patches = allPatches[bundle] ?: return@forEach
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
val patchOptions = patches.single { it.name == patchName }.options val patchOptions = patches.single { it.name == patchName }.options
configuredPatchOptions.forEach { (key, value) -> configuredPatchOptions.forEach { (key, value) ->
patchOptions[key] = value patchOptions[key] = value
}
} }
} }
patchList
} }
onProgress(null, State.COMPLETED, null) // Loading patches val session = runStep(StepId.ReadAPK, onEvent) {
Session(
cacheDir,
frameworkPath,
aaptPath,
logger,
File(inputFile),
onEvent,
)
}
Session( session.use { s ->
cacheDir, s.run(
frameworkPath,
aaptPath,
context,
logger,
File(inputFile),
onPatchCompleted = onPatchCompleted,
onProgress
).use { session ->
session.run(
File(outputFile), File(outputFile),
patchList patchList
) )

View File

@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.runtime.process.IPatcherEvents import app.revanced.manager.patcher.runtime.process.IPatcherEvents
import app.revanced.manager.patcher.runtime.process.IPatcherProcess import app.revanced.manager.patcher.runtime.process.IPatcherProcess
import app.revanced.manager.patcher.LibraryResolver 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.logger.Logger
import app.revanced.manager.patcher.runtime.process.Parameters import app.revanced.manager.patcher.runtime.process.Parameters
import app.revanced.manager.patcher.runtime.process.PatchConfiguration import app.revanced.manager.patcher.runtime.process.PatchConfiguration
import app.revanced.manager.patcher.runtime.process.PatcherProcess import app.revanced.manager.patcher.runtime.process.PatcherProcess
import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.patcher.toEvent
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) = coroutineScope { ) = coroutineScope {
// Get the location of our own Apk. // Get the location of our own Apk.
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
} }
val patching = CompletableDeferred<Unit>() val patching = CompletableDeferred<Unit>()
val scope = this
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val binder = awaitBinderConnection() val binder = awaitBinderConnection()
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
val eventHandler = object : IPatcherEvents.Stub() { val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
override fun patchSucceeded() { override fun event(event: ProgressEventParcel?) {
scope.launch { onPatchCompleted() } event?.let { onEvent(it.toEvent()) }
} }
override fun progress(name: String?, state: String?, msg: String?) =
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
override fun finished(exceptionStackTrace: String?) { override fun finished(exceptionStackTrace: String?) {
binder.exit() binder.exit()

View File

@@ -4,9 +4,9 @@ import android.content.Context
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository 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.aapt.Aapt
import app.revanced.manager.patcher.logger.Logger 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.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) )
} }

View File

@@ -8,12 +8,15 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
import app.revanced.manager.BuildConfig import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session 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.LogLevel
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle 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.runtime.ProcessRuntime
import app.revanced.manager.ui.model.State import app.revanced.manager.patcher.toParcel
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
/** /**
* The main class that runs inside the runner process launched by [ProcessRuntime]. * The main class that runs inside the runner process launched by [ProcessRuntime].
*/ */
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { class PatcherProcess() : IPatcherProcess.Stub() {
private var eventBinder: IPatcherEvents? = null private var eventBinder: IPatcherEvents? = null
private val scope = private val scope =
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
override fun exit() = exitProcess(0) override fun exit() = exitProcess(0)
override fun start(parameters: Parameters, events: IPatcherEvents) { override fun start(parameters: Parameters, events: IPatcherEvents) {
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
eventBinder = events eventBinder = events
scope.launch { scope.launch {
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName) val patchList = runStep(StepId.LoadPatches, ::onEvent) {
val patchList = parameters.configurations.flatMap { config -> val allPatches = PatchBundle.Loader.patches(
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList()) parameters.configurations.map { it.bundle },
parameters.packageName
)
parameters.configurations.flatMap { config ->
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
.filter { it.name in config.patches } .filter { it.name in config.patches }
.associateBy { it.name } .associateBy { it.name }
config.options.forEach { (patchName, opts) -> config.options.forEach { (patchName, opts) ->
val patchOptions = patches[patchName]?.options val patchOptions = patches[patchName]?.options
?: throw Exception("Patch with name $patchName does not exist.") ?: throw Exception("Patch with name $patchName does not exist.")
opts.forEach { (key, value) -> opts.forEach { (key, value) ->
patchOptions[key] = value patchOptions[key] = value
}
} }
}
patches.values patches.values
}
} }
events.progress(null, State.COMPLETED.name, null) // Loading patches val session = runStep(StepId.ReadAPK, ::onEvent) {
Session(
cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir,
logger = logger,
input = File(parameters.inputFile),
onEvent = ::onEvent,
)
}
Session( session.use {
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) it.run(File(parameters.outputFile), patchList)
} }
@@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
} }
} }
val ipcInterface = PatcherProcess(appContext) val ipcInterface = PatcherProcess()
appContext.sendBroadcast(Intent().apply { appContext.sendBroadcast(Intent().apply {
action = ProcessRuntime.CONNECT_TO_APP_ACTION action = ProcessRuntime.CONNECT_TO_APP_ACTION

View File

@@ -24,19 +24,22 @@ import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger 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.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.patcher.toRemoteError
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.downloader.GetScope
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp 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.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@@ -48,9 +51,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit @OptIn(DownloaderHostApi::class)
@OptIn(PluginHostApi::class)
class PatcherWorker( class PatcherWorker(
context: Context, context: Context,
parameters: WorkerParameters parameters: WorkerParameters
@@ -58,7 +59,7 @@ class PatcherWorker(
private val workerRepository: WorkerRepository by inject() private val workerRepository: WorkerRepository by inject()
private val prefs: PreferencesManager by inject() private val prefs: PreferencesManager by inject()
private val keystoreManager: KeystoreManager by inject() private val keystoreManager: KeystoreManager by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject() private val downloaderRepository: DownloaderRepository by inject()
private val downloadedAppRepository: DownloadedAppRepository by inject() private val downloadedAppRepository: DownloadedAppRepository by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
@@ -71,11 +72,9 @@ class PatcherWorker(
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
val logger: Logger, val logger: Logger,
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit, val handleStartActivityRequest: suspend (LoadedDownloader, Intent) -> ActivityResult,
val onPatchCompleted: suspend () -> Unit,
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: suspend (File) -> Unit, val setInputFile: suspend (File) -> Unit,
val onProgress: ProgressEventHandler val onEvent: (ProgressEvent) -> Unit,
) { ) {
val packageName get() = input.packageName val packageName get() = input.packageName
} }
@@ -140,10 +139,6 @@ class PatcherWorker(
} }
private suspend fun runPatcher(args: Args): Result { 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") val patchedApk = fs.tempDir.resolve("patched.apk")
return try { return try {
@@ -155,59 +150,73 @@ class PatcherWorker(
} }
} }
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) = suspend fun download(downloader: LoadedDownloader, data: Parcelable) =
downloadedAppRepository.download( downloadedAppRepository.download(
plugin, downloader,
data, data,
args.packageName, args.packageName,
args.input.version, args.input.version,
prefs.suggestedVersionSafeguard.get(), prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(), !prefs.disablePatchVersionCompatCheck.get(),
onDownload = args.onDownloadProgress onDownload = { progress ->
).also { args.onEvent(
args.setInputFile(it) ProgressEvent.Progress(
updateProgress(state = State.COMPLETED) // Download APK stepId = StepId.DownloadAPK,
} current = progress.first,
total = progress.second
)
)
}
).also { args.setInputFile(it) }
val inputFile = when (val selectedApp = args.input) { val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> { is SelectedApp.Download -> {
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data) runStep(StepId.DownloadAPK, args.onEvent) {
val (downloader, data) = downloaderRepository.unwrapParceledData(
selectedApp.data
)
download(plugin, data) download(downloader, data)
}
} }
is SelectedApp.Search -> { is SelectedApp.Search -> {
downloaderPluginRepository.loadedPluginsFlow.first() runStep(StepId.DownloadAPK, args.onEvent) {
.firstNotNullOfOrNull { plugin -> downloaderRepository.loadedDownloaderPackageFlow.first()
try { .firstNotNullOfOrNull { downloader ->
val getScope = object : GetScope { try {
override val pluginPackageName = plugin.packageName val getScope = object : GetScope {
override val hostPackageName = applicationContext.packageName override val downloaderPackageName = downloader.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? { override val hostPackageName =
val result = args.handleStartActivityRequest(plugin, intent) applicationContext.packageName
return when (result.resultCode) {
Activity.RESULT_OK -> result.data override suspend fun requestStartActivity(intent: Intent): Intent? {
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() val result =
else -> throw UserInteractionException.Activity.NotCompleted( args.handleStartActivityRequest(downloader, intent)
result.resultCode, return when (result.resultCode) {
result.data Activity.RESULT_OK -> result.data
) Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
} }
} }
} withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { downloader.get(
plugin.get( getScope,
getScope, selectedApp.packageName,
selectedApp.packageName, selectedApp.version
selectedApp.version )
) }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } } catch (e: UserInteractionException.Activity.NotCompleted) {
} catch (e: UserInteractionException.Activity.NotCompleted) { throw e
throw e } catch (_: UserInteractionException) {
} catch (_: UserInteractionException) { null
null }?.let { (data, _) -> download(downloader, data) }
}?.let { (data, _) -> download(plugin, data) } } ?: throw Exception("App is not available.")
} ?: throw Exception("App is not available.") }
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@@ -227,12 +236,12 @@ class PatcherWorker(
args.selectedPatches, args.selectedPatches,
args.options, args.options,
args.logger, args.logger,
args.onPatchCompleted, args.onEvent,
args.onProgress
) )
keystoreManager.sign(patchedApk, File(args.output)) runStep(StepId.SignAPK, args.onEvent) {
updateProgress(state = State.COMPLETED) // Signing keystoreManager.sign(patchedApk, File(args.output))
}
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
Result.success() Result.success()
@@ -241,11 +250,21 @@ class PatcherWorker(
tag, tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
) )
updateProgress(state = State.FAILED, message = e.originalStackTrace) args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure() Result.failure()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e) Log.e(tag, "An exception occurred while patching".logFmt(), e)
updateProgress(state = State.FAILED, message = e.stackTraceToString()) args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()

View File

@@ -1,53 +0,0 @@
package app.revanced.manager.service
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
@Suppress("DEPRECATION")
class InstallService : Service() {
override fun onStartCommand(
intent: Intent, flags: Int, startId: Int
): Int {
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}.apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
else -> {
sendBroadcast(Intent().apply {
action = APP_INSTALL_ACTION
`package` = packageName
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
})
}
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
}
}

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.ui.component package app.revanced.manager.ui.component
import android.annotation.SuppressLint
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
enum class DialogKind( enum class DialogKind(
val flag: Int, val flag: Int,
val title: Int, val title: Int,
@StringRes val contentStringResId: Int, @param:StringRes val contentStringResId: Int,
val icon: ImageVector = Icons.Outlined.ErrorOutline, val icon: ImageVector = Icons.Outlined.ErrorOutline,
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
val dismissButton: InstallerStatusDialogButton? = null, val dismissButton: InstallerStatusDialogButton? = null,
@@ -133,10 +134,8 @@ enum class DialogKind(
title = R.string.installation_storage_issue_dialog_title, title = R.string.installation_storage_issue_dialog_title,
contentStringResId = R.string.installation_storage_issue_description, contentStringResId = R.string.installation_storage_issue_description,
), ),
@RequiresApi(34)
FAILURE_TIMEOUT( FAILURE_TIMEOUT(
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT, flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
title = R.string.installation_timeout_dialog_title, title = R.string.installation_timeout_dialog_title,
contentStringResId = R.string.installation_timeout_description, contentStringResId = R.string.installation_timeout_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model -> confirmButton = installerStatusDialogButton(R.string.install_app) { model ->

View File

@@ -1,7 +1,7 @@
package app.revanced.manager.ui.component.patcher package app.revanced.manager.ui.component.patcher
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
@@ -21,6 +20,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LoadingIndicator 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.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider import app.revanced.manager.ui.model.Step
import java.util.Locale import java.util.Locale
import kotlin.math.floor import kotlin.math.floor
@@ -52,21 +50,10 @@ import kotlin.math.floor
fun Steps( fun Steps(
category: StepCategory, category: StepCategory,
steps: List<Step>, steps: List<Step>,
stepCount: Pair<Int, Int>? = null, isExpanded: Boolean = false,
stepProgressProvider: StepProgressProvider onExpand: () -> Unit,
onClick: () -> Unit
) { ) {
var expanded by rememberSaveable { mutableStateOf(true) }
val categoryColor by animateColorAsState(
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
label = "category"
)
val cardColor by animateColorAsState(
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
label = "card"
)
val state = remember(steps) { val state = remember(steps) {
when { when {
steps.all { it.state == State.COMPLETED } -> State.COMPLETED steps.all { it.state == State.COMPLETED } -> State.COMPLETED
@@ -76,62 +63,69 @@ fun Steps(
} }
} }
val filteredSteps = remember(steps) {
val failedCount = steps.count { it.state == State.FAILED }
steps.filter { step ->
// Show hidden steps if it's the only failed step.
!step.hide || (step.state == State.FAILED && failedCount == 1)
}
}
LaunchedEffect(state) {
if (state == State.RUNNING || state == State.FAILED)
onExpand()
}
Column( Column(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(16.dp)) .clip(MaterialTheme.shapes.large)
.fillMaxWidth() .fillMaxWidth()
.background(cardColor) .background(MaterialTheme.colorScheme.surfaceContainerLow)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(16.dp)) .clickable(true, onClick = onClick)
.clickable { expanded = !expanded } .fillMaxWidth()
.background(categoryColor) .padding(20.dp)
) { ) {
Row( StepIcon(state = state, size = 24.dp)
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(16.dp)
) {
StepIcon(state = state, size = 24.dp)
Text(stringResource(category.displayName)) Text(stringResource(category.displayName))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
val stepProgress = remember(stepCount, steps) { Text(
stepCount?.let { (current, total) -> "$current/$total" } text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}" style = MaterialTheme.typography.labelSmall
} )
Text( ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
text = stepProgress,
style = MaterialTheme.typography.labelSmall
)
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
}
} }
AnimatedVisibility(visible = expanded) { AnimatedVisibility(visible = isExpanded) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.background(MaterialTheme.colorScheme.background.copy(0.6f))
.fillMaxWidth()
.padding(top = 10.dp)
) { ) {
steps.forEach { step -> filteredSteps.forEachIndexed { index, step ->
val (progress, progressText) = when (step.progressKey) { val (progress, progressText) = step.progress?.let { (current, total) ->
null -> null if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> else null to "${current.megaBytes} MB"
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
else null to "${downloaded.megaBytes} MB"
}
} ?: (null to null) } ?: (null to null)
SubStep( SubStep(
name = step.name, name = step.title,
state = step.state, state = step.state,
message = step.message, message = step.message,
progress = progress, progress = progress,
progressText = progressText progressText = progressText,
isFirst = index == 0,
isLast = index == filteredSteps.lastIndex,
) )
} }
} }
@@ -145,7 +139,9 @@ fun SubStep(
state: State, state: State,
message: String? = null, message: String? = null,
progress: Float? = null, progress: Float? = null,
progressText: String? = null progressText: String? = null,
isFirst: Boolean = false,
isLast: Boolean = false,
) { ) {
var messageExpanded by rememberSaveable { mutableStateOf(true) } var messageExpanded by rememberSaveable { mutableStateOf(true) }
@@ -156,22 +152,22 @@ fun SubStep(
clickable { messageExpanded = !messageExpanded } clickable { messageExpanded = !messageExpanded }
else this else this
} }
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
.padding(horizontal = 20.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
Box( StepIcon(
modifier = Modifier.size(24.dp), size = 18.dp,
contentAlignment = Alignment.Center state = state,
) { progress = progress,
StepIcon(state, progress, size = 20.dp) )
}
Text( Text(
text = name, text = name,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.labelLarge,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, true), modifier = Modifier.weight(1f, true),
@@ -201,7 +197,7 @@ fun SubStep(
text = message.orEmpty(), text = message.orEmpty(),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
) )
} }
} }
@@ -211,40 +207,44 @@ fun SubStep(
fun StepIcon(state: State, progress: Float? = null, size: Dp) { fun StepIcon(state: State, progress: Float? = null, size: Dp) {
val strokeWidth = Dp(floor(size.value / 10) + 1) val strokeWidth = Dp(floor(size.value / 10) + 1)
when (state) { Crossfade(targetState = state, label = "State CrossFade") { state ->
State.COMPLETED -> Icon( when (state) {
Icons.Filled.CheckCircle, State.COMPLETED -> Icon(
contentDescription = stringResource(R.string.step_completed), Icons.Filled.CheckCircle,
tint = MaterialTheme.colorScheme.surfaceTint, contentDescription = stringResource(R.string.step_completed),
modifier = Modifier.size(size) tint = Color(0xFF59B463),
) modifier = Modifier.size(size)
State.FAILED -> Icon(
Icons.Filled.Cancel,
contentDescription = stringResource(R.string.step_failed),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(size)
)
State.WAITING -> Icon(
Icons.Outlined.Circle,
contentDescription = stringResource(R.string.step_waiting),
tint = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.size(size)
)
State.RUNNING ->
LoadingIndicator(
modifier = stringResource(R.string.step_running).let { description ->
Modifier
.size(size)
.semantics {
contentDescription = description
}
},
progress = { progress },
strokeWidth = strokeWidth
) )
State.FAILED -> Icon(
Icons.Filled.Cancel,
contentDescription = stringResource(R.string.step_failed),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(size)
)
State.WAITING -> Icon(
Icons.Outlined.Circle,
contentDescription = stringResource(R.string.step_waiting),
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
modifier = Modifier.size(size)
)
State.RUNNING -> {
LoadingIndicator(
modifier = stringResource(R.string.step_running).let { description ->
Modifier
.size(size)
.semantics {
contentDescription = description
}
},
progress = { progress },
strokeWidth = strokeWidth
)
}
}
} }
} }

View File

@@ -3,6 +3,7 @@ package app.revanced.manager.ui.model
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.StepId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) { enum class StepCategory(@StringRes val displayName: Int) {
@@ -15,19 +16,20 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED WAITING, RUNNING, FAILED, COMPLETED
} }
enum class ProgressKey {
DOWNLOAD
}
interface StepProgressProvider {
val downloadProgress: Pair<Long, Long?>?
}
@Parcelize @Parcelize
data class Step( data class Step(
val name: String, val id: StepId,
val title: String,
val category: StepCategory, val category: StepCategory,
val state: State = State.WAITING, val state: State = State.WAITING,
val message: String? = null, val message: String? = null,
val progressKey: ProgressKey? = null val progress: Pair<Long, Long?>? = null,
) : Parcelable 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)

View File

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

View File

@@ -87,7 +87,7 @@ fun PatcherScreen(
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
viewModel.steps.groupBy { it.category } viewModel.steps.groupBy { it.category }.toList()
} }
} }
@@ -148,7 +148,7 @@ fun PatcherScreen(
}, },
title = { Text(title) }, title = { Text(title) },
text = { text = {
Text(stringResource(R.string.plugin_activity_dialog_body)) Text(stringResource(R.string.downloader_activity_dialog_body))
} }
) )
} }
@@ -213,6 +213,12 @@ fun PatcherScreen(
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
) { ) {
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
val expandCategory: (StepCategory?) -> Unit = { category ->
expandedCategory = category
}
LinearProgressIndicator( LinearProgressIndicator(
progress = { viewModel.progress }, progress = { viewModel.progress },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -224,14 +230,17 @@ fun PatcherScreen(
contentPadding = PaddingValues(16.dp) contentPadding = PaddingValues(16.dp)
) { ) {
items( items(
items = steps.toList(), items = steps,
key = { it.first } key = { it.first }
) { (category, steps) -> ) { (category, steps) ->
Steps( Steps(
category = category, category = category,
steps = steps, steps = steps,
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null, isExpanded = expandedCategory == category,
stepProgressProvider = viewModel onExpand = { expandCategory(category) },
onClick = {
expandCategory(if (expandedCategory == category) null else category)
}
) )
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,11 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application 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.PackageInfo
import android.content.pm.PackageInstaller
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@@ -19,7 +13,6 @@ import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import ru.solrudev.ackpine.session.Session
import ru.solrudev.ackpine.uninstaller.UninstallFailure
class InstalledAppInfoViewModel( class InstalledAppInfoViewModel(
packageName: String packageName: String
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
fun uninstall() { fun uninstall() {
val app = installedApp ?: return val app = installedApp ?: return
when (app.installType) { viewModelScope.launch {
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName) when (app.installType) {
InstallType.DEFAULT -> {
InstallType.MOUNT -> viewModelScope.launch { when (val result = pm.uninstallPackage(app.currentPackageName)) {
rootInstaller.uninstall(app.currentPackageName) is Session.State.Failed<UninstallFailure> -> {
installedAppRepository.delete(app) val msg = result.failure.message.orEmpty()
onBackClick() context.toast(
} this@InstalledAppInfoViewModel.context.getString(
} R.string.uninstall_app_fail,
} msg
)
private val uninstallBroadcastReceiver = object : BroadcastReceiver() { )
override fun onReceive(context: Context?, intent: Intent?) { return@launch
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) { Session.State.Succeeded -> {}
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() { InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
super.onCleared() }
context.unregisterReceiver(uninstallBroadcastReceiver) installedAppRepository.delete(app)
onBackClick()
}
} }
} }

View File

@@ -1,11 +1,9 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.pm.PackageInstaller as AndroidPackageInstaller
import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
@@ -32,32 +29,35 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository 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.LogLevel
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.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.InstallerModel
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.withState
import app.revanced.manager.util.PM 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.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver 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.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -66,14 +66,23 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.component.inject 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.io.File
import java.nio.file.Files import java.nio.file.Files
import java.time.Duration import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Patcher.ViewModelParams private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { ) : ViewModel(), KoinComponent, InstallerModel {
private val app: Application by inject() private val app: Application by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
private val pm: PM by inject() private val pm: PM by inject()
@@ -81,6 +90,7 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val ackpineInstaller: PackageInstaller = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
private val selectedApp = input.selectedApp private val selectedApp = input.selectedApp
@@ -95,7 +105,6 @@ class PatcherViewModel(
mutableStateOf<String?>(null) mutableStateOf<String?>(null)
} }
private set private set
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
var packageInstallerStatus: Int? by savedStateHandle.saveable( var packageInstallerStatus: Int? by savedStateHandle.saveable(
key = "packageInstallerStatus", key = "packageInstallerStatus",
stateSaver = autoSaver() stateSaver = autoSaver()
@@ -104,7 +113,7 @@ class PatcherViewModel(
} }
private set private set
var isInstalling by mutableStateOf(ongoingPmSession) var isInstalling by mutableStateOf(false)
private set private set
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf( private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
@@ -123,6 +132,18 @@ class PatcherViewModel(
} }
} }
/**
* This coroutine scope is used to await installations.
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
*/
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
/**
* Holds the package name of the Apk we are trying to install.
*/
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
private var inputFile: File? by savedStateHandle.saveableVar() private var inputFile: File? by savedStateHandle.saveableVar()
private val outputFile = tempDir.resolve("output.apk") private val outputFile = tempDir.resolve("output.apk")
@@ -138,35 +159,15 @@ class PatcherViewModel(
} }
} }
private val patchCount = input.selectedPatches.values.sumOf { it.size }
private var completedPatchCount by savedStateHandle.saveable {
// SavedStateHandle.saveable only supports the boxed version.
@Suppress("AutoboxingStateCreation") mutableStateOf(
0
)
}
val patchesProgress get() = completedPatchCount to patchCount
override var downloadProgress by savedStateHandle.saveable(
key = "downloadProgress",
stateSaver = autoSaver()
) {
mutableStateOf<Pair<Long, Long?>?>(null)
}
private set
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
generateSteps( generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
app,
input.selectedApp
).toMutableStateList()
} }
private var currentStepIndex = 0
val progress by derivedStateOf { val progress by derivedStateOf {
val current = steps.count { val steps = steps.filter { it.id != StepId.ExecutePatches }
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + completedPatchCount
val total = steps.size - 1 + patchCount val current = steps.count { it.state == State.COMPLETED }
val total = steps.size
current.toFloat() / total.toFloat() current.toFloat() / total.toFloat()
} }
@@ -174,67 +175,46 @@ class PatcherViewModel(
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> { private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( ParcelUuid(
"patching", PatcherWorker.Args( workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
input.selectedApp, "patching", PatcherWorker.Args(
outputFile.path, input.selectedApp,
input.selectedPatches, outputFile.path,
input.options, input.selectedPatches,
logger, input.options,
onDownloadProgress = { logger,
withContext(Dispatchers.Main) { setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
downloadProgress = it handleStartActivityRequest = { downloader, intent ->
} withContext(Dispatchers.Main) {
}, if (currentActivityRequest != null) throw Exception("Another request is already pending.")
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 { try {
with(CompletableDeferred<ActivityResult>()) { // Wait for the dialog interaction.
launchedActivity = this val accepted = with(CompletableDeferred<Boolean>()) {
launchActivityChannel.send(intent) currentActivityRequest = this to downloader.name
await() 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 { } finally {
launchedActivity = null currentActivityRequest = 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 = val patcherSucceeded =
@@ -246,64 +226,26 @@ class PatcherViewModel(
} }
} }
private val installerBroadcastReceiver = object : BroadcastReceiver() { init {
override fun onReceive(context: Context?, intent: Intent?) { // TODO: detect system-initiated process death during the patching process.
when (intent?.action) {
InstallService.APP_INSTALL_ACTION -> {
val pmStatus = intent.getIntExtra(
InstallService.EXTRA_INSTALL_STATUS,
PackageInstaller.STATUS_FAILURE
)
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) installerSessionId?.uuid?.let { id ->
?.let(logger::trace) viewModelScope.launch {
try {
if (pmStatus == PackageInstaller.STATUS_SUCCESS) { isInstalling = true
app.toast(app.getString(R.string.install_app_success)) uiSafe(app, R.string.install_app_fail, "Failed to install") {
installedPackageName = // The process was killed during installation. Await the session again.
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) withContext(Dispatchers.IO) {
viewModelScope.launch { ackpineInstaller.getSession(id)
installedAppRepository.addOrUpdate( }?.let {
installedPackageName!!, awaitInstallation(it)
packageName,
input.selectedApp.version
?: pm.getPackageInfo(outputFile)?.versionName!!,
InstallType.DEFAULT,
input.selectedPatches
)
} }
} else packageInstallerStatus = pmStatus }
} finally {
isInstalling = false 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 { viewModelScope.launch {
installedApp = installedAppRepository.get(packageName) installedApp = installedAppRepository.get(packageName)
@@ -313,7 +255,6 @@ class PatcherViewModel(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId.uuid) workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
@@ -327,7 +268,37 @@ class PatcherViewModel(
} }
} }
private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
val stepIndex = steps.indexOfFirst {
event.stepId?.let { id -> id == it.id }
?: (it.state == State.RUNNING || it.state == State.WAITING)
}
if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
when (event) {
is ProgressEvent.Started -> withState(State.RUNNING)
is ProgressEvent.Progress -> withState(
message = event.message ?: message,
progress = event.current?.let { event.current to event.total } ?: progress
)
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
is ProgressEvent.Failed -> {
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
withState(
State.FAILED,
message = event.error.stackTrace,
progress = null
)
}
}
}
}
fun onBack() { fun onBack() {
installerCoroutineScope.cancel()
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. // tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
tempDir.deleteRecursively() tempDir.deleteRecursively()
} }
@@ -372,44 +343,93 @@ class PatcherViewModel(
fun open() = installedPackageName?.let(pm::launch) fun open() = installedPackageName?.let(pm::launch)
fun install(installType: InstallType) = viewModelScope.launch { private suspend fun startInstallation(file: File, packageName: String) {
var pmInstallStarted = false val session = withContext(Dispatchers.IO) {
try { ackpineInstaller.createSession(Uri.fromFile(file)) {
isInstalling = true confirmation = Confirmation.IMMEDIATE
}
}
withContext(Dispatchers.Main) {
installerPkgName = packageName
}
awaitInstallation(session)
}
val currentPackageInfo = pm.getPackageInfo(outputFile) private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
?: throw Exception("Failed to load application info") Dispatchers.Main
) {
// If the app is currently installed val result = installerCoroutineScope.async {
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName) try {
if (existingPackageInfo != null) { installerSessionId = ParcelUuid(session.id)
// Check if the app version is less than the installed version withContext(Dispatchers.IO) {
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) { session.await()
// Exit if the selected app version is less than the installed version
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
} }
} finally {
installerSessionId = null
}
}.await()
when (result) {
is Session.State.Failed<InstallFailure> -> {
result.failure.message?.let(logger::trace)
packageInstallerStatus = result.failure.asCode()
} }
when (installType) { Session.State.Succeeded -> {
InstallType.DEFAULT -> { app.toast(app.getString(R.string.install_app_success))
// Check if the app is mounted as root installedPackageName = installerPkgName
// If it is, unmount it first, silently installedAppRepository.addOrUpdate(
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { installerPkgName,
rootInstaller.unmount(packageName) packageName,
} input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT,
input.selectedPatches
)
}
}
}
// Install regularly fun install(installType: InstallType) = viewModelScope.launch {
pm.installApp(listOf(outputFile)) isInstalling = true
pmInstallStarted = true var needsRootUninstall = false
try {
uiSafe(app, R.string.install_app_fail, "Failed to install") {
val currentPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
?: throw Exception("Failed to load application info")
// If the app is currently installed
val existingPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
existingPackageInfo
)
) {
// Exit if the selected app version is less than the installed version
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
} }
InstallType.MOUNT -> { when (installType) {
try { InstallType.DEFAULT -> {
val packageInfo = pm.getPackageInfo(outputFile) // Check if the app is mounted as root
?: throw Exception("Failed to load application info") // If it is, unmount it first, silently
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
rootInstaller.unmount(packageName)
}
// Install regularly
startInstallation(outputFile, currentPackageInfo.packageName)
}
InstallType.MOUNT -> {
val label = with(pm) { val label = with(pm) {
packageInfo.label() currentPackageInfo.label()
} }
// Check for base APK, first check if the app is already installed // Check for base APK, first check if the app is already installed
@@ -417,15 +437,17 @@ class PatcherViewModel(
// If the app is not installed, check if the output file is a base apk // If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) { if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package // Exit if there is no base APK package
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID packageInstallerStatus =
AndroidPackageInstaller.STATUS_FAILURE_INVALID
return@launch return@launch
} }
} }
val inputVersion = input.selectedApp.version val inputVersion = input.selectedApp.version
?: inputFile?.let(pm::getPackageInfo)?.versionName ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
?: throw Exception("Failed to determine input APK version") ?: throw Exception("Failed to determine input APK version")
needsRootUninstall = true
// Install as root // Install as root
rootInstaller.install( rootInstaller.install(
outputFile, outputFile,
@@ -436,7 +458,7 @@ class PatcherViewModel(
) )
installedAppRepository.addOrUpdate( installedAppRepository.addOrUpdate(
packageInfo.packageName, currentPackageInfo.packageName,
packageName, packageName,
inputVersion, inputVersion,
InstallType.MOUNT, InstallType.MOUNT,
@@ -448,21 +470,20 @@ class PatcherViewModel(
installedPackageName = packageName installedPackageName = packageName
app.toast(app.getString(R.string.install_app_success)) app.toast(app.getString(R.string.install_app_success))
} catch (e: Exception) { needsRootUninstall = false
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 { } finally {
if (!pmInstallStarted) isInstalling = false isInstalling = false
if (needsRootUninstall) {
try {
withContext(NonCancellable) {
rootInstaller.uninstall(packageName)
}
} catch (_: Exception) {
}
}
} }
} }
@@ -473,12 +494,27 @@ class PatcherViewModel(
override fun reinstall() { override fun reinstall() {
viewModelScope.launch { viewModelScope.launch {
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { try {
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
?: throw Exception("Failed to load application info")
pm.installApp(listOf(outputFile))
isInstalling = true isInstalling = true
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
val pkgName = withContext(Dispatchers.IO) {
pm.getPackageInfo(outputFile)?.packageName
?: throw Exception("Failed to load application info")
}
when (val result = pm.uninstallPackage(pkgName)) {
is Session.State.Failed<UninstallFailure> -> {
result.failure.message?.let(logger::trace)
packageInstallerStatus = result.failure.asCode()
return@launch
}
Session.State.Succeeded -> {}
}
startInstallation(outputFile, pkgName)
}
} finally {
isInstalling = false
} }
} }
} }
@@ -497,34 +533,66 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg) LogLevel.ERROR -> Log.e(TAG, msg)
} }
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> { fun generateSteps(
val needsDownload = context: Context,
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search selectedApp: SelectedApp,
selectedPatches: PatchSelection
): List<Step> = buildList {
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
add(
Step(
StepId.DownloadAPK,
context.getString(R.string.download_apk),
StepCategory.PREPARING
)
)
return listOfNotNull( add(
Step(
context.getString(R.string.download_apk),
StepCategory.PREPARING,
state = State.RUNNING,
progressKey = ProgressKey.DOWNLOAD,
).takeIf { needsDownload },
Step( Step(
StepId.LoadPatches,
context.getString(R.string.patcher_step_load_patches), context.getString(R.string.patcher_step_load_patches),
StepCategory.PREPARING, StepCategory.PREPARING
state = if (needsDownload) State.WAITING else State.RUNNING, )
), )
add(
Step( Step(
StepId.ReadAPK,
context.getString(R.string.patcher_step_unpack), context.getString(R.string.patcher_step_unpack),
StepCategory.PREPARING StepCategory.PREPARING
), )
)
add(
Step( Step(
StepId.ExecutePatches,
context.getString(R.string.execute_patches), context.getString(R.string.execute_patches),
StepCategory.PATCHING StepCategory.PATCHING,
), hide = true
)
)
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING), selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING) 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
)
) )
} }
} }

View File

@@ -8,7 +8,6 @@ import android.os.Parcelable
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -23,19 +22,19 @@ import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
@@ -60,7 +59,7 @@ import kotlinx.parcelize.Parcelize
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class SelectedAppInfoViewModel( class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
@@ -68,13 +67,13 @@ class SelectedAppInfoViewModel(
private val bundleRepository: PatchBundleRepository = get() private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get() private val optionsRepository: PatchOptionsRepository = get()
private val pluginsRepository: DownloaderPluginRepository = get() private val downloaderRepository: DownloaderRepository = get()
private val installedAppRepository: InstalledAppRepository = get() private val installedAppRepository: InstalledAppRepository = get()
private val rootInstaller: RootInstaller = get() private val rootInstaller: RootInstaller = get()
private val pm: PM = get() private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get() val prefs: PreferencesManager = get()
val plugins = pluginsRepository.loadedPluginsFlow val downloader = downloaderRepository.loadedDownloaderPackageFlow
val desiredVersion = input.app.version val desiredVersion = input.app.version
val packageName = input.app.packageName val packageName = input.app.packageName
@@ -129,8 +128,6 @@ class SelectedAppInfoViewModel(
} }
var options: Options by savedStateHandle.saveable { var options: Options by savedStateHandle.saveable {
val state = mutableStateOf<Options>(emptyMap())
viewModelScope.launch { viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps. if (!persistConfiguration) return@launch // TODO: save options for patched apps.
val bundlePatches = bundleInfoFlow.first() val bundlePatches = bundleInfoFlow.first()
@@ -141,7 +138,7 @@ class SelectedAppInfoViewModel(
} }
} }
state mutableStateOf(emptyMap())
} }
private set private set
@@ -149,8 +146,6 @@ class SelectedAppInfoViewModel(
if (input.patches != null) if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches)) return@saveable mutableStateOf(SelectionState.Customized(input.patches))
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
// Try to get the previous selection if customization is enabled. // Try to get the previous selection if customization is enabled.
viewModelScope.launch { viewModelScope.launch {
if (!prefs.disableSelectionWarning.get()) return@launch if (!prefs.disableSelectionWarning.get()) return@launch
@@ -160,20 +155,20 @@ class SelectedAppInfoViewModel(
selectionState = SelectionState.Customized(previous) selectionState = SelectionState.Customized(previous)
} }
selection mutableStateOf(SelectionState.Default)
} }
var showSourceSelector by mutableStateOf(false) var showSourceSelector by mutableStateOf(false)
private set private set
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null) private var downloaderAction: Pair<LoadedDownloader, Job>? by mutableStateOf(null)
val activePluginAction get() = pluginAction?.first?.packageName val activeDownloaderAction get() = downloaderAction?.first?.packageName
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null) private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
private val launchActivityChannel = Channel<Intent>() private val launchActivityChannel = Channel<Intent>()
val launchActivityFlow = launchActivityChannel.receiveAsFlow() val launchActivityFlow = launchActivityChannel.receiveAsFlow()
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app -> val errorFlow = combine(downloader, snapshotFlow { selectedApp }) { downloaderList, app ->
when { when {
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins app is SelectedApp.Search && downloaderList.isEmpty() -> Error.NoDownloader
else -> null else -> null
} }
} }
@@ -183,23 +178,23 @@ class SelectedAppInfoViewModel(
showSourceSelector = true showSourceSelector = true
} }
private fun cancelPluginAction() { private fun cancelDownloaderAction() {
pluginAction?.second?.cancel() downloaderAction?.second?.cancel()
pluginAction = null downloaderAction = null
} }
fun dismissSourceSelector() { fun dismissSourceSelector() {
cancelPluginAction() cancelDownloaderAction()
showSourceSelector = false showSourceSelector = false
} }
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) { fun searchUsingDownloader(downloader: LoadedDownloader) {
cancelPluginAction() cancelDownloaderAction()
pluginAction = plugin to viewModelScope.launch { downloaderAction = downloader to viewModelScope.launch {
try { try {
val scope = object : GetScope { val scope = object : GetScope {
override val hostPackageName = app.packageName override val hostPackageName = app.packageName
override val pluginPackageName = plugin.packageName override val downloaderPackageName = downloader.packageName
override suspend fun requestStartActivity(intent: Intent) = override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished") if (launchedActivity != null) error("Previous activity has not finished")
@@ -224,7 +219,7 @@ class SelectedAppInfoViewModel(
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
plugin.get(scope, packageName, desiredVersion) downloader.get(scope, packageName, desiredVersion)
}?.let { (data, version) -> }?.let { (data, version) ->
if (desiredVersion != null && version != desiredVersion) { if (desiredVersion != null && version != desiredVersion) {
app.toast(app.getString(R.string.downloader_invalid_version)) app.toast(app.getString(R.string.downloader_invalid_version))
@@ -233,7 +228,7 @@ class SelectedAppInfoViewModel(
selectedApp = SelectedApp.Download( selectedApp = SelectedApp.Download(
packageName, packageName,
version, version,
ParceledDownloaderData(plugin, data) ParceledDownloaderData(downloader, data)
) )
} ?: app.toast(app.getString(R.string.downloader_app_not_found)) } ?: app.toast(app.getString(R.string.downloader_app_not_found))
} catch (e: UserInteractionException.Activity) { } catch (e: UserInteractionException.Activity) {
@@ -244,13 +239,13 @@ class SelectedAppInfoViewModel(
app.toast(app.getString(R.string.downloader_error, e.simpleMessage())) app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
Log.e(tag, "Downloader.get threw an exception", e) Log.e(tag, "Downloader.get threw an exception", e)
} finally { } finally {
pluginAction = null downloaderAction = null
dismissSourceSelector() dismissSourceSelector()
} }
} }
} }
fun handlePluginActivityResult(result: ActivityResult) { fun handleDownloaderActivityResult(result: ActivityResult) {
launchedActivity?.complete(result) launchedActivity?.complete(result)
} }
@@ -311,8 +306,8 @@ class SelectedAppInfoViewModel(
} }
} }
enum class Error(@StringRes val resourceId: Int) { enum class Error(@param:StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available) NoDownloader(R.string.no_downloader_available)
} }
private companion object { private companion object {

View File

@@ -1,18 +1,13 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver import android.net.Uri
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService 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.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload import io.ktor.client.plugins.onDownload
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject 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( class UpdateViewModel(
private val downloadOnScreenEntry: Boolean private val downloadOnScreenEntry: Boolean
@@ -39,10 +39,11 @@ class UpdateViewModel(
private val app: Application by inject() private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject() private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject() private val http: HttpService by inject()
private val pm: PM by inject()
private val networkInfo: NetworkInfo by inject() private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem 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) var downloadedSize by mutableLongStateOf(0L)
private set private set
var totalSize by mutableLongStateOf(0L) var totalSize by mutableLongStateOf(0L)
@@ -62,14 +63,17 @@ class UpdateViewModel(
private set private set
private val location = fs.tempDir.resolve("updater.apk") private val location = fs.tempDir.resolve("updater.apk")
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
if (downloadOnScreenEntry) { init {
downloadUpdate() viewModelScope.launch {
} else { uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
state = State.CAN_DOWNLOAD releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
if (downloadOnScreenEntry) {
downloadUpdate()
} else {
state = State.CAN_DOWNLOAD
}
} }
} }
} }
@@ -78,7 +82,7 @@ class UpdateViewModel(
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!! val release = releaseInfo!!
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) { if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
showInternetCheckDialog = true showInternetCheckDialog = true
} else { } else {
state = State.DOWNLOADING state = State.DOWNLOADING
@@ -98,50 +102,36 @@ class UpdateViewModel(
fun installUpdate() = viewModelScope.launch { fun installUpdate() = viewModelScope.launch {
state = State.INSTALLING state = State.INSTALLING
val result = withContext(Dispatchers.IO) {
ackpineInstaller.createSession(Uri.fromFile(location)) {
confirmation = Confirmation.IMMEDIATE
}.await()
}
pm.installApp(listOf(location)) when (result) {
} is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
is InstallFailure.Aborted -> state = State.CAN_INSTALL
private val installBroadcastReceiver = object : BroadcastReceiver() { else -> {
override fun onReceive(context: Context?, intent: Intent?) { val msg = failure.message.orEmpty()
intent?.let { app.toast(app.getString(R.string.install_app_fail, msg))
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) installError = msg
val extra = state = State.FAILED
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() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver)
job.cancel()
location.delete() location.delete()
} }
enum class State(@StringRes val title: Int) { enum class State(@param:StringRes val title: Int) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update), DOWNLOADING(R.string.downloading_manager_update),
CAN_INSTALL(R.string.ready_to_install_update), CAN_INSTALL(R.string.ready_to_install_update),

View File

@@ -0,0 +1,30 @@
package app.revanced.manager.util
import android.annotation.SuppressLint
import android.content.pm.PackageInstaller
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.uninstaller.UninstallFailure
/**
* Converts an Ackpine installation failure into a PM status code
*/
fun InstallFailure.asCode() = when (this) {
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
else -> PackageInstaller.STATUS_FAILURE
}
/**
* Converts an Ackpine uninstallation failure into a PM status code
*/
fun UninstallFailure.asCode() = when (this) {
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
else -> PackageInstaller.STATUS_FAILURE
}

View File

@@ -2,11 +2,8 @@ package app.revanced.manager.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
@@ -16,8 +13,6 @@ import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import app.revanced.manager.domain.repository.PatchBundleRepository 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize 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 import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
@Immutable @Immutable
@Parcelize @Parcelize
data class AppInfo( data class AppInfo(
@@ -40,7 +38,8 @@ data class AppInfo(
@SuppressLint("QueryPermissionsNeeded") @SuppressLint("QueryPermissionsNeeded")
class PM( class PM(
private val app: Application, private val app: Application,
patchBundleRepository: PatchBundleRepository patchBundleRepository: PatchBundleRepository,
private val uninstaller: PackageUninstaller
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@@ -145,17 +144,11 @@ class PM(
false false
) )
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) { suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
val packageInstaller = app.packageManager.packageInstaller uninstaller.createSession(pkg) {
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> confirmation = Confirmation.IMMEDIATE
apks.forEach { apk -> session.writeApk(apk) } config()
session.commit(app.installIntentSender) }.await()
}
}
fun uninstallPackage(pkg: String) {
val packageInstaller = app.packageManager.packageInstaller
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
} }
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
@@ -164,44 +157,4 @@ class PM(
} }
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls() 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
} }

View File

@@ -33,6 +33,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -82,6 +83,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) { inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
try { try {
block() block()
} catch (e: CancellationException) {
throw e
} catch (error: Exception) { } catch (error: Exception) {
// You can only toast on the main thread. // You can only toast on the main thread.
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {

View File

@@ -19,8 +19,8 @@ Second \"item\" text"</string>
<string name="cli">CLI</string> <string name="cli">CLI</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string> <string name="downloader_host_permission_label">ReVanced Manager downloader host</string>
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string> <string name="downloader_host_permission_description">Used to control access to ReVanced Manager downloader. Only ReVanced Manager has this.</string>
<string name="toast_copied_to_clipboard">Copied!</string> <string name="toast_copied_to_clipboard">Copied!</string>
<string name="copy_to_clipboard">Copy to clipboard</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="select_app">Select an app</string>
<string name="patches_count_selected">%1$d/%2$d selected</string> <string name="patches_count_selected">%1$d/%2$d selected</string>
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string> <string name="new_downloader_notification">New downloader available. Click here to configure them.</string>
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string> <string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
<string name="import_">Import</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_title">Select source</string>
<string name="app_source_dialog_option_auto">Auto</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_description">Use all available downloader to download the app</string>
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string> <string name="app_source_dialog_option_auto_unavailable">No downloader available</string>
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string> <string name="app_source_dialog_option_installed_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> <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">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string> <string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string> <string name="downloads">Downloads</string>
<string name="downloads_description">Downloader plugins and downloaded apps</string> <string name="downloads_description">Downloader and downloaded apps</string>
<string name="import_export">Import &amp; export</string> <string name="import_export">Import &amp; export</string>
<string name="import_export_description">Keystore, patch options and selection</string> <string name="import_export_description">Keystore, patch options and selection</string>
<string name="advanced">Advanced</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">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_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="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader_plugins">Plugins</string> <string name="downloader">Downloader</string>
<string name="downloader_plugin_state_trusted">Trusted</string> <string name="downloader_state_trusted">Trusted%s</string>
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string> <string name="downloader_state_failed">Failed to load. Click for more details</string>
<string name="downloader_plugin_state_untrusted">Untrusted</string> <string name="downloader_state_untrusted">Untrusted</string>
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string> <string name="downloader_trust_dialog_title">Trust downloader?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string> <string name="downloader_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_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_plugin_trust_dialog_signature">Signature:\n\n%s</string> <string name="downloader_trust_dialog_signature">Signature:\n\n%s</string>
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string> <string name="downloader_trust_dialog_name">Downloader:\n%s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string> <string name="downloader_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_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found.</string> <string name="downloader_settings_no_apps">No downloaded apps found.</string>
<string name="search_apps">Search apps…</string> <string name="search_apps">Search apps…</string>
@@ -217,6 +217,10 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</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="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string> <string name="process_runtime">Run Patcher in another process (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string> <string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
@@ -312,8 +316,8 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="downloader_invalid_version">Downloader did not fetch the correct version</string> <string name="downloader_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_app_not_found">Downloader did not find the app</string>
<string name="downloader_error">Downloader error: %s</string> <string name="downloader_error">Downloader error: %s</string>
<string name="downloader_no_plugins_installed">No downloader installed.</string> <string name="no_downloader_installed">No downloader installed.</string>
<string name="downloader_no_plugins_available">There are downloaders installed but none are trusted. Check your settings.</string> <string name="no_downloader_available">There are downloader installed but none are trusted. Check your settings.</string>
<string name="already_patched">Already patched</string> <string name="already_patched">Already patched</string>
<string name="patch_selector_sheet_filter_title">Filter</string> <string name="patch_selector_sheet_filter_title">Filter</string>
@@ -341,7 +345,7 @@ It is only compatible with the following version(s): %2$s"</string>
<string name="save_apk_success">APK Saved</string> <string name="save_apk_success">APK Saved</string>
<string name="sign_fail">Failed to sign APK: %s</string> <string name="sign_fail">Failed to sign APK: %s</string>
<string name="save_logs">Save logs</string> <string name="save_logs">Save logs</string>
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string> <string name="downloader_activity_dialog_body">User interaction is required in order to proceed with this downloader.</string>
<string name="select_install_type">Select installation type</string> <string name="select_install_type">Select installation type</string>
<string name="patcher_step_group_preparing">Preparing</string> <string name="patcher_step_group_preparing">Preparing</string>

View File

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

View File

@@ -37,6 +37,7 @@ kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3" hidden-api-stub = "4.3.3"
binary-compatibility-validator = "0.17.0" binary-compatibility-validator = "0.17.0"
semver-parser = "3.0.0" semver-parser = "3.0.0"
ackpine = "0.18.5"
[libraries] [libraries]
# AndroidX Core # AndroidX Core
@@ -133,6 +134,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
# Semantic versioning parser # Semantic versioning parser
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-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] [plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }