diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6b2286d..85858f6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -70,7 +70,8 @@ body: Before creating a new bug report, please keep the following in mind: - - **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-library/labels/Bug%20report). + - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-library/issues?q=label%3A%22Bug+report%22). + - **Review the contribution guidelines**: Make sure your bug request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-library/blob/main/CONTRIBUTING.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: @@ -100,7 +101,7 @@ body: label: Acknowledgements description: Your bug report will be closed if you don't follow the checklist below. options: - - label: This issue is not a duplicate of an existing bug report. + - label: I have checked all open and closed bug reports and this is not a duplicate. required: true - label: I have chosen an appropriate title. required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 6ec076b..b874069 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -70,8 +70,8 @@ body: Before creating a new feature request, please keep the following in mind: - - **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-library/labels/Feature%20request). - - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-library/blob/main/CONTRIBUTING.md). + - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-library/issues?q=label%3A%22Feature+request%22). + - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-library/blob/main/CONTRIBUTING.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: @@ -98,7 +98,7 @@ body: label: Acknowledgements description: Your feature request will be closed if you don't follow the checklist below. options: - - label: This issue is not a duplicate of an existing feature request. + - label: I have checked all open and closed feature requests and this is not a duplicate. required: true - label: I have chosen an appropriate title. required: true diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 250871b..cab9162 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -19,6 +19,9 @@ jobs: - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 + - name: Setup Java + run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV + - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cb2b20..b06d6b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 + - name: Setup Java + run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV + - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -42,7 +45,7 @@ jobs: with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - fingerprint: ${{ env.GPG_FINGERPRINT }} + fingerprint: ${{ vars.GPG_FINGERPRINT }} - name: Release env: diff --git a/.gitignore b/.gitignore index 8e77bd2..83f36ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,118 +1,11 @@ -### Java template -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -.idea/artifacts -.idea/compiler.xml -.idea/jarRepositories.xml -.idea/modules.xml -.idea/*.iml -.idea/modules *.iml -*.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ -.idea - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Gradle template .gradle -**/build/ -!src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Cache of project -.gradletasknamecache - -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - -# Dependency directories +.idea +.DS_Store +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata node_modules/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 623eef1..494767a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# [3.0.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v2.4.0-dev.1...v3.0.0-dev.1) (2024-08-06) + + +### Bug Fixes + +* Make functions internal which are supposed to be internal ([893d22d](https://github.com/ReVanced/revanced-library/commit/893d22d7938fa1c7544795635ed2ffacdd0cbf0d)) + + +### Build System + +* Refactor to DSL to bump ReVanced Patcher ([7f5d6da](https://github.com/ReVanced/revanced-library/commit/7f5d6dad7ba73e2ee53010241ba3204d04860a22)) + + +### Features + +* Remove deprecated functions ([b9bf3bc](https://github.com/ReVanced/revanced-library/commit/b9bf3bc88284c0381c7370c3606b662da2ef380d)) + + +### BREAKING CHANGES + +* Some functions have been removed. +* Some functions are not available anymore. +* The signature of some functions has changed. + +# [2.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v2.3.0...v2.4.0-dev.1) (2024-04-07) + + +### Features + +* Add local Android installer ([#25](https://github.com/ReVanced/revanced-library/issues/25)) ([43d655a](https://github.com/ReVanced/revanced-library/commit/43d655aea5d86288ae9916630e0f30de219d5cfb)) + # [2.3.0](https://github.com/ReVanced/revanced-library/compare/v2.2.1...v2.3.0) (2024-03-14) diff --git a/api/android/revanced-library.api b/api/android/revanced-library.api new file mode 100644 index 0000000..3deedb0 --- /dev/null +++ b/api/android/revanced-library.api @@ -0,0 +1,221 @@ +public final class app/revanced/library/ApkSigner { + public static final field INSTANCE Lapp/revanced/library/ApkSigner; + public final fun newApkSigner (Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; + public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; + public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; + public final fun readPrivateKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/library/ApkSigner$KeyStoreEntry { + public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; + public final fun getPrivateKeyCertificatePair ()Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair { + public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V + public final fun getCertificate ()Ljava/security/cert/X509Certificate; + public final fun getPrivateKey ()Ljava/security/PrivateKey; +} + +public final class app/revanced/library/ApkSigner$Signer { + public final fun signApk (Ljava/io/File;Ljava/io/File;)V +} + +public final class app/revanced/library/ApkUtils { + public static final field INSTANCE Lapp/revanced/library/ApkUtils; + public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)V + public final fun signApk (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V +} + +public final class app/revanced/library/ApkUtils$KeyStoreDetails { + public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getKeyStore ()Ljava/io/File; + public final fun getKeyStorePassword ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; +} + +public final class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails { + public fun ()V + public fun (Ljava/lang/String;Ljava/util/Date;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCommonName ()Ljava/lang/String; + public final fun getValidUntil ()Ljava/util/Date; +} + +public final class app/revanced/library/OptionsKt { + public static final fun setOptions (Ljava/util/Set;Ljava/util/Map;)V +} + +public final class app/revanced/library/PatchKt { + public static final fun mostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map; + public static synthetic fun mostCommonCompatibleVersions$default (Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; +} + +public final class app/revanced/library/SerializationKt { + public static final fun serializeTo (Ljava/util/Set;Ljava/io/OutputStream;Z)V + public static synthetic fun serializeTo$default (Ljava/util/Set;Ljava/io/OutputStream;ZILjava/lang/Object;)V +} + +public final class app/revanced/library/Utils { + public static final field INSTANCE Lapp/revanced/library/Utils; + public final fun isAndroidEnvironment ()Z +} + +public final class app/revanced/library/installation/command/AdbShellCommandRunner : app/revanced/library/installation/command/ShellCommandRunner { +} + +public abstract interface class app/revanced/library/installation/command/ILocalShellCommandRunnerRootService : android/os/IInterface { + public static final field DESCRIPTOR Ljava/lang/String; + public abstract fun getFileSystemService ()Landroid/os/IBinder; +} + +public class app/revanced/library/installation/command/ILocalShellCommandRunnerRootService$Default : app/revanced/library/installation/command/ILocalShellCommandRunnerRootService { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public fun getFileSystemService ()Landroid/os/IBinder; +} + +public abstract class app/revanced/library/installation/command/ILocalShellCommandRunnerRootService$Stub : android/os/Binder, app/revanced/library/installation/command/ILocalShellCommandRunnerRootService { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/library/installation/command/ILocalShellCommandRunnerRootService; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public final class app/revanced/library/installation/command/LocalShellCommandRunner : app/revanced/library/installation/command/ShellCommandRunner, android/content/ServiceConnection, java/io/Closeable { + public fun close ()V + public fun onServiceConnected (Landroid/content/ComponentName;Landroid/os/IBinder;)V + public fun onServiceDisconnected (Landroid/content/ComponentName;)V +} + +public abstract interface class app/revanced/library/installation/command/RunResult { + public abstract fun getError ()Ljava/lang/String; + public abstract fun getExitCode ()I + public abstract fun getOutput ()Ljava/lang/String; + public abstract fun waitFor ()V +} + +public final class app/revanced/library/installation/command/RunResult$DefaultImpls { + public static fun waitFor (Lapp/revanced/library/installation/command/RunResult;)V +} + +public abstract class app/revanced/library/installation/command/ShellCommandRunner { + protected final fun getLogger ()Ljava/util/logging/Logger; + protected abstract fun runCommand (Ljava/lang/String;)Lapp/revanced/library/installation/command/RunResult; +} + +public final class app/revanced/library/installation/installer/AdbInstaller : app/revanced/library/installation/installer/Installer { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class app/revanced/library/installation/installer/AdbInstallerResult { +} + +public final class app/revanced/library/installation/installer/AdbInstallerResult$Failure : app/revanced/library/installation/installer/AdbInstallerResult { + public final fun getException ()Ljava/lang/Exception; +} + +public final class app/revanced/library/installation/installer/AdbInstallerResult$Success : app/revanced/library/installation/installer/AdbInstallerResult { + public static final field INSTANCE Lapp/revanced/library/installation/installer/AdbInstallerResult$Success; +} + +public final class app/revanced/library/installation/installer/AdbRootInstaller : app/revanced/library/installation/installer/RootInstaller { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/library/installation/installer/DeviceNotFoundException : java/lang/Exception { + public fun ()V +} + +public class app/revanced/library/installation/installer/Installation { + public final fun getApkFilePath ()Ljava/lang/String; +} + +public abstract class app/revanced/library/installation/installer/Installer { + public abstract fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getLogger ()Ljava/util/logging/Logger; + public abstract fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/library/installation/installer/Installer$Apk { + public fun (Ljava/io/File;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFile ()Ljava/io/File; + public final fun getPackageName ()Ljava/lang/String; +} + +public final class app/revanced/library/installation/installer/LocalInstaller : app/revanced/library/installation/installer/Installer, java/io/Closeable { + public static final field Companion Lapp/revanced/library/installation/installer/LocalInstaller$Companion; + public fun (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)V + public fun close ()V + public fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/library/installation/installer/LocalInstaller$Companion { +} + +public final class app/revanced/library/installation/installer/LocalInstallerResult { + public final fun getExtra ()Ljava/lang/String; + public final fun getPackageName ()Ljava/lang/String; + public final fun getPmStatus ()I +} + +public final class app/revanced/library/installation/installer/LocalInstallerService : android/app/Service { + public fun ()V + public fun onBind (Landroid/content/Intent;)Landroid/os/IBinder; + public fun onStartCommand (Landroid/content/Intent;II)I +} + +public final class app/revanced/library/installation/installer/LocalRootInstaller : app/revanced/library/installation/installer/RootInstaller, java/io/Closeable { + public fun (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Landroid/content/Context;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V +} + +public final class app/revanced/library/installation/installer/RootInstallation : app/revanced/library/installation/installer/Installation { + public final fun getInstalledApkFilePath ()Ljava/lang/String; + public final fun getMounted ()Z +} + +public abstract class app/revanced/library/installation/installer/RootInstaller : app/revanced/library/installation/installer/Installer { + public fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getShellCommandRunner ()Lapp/revanced/library/installation/command/ShellCommandRunner; + public fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun invoke (Ljava/lang/String;)Lapp/revanced/library/installation/command/RunResult; + protected final fun move (Ljava/io/File;Ljava/lang/String;)V + public fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun write (Ljava/lang/String;Ljava/lang/String;)V +} + +public final class app/revanced/library/installation/installer/RootInstallerResult : java/lang/Enum { + public static final field FAILURE Lapp/revanced/library/installation/installer/RootInstallerResult; + public static final field SUCCESS Lapp/revanced/library/installation/installer/RootInstallerResult; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/library/installation/installer/RootInstallerResult; + public static fun values ()[Lapp/revanced/library/installation/installer/RootInstallerResult; +} + +public final class app/revanced/library/logging/Logger { + public static final field INSTANCE Lapp/revanced/library/logging/Logger; + public final fun addHandler (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V + public final fun removeAllHandlers ()V + public final fun setDefault ()V + public final fun setFormat (Ljava/lang/String;)V + public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V +} + diff --git a/api/jvm/revanced-library.api b/api/jvm/revanced-library.api new file mode 100644 index 0000000..81007f9 --- /dev/null +++ b/api/jvm/revanced-library.api @@ -0,0 +1,167 @@ +public final class app/revanced/library/ApkSigner { + public static final field INSTANCE Lapp/revanced/library/ApkSigner; + public final fun newApkSigner (Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; + public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; + public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; + public final fun readPrivateKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/library/ApkSigner$KeyStoreEntry { + public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; + public final fun getPrivateKeyCertificatePair ()Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair { + public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V + public final fun getCertificate ()Ljava/security/cert/X509Certificate; + public final fun getPrivateKey ()Ljava/security/PrivateKey; +} + +public final class app/revanced/library/ApkSigner$Signer { + public final fun signApk (Ljava/io/File;Ljava/io/File;)V +} + +public final class app/revanced/library/ApkUtils { + public static final field INSTANCE Lapp/revanced/library/ApkUtils; + public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)V + public final fun signApk (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V +} + +public final class app/revanced/library/ApkUtils$KeyStoreDetails { + public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getKeyStore ()Ljava/io/File; + public final fun getKeyStorePassword ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; +} + +public final class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails { + public fun ()V + public fun (Ljava/lang/String;Ljava/util/Date;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCommonName ()Ljava/lang/String; + public final fun getValidUntil ()Ljava/util/Date; +} + +public final class app/revanced/library/OptionsKt { + public static final fun setOptions (Ljava/util/Set;Ljava/util/Map;)V +} + +public final class app/revanced/library/PatchKt { + public static final fun mostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map; + public static synthetic fun mostCommonCompatibleVersions$default (Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; +} + +public final class app/revanced/library/SerializationKt { + public static final fun serializeTo (Ljava/util/Set;Ljava/io/OutputStream;Z)V + public static synthetic fun serializeTo$default (Ljava/util/Set;Ljava/io/OutputStream;ZILjava/lang/Object;)V +} + +public final class app/revanced/library/Utils { + public static final field INSTANCE Lapp/revanced/library/Utils; + public final fun isAndroidEnvironment ()Z +} + +public final class app/revanced/library/installation/command/AdbShellCommandRunner : app/revanced/library/installation/command/ShellCommandRunner { +} + +public abstract interface class app/revanced/library/installation/command/RunResult { + public abstract fun getError ()Ljava/lang/String; + public abstract fun getExitCode ()I + public abstract fun getOutput ()Ljava/lang/String; + public abstract fun waitFor ()V +} + +public final class app/revanced/library/installation/command/RunResult$DefaultImpls { + public static fun waitFor (Lapp/revanced/library/installation/command/RunResult;)V +} + +public abstract class app/revanced/library/installation/command/ShellCommandRunner { + protected final fun getLogger ()Ljava/util/logging/Logger; + protected abstract fun runCommand (Ljava/lang/String;)Lapp/revanced/library/installation/command/RunResult; +} + +public final class app/revanced/library/installation/installer/AdbInstaller : app/revanced/library/installation/installer/Installer { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class app/revanced/library/installation/installer/AdbInstallerResult { +} + +public final class app/revanced/library/installation/installer/AdbInstallerResult$Failure : app/revanced/library/installation/installer/AdbInstallerResult { + public final fun getException ()Ljava/lang/Exception; +} + +public final class app/revanced/library/installation/installer/AdbInstallerResult$Success : app/revanced/library/installation/installer/AdbInstallerResult { + public static final field INSTANCE Lapp/revanced/library/installation/installer/AdbInstallerResult$Success; +} + +public final class app/revanced/library/installation/installer/AdbRootInstaller : app/revanced/library/installation/installer/RootInstaller { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/library/installation/installer/DeviceNotFoundException : java/lang/Exception { + public fun ()V +} + +public class app/revanced/library/installation/installer/Installation { + public final fun getApkFilePath ()Ljava/lang/String; +} + +public abstract class app/revanced/library/installation/installer/Installer { + public abstract fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getLogger ()Ljava/util/logging/Logger; + public abstract fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/library/installation/installer/Installer$Apk { + public fun (Ljava/io/File;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFile ()Ljava/io/File; + public final fun getPackageName ()Ljava/lang/String; +} + +public final class app/revanced/library/installation/installer/RootInstallation : app/revanced/library/installation/installer/Installation { + public final fun getInstalledApkFilePath ()Ljava/lang/String; + public final fun getMounted ()Z +} + +public abstract class app/revanced/library/installation/installer/RootInstaller : app/revanced/library/installation/installer/Installer { + public fun getInstallation (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getShellCommandRunner ()Lapp/revanced/library/installation/command/ShellCommandRunner; + public fun install (Lapp/revanced/library/installation/installer/Installer$Apk;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun invoke (Ljava/lang/String;)Lapp/revanced/library/installation/command/RunResult; + protected final fun move (Ljava/io/File;Ljava/lang/String;)V + public fun uninstall (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun write (Ljava/lang/String;Ljava/lang/String;)V +} + +public final class app/revanced/library/installation/installer/RootInstallerResult : java/lang/Enum { + public static final field FAILURE Lapp/revanced/library/installation/installer/RootInstallerResult; + public static final field SUCCESS Lapp/revanced/library/installation/installer/RootInstallerResult; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/library/installation/installer/RootInstallerResult; + public static fun values ()[Lapp/revanced/library/installation/installer/RootInstallerResult; +} + +public final class app/revanced/library/logging/Logger { + public static final field INSTANCE Lapp/revanced/library/logging/Logger; + public final fun addHandler (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V + public final fun removeAllHandlers ()V + public final fun setDefault ()V + public final fun setFormat (Ljava/lang/String;)V + public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V +} + diff --git a/api/revanced-library.api b/api/revanced-library.api deleted file mode 100644 index 54f5e86..0000000 --- a/api/revanced-library.api +++ /dev/null @@ -1,191 +0,0 @@ -public final class app/revanced/library/ApkSigner { - public static final field INSTANCE Lapp/revanced/library/ApkSigner; - public final fun newApkSigner (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; - public final fun newApkSigner (Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; - public final fun newApkSigner (Ljava/lang/String;Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer; - public final fun newApkSigner (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer; - public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Set;)V - public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; - public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; - public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; - public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; - public final fun readPrivateKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; -} - -public final class app/revanced/library/ApkSigner$KeyStoreEntry { - public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V - public final fun getAlias ()Ljava/lang/String; - public final fun getPassword ()Ljava/lang/String; - public final fun getPrivateKeyCertificatePair ()Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; -} - -public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair { - public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V - public final fun getCertificate ()Ljava/security/cert/X509Certificate; - public final fun getPrivateKey ()Ljava/security/PrivateKey; -} - -public final class app/revanced/library/ApkSigner$Signer { - public final fun signApk (Lcom/android/tools/build/apkzlib/zip/ZFile;)V - public final fun signApk (Ljava/io/File;)V - public final fun signApk (Ljava/io/File;Ljava/io/File;)V -} - -public final class app/revanced/library/ApkUtils { - public static final field INSTANCE Lapp/revanced/library/ApkUtils; - public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)V - public final fun newPrivateKeyCertificatePair (Lapp/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; - public final fun readPrivateKeyCertificatePairFromKeyStore (Lapp/revanced/library/ApkUtils$KeyStoreDetails;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; - public final fun sign (Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V - public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V - public final fun sign (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V - public final fun signApk (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)V -} - -public final class app/revanced/library/ApkUtils$KeyStoreDetails { - public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAlias ()Ljava/lang/String; - public final fun getKeyStore ()Ljava/io/File; - public final fun getKeyStorePassword ()Ljava/lang/String; - public final fun getPassword ()Ljava/lang/String; -} - -public final class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails { - public fun ()V - public fun (Ljava/lang/String;Ljava/util/Date;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCommonName ()Ljava/lang/String; - public final fun getValidUntil ()Ljava/util/Date; -} - -public final class app/revanced/library/ApkUtils$SigningOptions { - public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAlias ()Ljava/lang/String; - public final fun getKeyStore ()Ljava/io/File; - public final fun getKeyStorePassword ()Ljava/lang/String; - public final fun getPassword ()Ljava/lang/String; - public final fun getSigner ()Ljava/lang/String; -} - -public final class app/revanced/library/Options { - public static final field INSTANCE Lapp/revanced/library/Options; - public final fun deserialize (Ljava/lang/String;)[Lapp/revanced/library/Options$Patch; - public final fun serialize (Ljava/util/Set;Z)Ljava/lang/String; - public static synthetic fun serialize$default (Lapp/revanced/library/Options;Ljava/util/Set;ZILjava/lang/Object;)Ljava/lang/String; - public final fun setOptions (Ljava/util/Set;Ljava/io/File;)V - public final fun setOptions (Ljava/util/Set;Ljava/lang/String;)V -} - -public final class app/revanced/library/Options$Patch { - public final fun getOptions ()Ljava/util/List; - public final fun getPatchName ()Ljava/lang/String; -} - -public final class app/revanced/library/Options$Patch$Option { - public final fun getKey ()Ljava/lang/String; - public final fun getValue ()Ljava/lang/Object; -} - -public final class app/revanced/library/PatchUtils { - public static final field INSTANCE Lapp/revanced/library/PatchUtils; - public final fun getMostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map; - public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; -} - -public final class app/revanced/library/PatchUtils$Json { - public static final field INSTANCE Lapp/revanced/library/PatchUtils$Json; - public final fun deserialize (Ljava/io/InputStream;Ljava/lang/Class;)Ljava/util/Set; - public final fun serialize (Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;)V - public static synthetic fun serialize$default (Lapp/revanced/library/PatchUtils$Json;Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;ILjava/lang/Object;)V -} - -public final class app/revanced/library/PatchUtils$Json$FullJsonPatch : app/revanced/library/PatchUtils$Json$JsonPatch { - public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$Companion; - public final fun getCompatiblePackages ()Ljava/util/Set; - public final fun getDependencies ()Ljava/util/Set; - public final fun getDescription ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getOptions ()Ljava/util/Map; - public final fun getRequiresIntegrations ()Z - public final fun getUse ()Z - public final fun setRequiresIntegrations (Z)V -} - -public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$Companion { - public final fun fromPatch (Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch; -} - -public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption { - public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion; - public final fun getDefault ()Ljava/lang/Object; - public final fun getDescription ()Ljava/lang/String; - public final fun getKey ()Ljava/lang/String; - public final fun getRequired ()Z - public final fun getTitle ()Ljava/lang/String; - public final fun getValueType ()Ljava/lang/String; - public final fun getValues ()Ljava/util/Map; -} - -public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion { - public final fun fromPatchOption (Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption; -} - -public abstract interface class app/revanced/library/PatchUtils$Json$JsonPatch { -} - -public abstract class app/revanced/library/adb/AdbManager { - public static final field Companion Lapp/revanced/library/adb/AdbManager$Companion; - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - protected final fun getDevice ()Lse/vidstige/jadb/JadbDevice; - protected final fun getLogger ()Ljava/util/logging/Logger; - public fun install (Lapp/revanced/library/adb/AdbManager$Apk;)V - public fun uninstall (Ljava/lang/String;)V -} - -public final class app/revanced/library/adb/AdbManager$Apk { - public fun (Ljava/io/File;Ljava/lang/String;)V - public synthetic fun (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getFile ()Ljava/io/File; - public final fun getPackageName ()Ljava/lang/String; -} - -public final class app/revanced/library/adb/AdbManager$Companion { - public final fun getAdbManager (Ljava/lang/String;Z)Lapp/revanced/library/adb/AdbManager; - public static synthetic fun getAdbManager$default (Lapp/revanced/library/adb/AdbManager$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lapp/revanced/library/adb/AdbManager; -} - -public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception { - public fun ()V -} - -public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception { -} - -public final class app/revanced/library/adb/AdbManager$PackageNameRequiredException : java/lang/Exception { -} - -public final class app/revanced/library/adb/AdbManager$RootAdbManager : app/revanced/library/adb/AdbManager { - public static final field Utils Lapp/revanced/library/adb/AdbManager$RootAdbManager$Utils; - public fun install (Lapp/revanced/library/adb/AdbManager$Apk;)V - public fun uninstall (Ljava/lang/String;)V -} - -public final class app/revanced/library/adb/AdbManager$RootAdbManager$Utils { -} - -public final class app/revanced/library/adb/AdbManager$UserAdbManager : app/revanced/library/adb/AdbManager { - public fun install (Lapp/revanced/library/adb/AdbManager$Apk;)V - public fun uninstall (Ljava/lang/String;)V -} - -public final class app/revanced/library/logging/Logger { - public static final field INSTANCE Lapp/revanced/library/logging/Logger; - public final fun addHandler (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V - public final fun removeAllHandlers ()V - public final fun setDefault ()V - public final fun setFormat (Ljava/lang/String;)V - public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V -} - diff --git a/build.gradle.kts b/build.gradle.kts index 0de3754..f7586aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,61 +1,95 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin) + alias(libs.plugins.android.library) alias(libs.plugins.binary.compatibility.validator) + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) `maven-publish` signing } group = "app.revanced" +// Because access to the project is necessary to authenticate with GitHub, +// the following block must be placed in the root build.gradle.kts file +// instead of the settings.gradle.kts file inside the dependencyResolutionManagement block. repositories { mavenCentral() mavenLocal() google() maven { - // A repository must be speficied for some reason. "registry" is a dummy. + // A repository must be specified for some reason. "registry" is a dummy. url = uri("https://maven.pkg.github.com/revanced/registry") credentials { username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") } } + maven { url = uri("https://jitpack.io") } } -dependencies { - implementation(libs.revanced.patcher) - implementation(libs.kotlin.reflect) - implementation(libs.jadb) // Fork with Shell v2 support. - implementation(libs.jackson.module.kotlin) - implementation(libs.apkzlib) - implementation(libs.apksig) - implementation(libs.bcpkix.jdk15on) - implementation(libs.guava) - - testImplementation(libs.revanced.patcher) - testImplementation(libs.kotlin.test) -} +kotlin { + jvm { + compilations.all { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + } + } + + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + } + + publishLibraryVariants("release") + } -tasks { - test { - useJUnitPlatform() - testLogging { - events("PASSED", "SKIPPED", "FAILED") + sourceSets { + androidMain.dependencies { + implementation(libs.core.ktx) + implementation(libs.libsu.nio) + implementation(libs.libsu.service) + } + + commonMain.dependencies { + implementation(libs.apksig) + implementation(libs.apkzlib) + implementation(libs.bcpkix.jdk15on) + implementation(libs.guava) + implementation(libs.jadb) + implementation(libs.kotlin.reflect) + implementation(libs.kotlinx.serialization.json) + implementation(libs.revanced.patcher) + } + + commonTest.dependencies { + implementation(libs.kotlin.test.junit) + implementation(libs.revanced.patcher) } } } -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) +android { + namespace = "app.revanced.library" + compileSdk = 34 + defaultConfig { + minSdk = 26 + } + + buildFeatures { + aidl = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } java { targetCompatibility = JavaVersion.VERSION_11 - - withSourcesJar() } publishing { @@ -72,8 +106,6 @@ publishing { publications { create("revanced-library-publication") { - from(components["java"]) - version = project.version.toString() pom { diff --git a/gradle.properties b/gradle.properties index abd574e..ffb848e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,11 @@ -org.gradle.parallel = true +version = 3.0.0-dev.1 +#Gradle +org.gradle.jvmargs = -Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options="-Xmx2048M" org.gradle.caching = true +org.gradle.configuration-cache = true +org.gradle.parallel = true +#Kotlin kotlin.code.style = official -version = 2.3.0 +#Android +android.useAndroidX = true +android.nonTransitiveRClass = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 04a06fc..3fd7d4a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,35 @@ [versions] -jackson-module-kotlin = "2.15.0" -jadb = "1.2.1" -kotlin = "1.9.22" -revanced-patcher = "19.3.1" -binary-compatibility-validator = "0.14.0" -apkzlib = "8.3.0" +android = "8.5.1" bcpkix-jdk15on = "1.70" +binary-compatibility-validator = "0.15.1" +core-ktx = "1.13.1" guava = "33.0.0-jre" -apksig = "8.3.0" +jadb = "1.2.1" +kotlin = "2.0.0" +kotlinx-coroutines = "1.8.1" +kotlinx-serialization = "1.7.1" +libsu = "5.2.2" +revanced-patcher = "20.0.0" [libraries] -jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" } -jadb = { module = "app.revanced:jadb", version.ref = "jadb" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } -apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "apkzlib" } +apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "android" } +apksig = { module = "com.android.tools.build:apksig", version.ref = "android" } bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" } +core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } guava = { module = "com.google.guava:guava", version.ref = "guava" } -apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } +jadb = { module = "app.revanced:jadb", version.ref = "jadb" } # Fork with Shell v2 support. +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } +libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" } +libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" } +revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } [plugins] +android-library = { id = "com.android.library", version.ref = "android" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3f203e9..68e8816 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip -distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dist \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fcb6fca..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle.kts b/settings.gradle.kts index 1df3126..e164b40 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,15 @@ +// TODO: Figure out why this causes problems. rootProject.name = "revanced-library" buildCache { local { - isEnabled = !System.getenv().containsKey("CI") + isEnabled = "CI" !in System.getenv() + } +} + +pluginManagement { + repositories { + google() + mavenCentral() } } diff --git a/src/androidMain/aidl/app/revanced/library/installation/command/ILocalShellCommandRunnerRootService.aidl b/src/androidMain/aidl/app/revanced/library/installation/command/ILocalShellCommandRunnerRootService.aidl new file mode 100644 index 0000000..39861ec --- /dev/null +++ b/src/androidMain/aidl/app/revanced/library/installation/command/ILocalShellCommandRunnerRootService.aidl @@ -0,0 +1,5 @@ +package app.revanced.library.installation.command; + +interface ILocalShellCommandRunnerRootService { + IBinder getFileSystemService(); +} diff --git a/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunner.kt b/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunner.kt new file mode 100644 index 0000000..f757ba3 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunner.kt @@ -0,0 +1,88 @@ +package app.revanced.library.installation.command + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.internal.BuilderImpl +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager +import java.io.Closeable +import java.io.File +import java.io.InputStream + +/** + * The [LocalShellCommandRunner] for running commands locally on the device. + * + * @param context The [Context] to use for binding to the [RootService]. + * @param onReady A callback to be invoked when [LocalShellCommandRunner] is ready to be used. + * @throws IllegalStateException If the main shell was already created + * + * @see ShellCommandRunner + */ +class LocalShellCommandRunner internal constructor( + private val context: Context, + private val onReady: () -> Unit, +) : ShellCommandRunner(), ServiceConnection, Closeable { + private var fileSystemManager: FileSystemManager? = null + + init { + logger.info("Binding to RootService") + val intent = Intent(context, LocalShellCommandRunnerRootService::class.java) + RootService.bind(intent, this) + } + + override fun runCommand(command: String) = shell.newJob().add(command).exec().let { + object : RunResult { + override val exitCode = it.code + override val output by lazy { it.out.joinToString("\n") } + override val error by lazy { it.err.joinToString("\n") } + } + } + + override fun hasRootPermission() = shell.isRoot + + /** + * Writes the given [content] to the given [targetFilePath]. + * + * @param content The [InputStream] to write. + * @param targetFilePath The path to write to. + * @throws NotReadyException If the [LocalShellCommandRunner] is not ready yet. + */ + override fun write(content: InputStream, targetFilePath: String) { + fileSystemManager?.let { + it.getFile(targetFilePath).newOutputStream().use { outputStream -> + content.copyTo(outputStream) + } + } ?: throw NotReadyException("FileSystemManager service is not ready yet") + } + + override fun move(file: File, targetFilePath: String) { + invoke("mv ${file.absolutePath} $targetFilePath") + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val ipc = ILocalShellCommandRunnerRootService.Stub.asInterface(service) + fileSystemManager = FileSystemManager.getRemote(ipc.fileSystemService) + + logger.info("LocalShellCommandRunner service is ready") + + onReady() + } + + override fun onServiceDisconnected(name: ComponentName?) { + fileSystemManager = null + + logger.info("LocalShellCommandRunner service is disconnected") + } + + override fun close() = RootService.unbind(this) + + private companion object { + private val shell = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER).build() + } + + internal class NotReadyException internal constructor(message: String) : Exception(message) +} diff --git a/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunnerRootService.kt b/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunnerRootService.kt new file mode 100644 index 0000000..e221257 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/command/LocalShellCommandRunnerRootService.kt @@ -0,0 +1,15 @@ +package app.revanced.library.installation.command + +import android.content.Intent +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager + +/** + * The [RootService] for the [LocalShellCommandRunner]. + */ +internal class LocalShellCommandRunnerRootService : RootService() { + override fun onBind(intent: Intent) = object : ILocalShellCommandRunnerRootService.Stub() { + override fun getFileSystemService() = + FileSystemManager.getService() + } +} diff --git a/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt new file mode 100644 index 0000000..cb3bf48 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt @@ -0,0 +1,104 @@ +package app.revanced.library.installation.installer + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import app.revanced.library.installation.installer.Installer.Apk +import java.io.Closeable +import java.io.File + +/** + * [LocalInstaller] for installing and uninstalling [Apk] files locally. + * + * @param context The [Context] to use for installing and uninstalling. + * @param onResult The callback to be invoked when the [Apk] is installed or uninstalled. + * + * @see Installer + */ +@Suppress("unused") +class LocalInstaller( + private val context: Context, + onResult: (result: LocalInstallerResult) -> Unit, +) : Installer(), Closeable { + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val pmStatus = intent.getIntExtra(LocalInstallerService.EXTRA_STATUS, -999) + val extra = intent.getStringExtra(LocalInstallerService.EXTRA_STATUS_MESSAGE)!! + val packageName = intent.getStringExtra(LocalInstallerService.EXTRA_PACKAGE_NAME)!! + + onResult.invoke(LocalInstallerResult(pmStatus, extra, packageName)) + } + } + + private val intentSender + get() = PendingIntent.getService( + context, + 0, + Intent(context, LocalInstallerService::class.java), + PendingIntent.FLAG_UPDATE_CURRENT, + ).intentSender + + init { + ContextCompat.registerReceiver( + context, + broadcastReceiver, + IntentFilter().apply { + addAction(LocalInstallerService.ACTION) + }, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + } + + override suspend fun install(apk: Apk) { + logger.info("Installing ${apk.file.name}") + + val packageInstaller = context.packageManager.packageInstaller + + packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> + session.writeApk(apk.file) + session.commit(intentSender) + } + } + + @SuppressLint("MissingPermission") + override suspend fun uninstall(packageName: String) { + logger.info("Uninstalling $packageName") + + val packageInstaller = context.packageManager.packageInstaller + + packageInstaller.uninstall(packageName, intentSender) + } + + override suspend fun getInstallation(packageName: String) = try { + val packageInfo = context.packageManager.getPackageInfo(packageName, 0) + + Installation(packageInfo.applicationInfo.sourceDir) + } catch (e: PackageManager.NameNotFoundException) { + null + } + + override fun close() = context.unregisterReceiver(broadcastReceiver) + + companion object { + private val sessionParams = PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL, + ).apply { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + + private fun PackageInstaller.Session.writeApk(apk: File) { + apk.inputStream().use { inputStream -> + openWrite(apk.name, 0, apk.length()).use { outputStream -> + inputStream.copyTo(outputStream, 1024 * 1024) + fsync(outputStream) + } + } + } + } +} diff --git a/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerResult.kt b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerResult.kt new file mode 100644 index 0000000..a326181 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerResult.kt @@ -0,0 +1,15 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.installer.Installer.Apk + +/** + * The result of installing or uninstalling an [Apk] locally using [LocalInstaller]. + * + * @param pmStatus The status code returned by the package manager. + * @param extra The extra information returned by the package manager. + * @param packageName The package name of the installed app. + * + * @see LocalInstaller + */ +@Suppress("MemberVisibilityCanBePrivate") +class LocalInstallerResult internal constructor(val pmStatus: Int, val extra: String, val packageName: String) diff --git a/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerService.kt b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerService.kt new file mode 100644 index 0000000..dc15569 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstallerService.kt @@ -0,0 +1,57 @@ +package app.revanced.library.installation.installer + +import android.app.Service +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Build +import android.os.IBinder + +class LocalInstallerService : Service() { + override fun onStartCommand( + intent: Intent, flags: Int, startId: Int + ): Int { + val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0) + 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 { + @Suppress("DEPRECATION") + intent.getParcelableExtra(Intent.EXTRA_INTENT) + }?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + + else -> { + sendBroadcast( + Intent().apply { + action = ACTION + `package` = packageName + + putExtra(EXTRA_STATUS, extraStatus) + putExtra(EXTRA_STATUS_MESSAGE, extraStatusMessage) + putExtra(EXTRA_PACKAGE_NAME, extraPackageName) + } + ) + } + } + + stopSelf() + + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + internal companion object { + internal const val ACTION = "PACKAGE_INSTALLER_ACTION" + + internal const val EXTRA_STATUS = "EXTRA_STATUS" + internal const val EXTRA_STATUS_MESSAGE = "EXTRA_STATUS_MESSAGE" + internal const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME" + } +} diff --git a/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt new file mode 100644 index 0000000..4247e83 --- /dev/null +++ b/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt @@ -0,0 +1,34 @@ +package app.revanced.library.installation.installer + +import android.content.Context +import app.revanced.library.installation.command.LocalShellCommandRunner +import app.revanced.library.installation.installer.Installer.Apk +import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException +import com.topjohnwu.superuser.ipc.RootService +import java.io.Closeable + +/** + * [LocalRootInstaller] for installing and uninstalling [Apk] files locally with using root permissions by mounting. + * + * @param context The [Context] to use for binding to the [RootService]. + * @param onReady A callback to be invoked when [LocalRootInstaller] is ready to be used. + * + * @throws NoRootPermissionException If the device does not have root permission. + * + * @see Installer + * @see LocalShellCommandRunner + */ +@Suppress("unused") +class LocalRootInstaller( + context: Context, + onReady: LocalRootInstaller.() -> Unit = {}, +) : RootInstaller( + { installer -> + LocalShellCommandRunner(context) { + (installer as LocalRootInstaller).onReady() + } + }, +), + Closeable { + override fun close() = (shellCommandRunner as LocalShellCommandRunner).close() +} diff --git a/src/main/kotlin/app/revanced/library/ApkSigner.kt b/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt similarity index 58% rename from src/main/kotlin/app/revanced/library/ApkSigner.kt rename to src/commonMain/kotlin/app/revanced/library/ApkSigner.kt index b653fe4..e020576 100644 --- a/src/main/kotlin/app/revanced/library/ApkSigner.kt +++ b/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt @@ -1,9 +1,6 @@ package app.revanced.library import com.android.apksig.ApkSigner.SignerConfig -import com.android.tools.build.apkzlib.sign.SigningExtension -import com.android.tools.build.apkzlib.sign.SigningOptions -import com.android.tools.build.apkzlib.zip.ZFile import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509v3CertificateBuilder @@ -13,7 +10,6 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import java.io.File import java.io.IOException import java.io.InputStream -import java.io.OutputStream import java.math.BigInteger import java.security.* import java.security.cert.X509Certificate @@ -197,105 +193,6 @@ object ApkSigner { ), ) - /** - * Read a [PrivateKeyCertificatePair] from a keystore entry. - * - * @param keyStore The keystore to read the entry from. - * @param keyStoreEntryAlias The alias of the key store entry to read. - * @param keyStoreEntryPassword The password for recovering the signing key. - * - * @return The read [PrivateKeyCertificatePair]. - * - * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. - */ - @Deprecated("This method will be removed in the future.") - fun readKeyCertificatePair( - keyStore: KeyStore, - keyStoreEntryAlias: String, - keyStoreEntryPassword: String, - ) = readPrivateKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword) - - /** - * Create a new keystore with a new keypair and saves it to the given [keyStoreOutputStream]. - * - * @param keyStoreOutputStream The stream to write the keystore to. - * @param keyStorePassword The password for the keystore. - * @param entries The entries to add to the keystore. - */ - @Deprecated("This method will be removed in the future.") - fun newKeyStore( - keyStoreOutputStream: OutputStream, - keyStorePassword: String?, - entries: Set, - ) = newKeyStore(entries).store( - keyStoreOutputStream, - keyStorePassword?.toCharArray(), - ) - - /** - * Create a new [Signer]. - * - * @param privateKeyCertificatePair The private key and certificate pair to use for signing. - * - * @return The new [Signer]. - * - * @see PrivateKeyCertificatePair - * @see Signer - */ - @Deprecated("This method will be removed in the future.") - fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) = - Signer( - SigningExtension( - SigningOptions.builder() - .setMinSdkVersion(21) // TODO: Extracting from the target APK would be ideal. - .setV1SigningEnabled(true) - .setV2SigningEnabled(true) - .setCertificates(privateKeyCertificatePair.certificate) - .setKey(privateKeyCertificatePair.privateKey) - .build(), - ), - ) - - /** - * Create a new [Signer]. - * - * @param signer The name of the signer. - * @param keyStore The keystore to use for signing. - * @param keyStoreEntryAlias The alias of the key store entry to use for signing. - * @param keyStoreEntryPassword The password for recovering the signing key. - * - * @return The new [Signer]. - * - * @see KeyStore - * @see Signer - */ - @Deprecated("This method will be removed in the future.") - fun newApkSigner( - signer: String, - keyStore: KeyStore, - keyStoreEntryAlias: String, - keyStoreEntryPassword: String, - ) = newApkSigner(signer, readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) - - /** - * Create a new [Signer]. - * - * @param keyStore The keystore to use for signing. - * @param keyStoreEntryAlias The alias of the key store entry to use for signing. - * @param keyStoreEntryPassword The password for recovering the signing key. - * - * @return The new [Signer]. - * - * @see KeyStore - * @see Signer - */ - @Deprecated("This method will be removed in the future.") - fun newApkSigner( - keyStore: KeyStore, - keyStoreEntryAlias: String, - keyStoreEntryPassword: String, - ) = newApkSigner("ReVanced", readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) - /** * An entry in a keystore. * @@ -322,48 +219,11 @@ object ApkSigner { val certificate: X509Certificate, ) - class Signer { - private val signerBuilder: com.android.apksig.ApkSigner.Builder? - private val signingExtension: SigningExtension? - - internal constructor(signerBuilder: com.android.apksig.ApkSigner.Builder) { - this.signerBuilder = signerBuilder - signingExtension = null - } - + class Signer internal constructor(private val signerBuilder: com.android.apksig.ApkSigner.Builder) { fun signApk(inputApkFile: File, outputApkFile: File) { logger.info("Signing APK") - signerBuilder?.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign() - } - - @Deprecated("This constructor will be removed in the future.") - internal constructor(signingExtension: SigningExtension) { - signerBuilder = null - this.signingExtension = signingExtension - } - - /** - * Sign an APK file. - * - * @param apkFile The APK file to sign. - */ - @Deprecated("This method will be removed in the future.") - fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { - @Suppress("DEPRECATION") - signApk(it) - } - - /** - * Sign an APK file. - * - * @param apkZFile The APK [ZFile] to sign. - */ - @Deprecated("This method will be removed in the future.") - fun signApk(apkZFile: ZFile) { - logger.info("Signing ${apkZFile.file.name}") - - signingExtension?.register(apkZFile) + signerBuilder.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign() } } } diff --git a/src/main/kotlin/app/revanced/library/ApkUtils.kt b/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt similarity index 68% rename from src/main/kotlin/app/revanced/library/ApkUtils.kt rename to src/commonMain/kotlin/app/revanced/library/ApkUtils.kt index b56c62c..a42cb80 100644 --- a/src/main/kotlin/app/revanced/library/ApkUtils.kt +++ b/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt @@ -103,8 +103,7 @@ object ApkUtils { * * @return The newly created private key and certificate pair. */ - @Deprecated("This method will be removed in the future.") - fun newPrivateKeyCertificatePair( + private fun newPrivateKeyCertificatePair( privateKeyCertificatePairDetails: PrivateKeyCertificatePairDetails, keyStoreDetails: KeyStoreDetails, ) = newPrivateKeyCertificatePair( @@ -132,8 +131,7 @@ object ApkUtils { * * @return The private key and certificate pair. */ - @Deprecated("This method will be removed in the future.") - fun readPrivateKeyCertificatePairFromKeyStore( + private fun readPrivateKeyCertificatePairFromKeyStore( keyStoreDetails: KeyStoreDetails, ) = ApkSigner.readPrivateKeyCertificatePair( ApkSigner.readKeyStore( @@ -168,91 +166,6 @@ object ApkUtils { }, ).signApk(inputApkFile, outputApkFile) - @Deprecated("This method will be removed in the future.") - private fun readOrNewPrivateKeyCertificatePair( - signingOptions: SigningOptions, - ): ApkSigner.PrivateKeyCertificatePair { - val privateKeyCertificatePairDetails = PrivateKeyCertificatePairDetails( - signingOptions.alias, - PrivateKeyCertificatePairDetails().validUntil, - ) - val keyStoreDetails = KeyStoreDetails( - signingOptions.keyStore, - signingOptions.keyStorePassword, - signingOptions.alias, - signingOptions.password, - ) - - return if (keyStoreDetails.keyStore.exists()) { - readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails) - } else { - newPrivateKeyCertificatePair(privateKeyCertificatePairDetails, keyStoreDetails) - } - } - - /** - * Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile]. - * - * @param inputApkFile The apk file to sign. - * @param outputApkFile The file to save the signed apk to. - * @param signer The name of the signer. - * @param privateKeyCertificatePair The private key and certificate pair to use for signing. - */ - @Deprecated("This method will be removed in the future.") - fun sign( - inputApkFile: File, - outputApkFile: File, - signer: String, - privateKeyCertificatePair: ApkSigner.PrivateKeyCertificatePair, - ) = ApkSigner.newApkSigner( - signer, - privateKeyCertificatePair, - ).signApk(inputApkFile, outputApkFile) - - /** - * Signs the apk file with the given options. - * - * @param signingOptions The options to use for signing. - */ - @Deprecated("This method will be removed in the future.") - fun File.sign(signingOptions: SigningOptions) = ApkSigner.newApkSigner( - signingOptions.signer, - readOrNewPrivateKeyCertificatePair(signingOptions), - ).signApk(this) - - /** - * Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile]. - * - * @param inputApkFile The apk file to sign. - * @param outputApkFile The file to save the signed apk to. - * @param signingOptions The options to use for signing. - */ - @Deprecated("This method will be removed in the future.") - fun sign(inputApkFile: File, outputApkFile: File, signingOptions: SigningOptions) = sign( - inputApkFile, - outputApkFile, - signingOptions.signer, - readOrNewPrivateKeyCertificatePair(signingOptions), - ) - - /** - * Options for signing an apk. - * - * @param keyStore The keystore to use for signing. - * @param keyStorePassword The password for the keystore. - * @param alias The alias of the key store entry to use for signing. - * @param password The password for recovering the signing key. - * @param signer The name of the signer. - */ - @Deprecated("This class will be removed in the future.") - class SigningOptions( - val keyStore: File, - val keyStorePassword: String?, - val alias: String = "ReVanced Key", - val password: String = "", - val signer: String = "ReVanced", - ) - /** * Details for a keystore. * diff --git a/src/commonMain/kotlin/app/revanced/library/Options.kt b/src/commonMain/kotlin/app/revanced/library/Options.kt new file mode 100644 index 0000000..32ada70 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/Options.kt @@ -0,0 +1,31 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package app.revanced.library + +import app.revanced.patcher.patch.OptionException +import app.revanced.patcher.patch.Patch +import java.util.logging.Logger + +typealias PatchName = String +typealias OptionKey = String +typealias OptionValue = Any? +typealias PatchesOptions = Map> + +private val logger = Logger.getLogger("Options") + +/** + * Set the options for a set of patches that have a name. + * + * @param options The options to set. The key is the patch name and the value is a map of option keys to option values. + */ +fun Set>.setOptions(options: PatchesOptions) = filter { it.name != null }.forEach { patch -> + val patchOptions = options[patch.name] ?: return@forEach + + patch.options.forEach option@{ option -> + try { + patch.options[option.key] = patchOptions[option.key] ?: return@option + } catch (e: OptionException) { + logger.warning("Could not set option value for the \"${patch.name}\" patch: ${e.message}") + } + } +} diff --git a/src/commonMain/kotlin/app/revanced/library/Patch.kt b/src/commonMain/kotlin/app/revanced/library/Patch.kt new file mode 100644 index 0000000..3595c77 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/Patch.kt @@ -0,0 +1,52 @@ +package app.revanced.library + +import app.revanced.patcher.patch.Package +import app.revanced.patcher.patch.Patch + +typealias PackageName = String +typealias Version = String +typealias Count = Int + +typealias VersionMap = LinkedHashMap +typealias PackageNameMap = Map + +/** + * Get the count of versions for each compatible package from the set of [Patch] ordered by the most common version. + * + * @param packageNames The names of the compatible packages to include. If null, all packages will be included. + * @param countUnusedPatches Whether to count patches that are not used. + * @return A map of package names to a map of versions to their count. + */ +fun Set>.mostCommonCompatibleVersions( + packageNames: Set? = null, + countUnusedPatches: Boolean = false, +): PackageNameMap = buildMap { + fun filterWantedPackages(compatiblePackages: List): List { + val wantedPackages = packageNames?.toHashSet() ?: return compatiblePackages + return compatiblePackages.filter { (name, _) -> name in wantedPackages } + } + + this@mostCommonCompatibleVersions.filter { it.use || countUnusedPatches } + .flatMap { it.compatiblePackages ?: emptyList() } + .let(::filterWantedPackages) + .forEach { (name, versions) -> + if (versions?.isEmpty() == true) { + return@forEach + } + + val versionMap = getOrPut(name) { linkedMapOf() } + + versions?.forEach { version -> + versionMap[version] = versionMap.getOrDefault(version, 0) + 1 + } + } + + // Sort the version maps by the most common version. + forEach { (packageName, versionMap) -> + this[packageName] = + versionMap + .asIterable() + .sortedWith(compareByDescending { it.value }) + .associate { it.key to it.value } as VersionMap + } +} diff --git a/src/commonMain/kotlin/app/revanced/library/Serialization.kt b/src/commonMain/kotlin/app/revanced/library/Serialization.kt new file mode 100644 index 0000000..1b1641a --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/Serialization.kt @@ -0,0 +1,114 @@ +package app.revanced.library + +import app.revanced.patcher.patch.* +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.encodeStructure +import kotlinx.serialization.json.* +import java.io.OutputStream + +private class PatchSerializer : KSerializer> { + override val descriptor = buildClassSerialDescriptor("Patch") { + element("name") + element("description") + element("use") + element>("dependencies") + element?>("compatiblePackages") + element("options", OptionSerializer.descriptor) + } + + override fun deserialize(decoder: Decoder) = throw NotImplementedError("Deserialization is unsupported") + + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: Patch<*>) { + encoder.encodeStructure(descriptor) { + encodeNullableSerializableElement( + descriptor, + 0, + String.serializer(), + value.name, + ) + encodeNullableSerializableElement( + descriptor, + 1, + String.serializer(), + value.description, + ) + encodeBooleanElement( + descriptor, + 2, + value.use, + ) + encodeSerializableElement( + descriptor, + 3, + ListSerializer(String.serializer()), + value.dependencies.map { it.name ?: it.toString() }, + ) + encodeNullableSerializableElement( + descriptor, + 4, + SetSerializer(PairSerializer(String.serializer(), SetSerializer(String.serializer()).nullable)), + value.compatiblePackages, + ) + encodeSerializableElement( + descriptor, + 5, + SetSerializer(OptionSerializer), + value.options.values.toSet(), + ) + } + } + + private object OptionSerializer : KSerializer> { + override val descriptor = buildClassSerialDescriptor("Option") { + element("key") + element("title") + element("description") + element("required") + // Type does not matter for serialization. Using String. + element("type") + element("default") + // Map value type does not matter for serialization. Using String. + element?>("values") + } + + override fun deserialize(decoder: Decoder) = throw NotImplementedError("Deserialization is unsupported") + + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: Option<*>) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.key) + encodeNullableSerializableElement(descriptor, 1, String.serializer(), value.title) + encodeNullableSerializableElement(descriptor, 2, String.serializer(), value.description) + encodeBooleanElement(descriptor, 3, value.required) + encodeSerializableElement(descriptor, 4, String.serializer(), value.type.toString()) + encodeNullableSerializableElement(descriptor, 5, serializer(value.type), value.default) + encodeNullableSerializableElement(descriptor, 6, MapSerializer(String.serializer(), serializer(value.type)), value.values) + } + } + } +} + +private val patchPrettySerializer by lazy { Json { prettyPrint = true } } +private val patchSerializer by lazy { Json } + +/** + * Serialize this set of [Patch] to JSON and write it to the given [outputStream]. + * + * @param outputStream The output stream to write the JSON to. + * @param prettyPrint Whether to pretty print the JSON. + */ +@OptIn(ExperimentalSerializationApi::class) +fun Set>.serializeTo( + outputStream: OutputStream, + prettyPrint: Boolean = true, +) = if (prettyPrint) { + patchPrettySerializer +} else { + patchSerializer +}.encodeToStream(SetSerializer(PatchSerializer()), this, outputStream) diff --git a/src/commonMain/kotlin/app/revanced/library/Utils.kt b/src/commonMain/kotlin/app/revanced/library/Utils.kt new file mode 100644 index 0000000..557831f --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/Utils.kt @@ -0,0 +1,18 @@ +package app.revanced.library + +/** + * Utils for the library. + */ +@Suppress("unused") +object Utils { + /** + * True if the environment is Android. + */ + val isAndroidEnvironment = + try { + Class.forName("android.app.Application") + true + } catch (e: ClassNotFoundException) { + false + } +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt b/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt new file mode 100644 index 0000000..aec0aa7 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt @@ -0,0 +1,59 @@ +package app.revanced.library.installation.command + +import app.revanced.library.installation.installer.getDevice +import se.vidstige.jadb.JadbDevice +import se.vidstige.jadb.RemoteFile +import java.io.File +import java.io.InputStream + +/** + * [AdbShellCommandRunner] for running commands on a device remotely using ADB. + * + * @see ShellCommandRunner + */ +class AdbShellCommandRunner : ShellCommandRunner { + private val device: JadbDevice + + /** + * Creates a [AdbShellCommandRunner] for the given device. + * + * @param device The device. + */ + internal constructor(device: JadbDevice) { + this.device = device + } + + /** + * Creates a [AdbShellCommandRunner] for the device with the given serial. + * + * @param deviceSerial deviceSerial The device serial. If null, the first connected device will be used. + */ + internal constructor(deviceSerial: String?) { + device = getDevice(deviceSerial, logger) + } + + override fun runCommand(command: String) = device.shellProcessBuilder(command).start().let { process -> + object : RunResult { + override val exitCode by lazy { process.waitFor() } + override val output by lazy { process.inputStream.bufferedReader().readText() } + override val error by lazy { process.errorStream.bufferedReader().readText() } + + override fun waitFor() { + process.waitFor() + } + } + } + + override fun hasRootPermission(): Boolean = invoke("whoami").exitCode == 0 + + override fun write(content: InputStream, targetFilePath: String) = + device.push(content, System.currentTimeMillis(), 644, RemoteFile(targetFilePath)) + + /** + * Moves the given [file] from the local to the target file path on the device. + * + * @param file The file to move. + * @param targetFilePath The target file path. + */ + override fun move(file: File, targetFilePath: String) = device.push(file, RemoteFile(targetFilePath)) +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt b/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt new file mode 100644 index 0000000..c5b0183 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt @@ -0,0 +1,26 @@ +package app.revanced.library.installation.command + +/** + * The result of a command execution. + */ +interface RunResult { + /** + * The exit code of the command. + */ + val exitCode: Int + + /** + * The output of the command. + */ + val output: String + + /** + * The error of the command. + */ + val error: String + + /** + * Waits for the command to finish. + */ + fun waitFor() {} +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/command/ShellCommandRunner.kt b/src/commonMain/kotlin/app/revanced/library/installation/command/ShellCommandRunner.kt new file mode 100644 index 0000000..554ac0d --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/command/ShellCommandRunner.kt @@ -0,0 +1,59 @@ +package app.revanced.library.installation.command + +import java.io.File +import java.io.InputStream +import java.util.logging.Logger + +/** + * [ShellCommandRunner] for running commands on a device. + */ +abstract class ShellCommandRunner internal constructor() { + protected val logger: Logger = Logger.getLogger(this::class.java.name) + + /** + * Writes the given [content] to the file at the given [targetFilePath] path. + * + * @param content The content of the file. + * @param targetFilePath The target file path. + */ + internal abstract fun write( + content: InputStream, + targetFilePath: String, + ) + + /** + * Moves the given [file] to the given [targetFilePath] path. + * + * @param file The file to move. + * @param targetFilePath The target file path. + */ + internal abstract fun move( + file: File, + targetFilePath: String, + ) + + /** + * Runs the given [command] on the device as root. + * + * @param command The command to run. + * @return The [RunResult]. + */ + protected abstract fun runCommand(command: String): RunResult + + /** + * Checks if the device has root permission. + * + * @return True if the device has root permission, false otherwise. + */ + internal abstract fun hasRootPermission(): Boolean + + /** + * Runs a command on the device as root. + * + * @param command The command to run. + * @return The [RunResult]. + */ + internal operator fun invoke( + command: String, + ) = runCommand("su -c \'$command\'") +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt new file mode 100644 index 0000000..c8e389c --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt @@ -0,0 +1,51 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.command.AdbShellCommandRunner +import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH +import app.revanced.library.installation.installer.Installer.Apk +import se.vidstige.jadb.JadbException +import se.vidstige.jadb.managers.Package +import se.vidstige.jadb.managers.PackageManager + +/** + * [AdbInstaller] for installing and uninstalling [Apk] files using ADB. + * + * @param deviceSerial The device serial. If null, the first connected device will be used. + * + * @see Installer + */ +class AdbInstaller( + deviceSerial: String? = null, +) : Installer() { + private val device = getDevice(deviceSerial, logger) + private val adbShellCommandRunner = AdbShellCommandRunner(device) + private val packageManager = PackageManager(device) + + init { + logger.fine("Connected to $deviceSerial") + } + + override suspend fun install(apk: Apk): AdbInstallerResult { + logger.info("Installing ${apk.file.name}") + + return runPackageManager { install(apk.file) } + } + + override suspend fun uninstall(packageName: String): AdbInstallerResult { + logger.info("Uninstalling $packageName") + + return runPackageManager { uninstall(Package(packageName)) } + } + + override suspend fun getInstallation(packageName: String): Installation? = packageManager.packages.find { + it.toString() == packageName + }?.let { Installation(adbShellCommandRunner(INSTALLED_APK_PATH).output) } + + private fun runPackageManager(block: PackageManager.() -> Unit) = try { + packageManager.run(block) + + AdbInstallerResult.Success + } catch (e: JadbException) { + AdbInstallerResult.Failure(e) + } +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstallerResult.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstallerResult.kt new file mode 100644 index 0000000..e519492 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstallerResult.kt @@ -0,0 +1,23 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.installer.Installer.Apk + +/** + * The result of installing or uninstalling an [Apk] via ADB using [AdbInstaller]. + * + * @see AdbInstaller + */ +@Suppress("MemberVisibilityCanBePrivate") +interface AdbInstallerResult { + /** + * The result of installing an [Apk] successfully. + */ + object Success : AdbInstallerResult + + /** + * The result of installing an [Apk] unsuccessfully. + * + * @param exception The exception that caused the installation to fail. + */ + class Failure internal constructor(val exception: Exception) : AdbInstallerResult +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt new file mode 100644 index 0000000..d2d771f --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt @@ -0,0 +1,23 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.command.AdbShellCommandRunner +import app.revanced.library.installation.installer.Installer.Apk +import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException + +/** + * [AdbRootInstaller] for installing and uninstalling [Apk] files with using ADB root permissions by mounting. + * + * @param deviceSerial The device serial. If null, the first connected device will be used. + * + * @throws NoRootPermissionException If the device does not have root permission. + * + * @see RootInstaller + * @see AdbShellCommandRunner + */ +class AdbRootInstaller( + deviceSerial: String? = null, +) : RootInstaller({ AdbShellCommandRunner(deviceSerial) }) { + init { + logger.fine("Connected to $deviceSerial") + } +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt new file mode 100644 index 0000000..24d9da1 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -0,0 +1,75 @@ +package app.revanced.library.installation.installer + +@Suppress("MemberVisibilityCanBePrivate") +internal object Constants { + const val PLACEHOLDER = "PLACEHOLDER" + + const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp" + const val MOUNT_PATH = "/data/adb/revanced/" + const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" + const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" + + const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" + const val MOUNT_GREP = "grep $PLACEHOLDER /proc/mounts" + const val DELETE = "rm -rf $PLACEHOLDER" + const val CREATE_DIR = "mkdir -p" + const val RESTART = "am start -S $PLACEHOLDER" + const val KILL = "am force-stop $PLACEHOLDER" + const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER" + const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH" + + const val MOUNT_APK = + "base_path=\"$MOUNTED_APK_PATH\" && " + + "mv $TMP_FILE_PATH \$base_path && " + + "chmod 644 \$base_path && " + + "chown system:system \$base_path && " + + "chcon u:object_r:apk_data_file:s0 \$base_path" + + const val UMOUNT = + "grep $PLACEHOLDER /proc/mounts | " + + "while read -r line; do echo \$line | " + + "cut -d ' ' -f 2 | " + + "sed 's/apk.*/apk/' | " + + "xargs -r umount -l; done" + + const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" + + val MOUNT_SCRIPT = + """ + #!/system/bin/sh + until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done + until [ -d "/sdcard/Android" ]; do sleep 1; done + + stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) + + # Make sure the app is installed. + if [ -z "${'$'}stock_path" ]; then + exit 1 + fi + + # Unmount any existing installations to prevent multiple unnecessary mounts. + $UMOUNT + + base_path="$MOUNTED_APK_PATH" + + chcon u:object_r:apk_data_file:s0 ${'$'}base_path + + # Use Magisk mirror, if possible. + if command -v magisk &> /dev/null; then + MIRROR="${'$'}(magisk --path)/.magisk/mirror" + fi + + mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path + + # Kill the app to force it to restart the mounted APK in case it's currently running. + $KILL + """.trimIndent() + + /** + * Replaces the [PLACEHOLDER] with the given [replacement]. + * + * @param replacement The replacement to use. + * @return The replaced string. + */ + operator fun String.invoke(replacement: String) = replace(PLACEHOLDER, replacement) +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/Installation.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/Installation.kt new file mode 100644 index 0000000..54efc6f --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/Installation.kt @@ -0,0 +1,11 @@ +package app.revanced.library.installation.installer + +/** + * [Installation] of an apk file. + * + * @param apkFilePath The apk file path. + */ +@Suppress("MemberVisibilityCanBePrivate") +open class Installation internal constructor( + val apkFilePath: String, +) diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt new file mode 100644 index 0000000..da709ea --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -0,0 +1,53 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.installer.Installer.Apk +import java.io.File +import java.util.logging.Logger + +/** + * [Installer] for installing and uninstalling [Apk] files. + * + * @param TInstallerResult The type of the result of the installation. + * @param TInstallation The type of the installation. + */ +abstract class Installer internal constructor() { + /** + * The [Logger]. + */ + protected val logger: Logger = Logger.getLogger(this::class.java.name) + + /** + * Installs the [Apk] file. + * + * @param apk The [Apk] file. + * + * @return The result of the installation. + */ + abstract suspend fun install(apk: Apk): TInstallerResult + + /** + * Uninstalls the package. + * + * @param packageName The package name. + * + * @return The result of the uninstallation. + */ + abstract suspend fun uninstall(packageName: String): TInstallerResult + + /** + * Gets the current installation or null if not installed. + * + * @param packageName The package name. + * + * @return The installation. + */ + abstract suspend fun getInstallation(packageName: String): TInstallation? + + /** + * Apk file for [Installer]. + * + * @param file The [Apk] file. + * @param packageName The package name of the [Apk] file. + */ + class Apk(val file: File, val packageName: String? = null) +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallation.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallation.kt new file mode 100644 index 0000000..95c7257 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallation.kt @@ -0,0 +1,15 @@ +package app.revanced.library.installation.installer + +/** + * [RootInstallation] of the apk file that is mounted to [installedApkFilePath] with root permissions. + * + * @param installedApkFilePath The installed apk file path or null if the apk is not installed. + * @param apkFilePath The mounting apk file path. + * @param mounted Whether the apk is mounted to [installedApkFilePath]. + */ +@Suppress("MemberVisibilityCanBePrivate") +class RootInstallation internal constructor( + val installedApkFilePath: String?, + apkFilePath: String, + val mounted: Boolean, +) : Installation(apkFilePath) diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt new file mode 100644 index 0000000..9fd4fac --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -0,0 +1,135 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.command.ShellCommandRunner +import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION_PATH +import app.revanced.library.installation.installer.Constants.DELETE +import app.revanced.library.installation.installer.Constants.EXISTS +import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH +import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT +import app.revanced.library.installation.installer.Constants.KILL +import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH +import app.revanced.library.installation.installer.Constants.MOUNT_APK +import app.revanced.library.installation.installer.Constants.MOUNT_GREP +import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT +import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH +import app.revanced.library.installation.installer.Constants.RESTART +import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH +import app.revanced.library.installation.installer.Constants.UMOUNT +import app.revanced.library.installation.installer.Constants.invoke +import app.revanced.library.installation.installer.Installer.Apk +import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException +import java.io.File + +/** + * [RootInstaller] for installing and uninstalling [Apk] files using root permissions by mounting. + * + * @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use. + * + * @throws NoRootPermissionException If the device does not have root permission. + */ +@Suppress("MemberVisibilityCanBePrivate") +abstract class RootInstaller internal constructor( + shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, +) : Installer() { + + /** + * The command runner used to run commands on the device. + */ + @Suppress("LeakingThis") + protected val shellCommandRunner = shellCommandRunnerSupplier(this) + + init { + if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException() + } + + /** + * Installs the given [apk] by mounting. + * + * @param apk The [Apk] to install. + * + * @throws PackageNameRequiredException If the [Apk] does not have a package name. + */ + override suspend fun install(apk: Apk): RootInstallerResult { + logger.info("Installing ${apk.packageName} by mounting") + + val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() + + // Setup files. + apk.file.move(TMP_FILE_PATH) + CREATE_INSTALLATION_PATH().waitFor() + MOUNT_APK(packageName)().waitFor() + + // Install and run. + TMP_FILE_PATH.write(MOUNT_SCRIPT(packageName)) + INSTALL_MOUNT_SCRIPT(packageName)().waitFor() + MOUNT_SCRIPT_PATH(packageName)().waitFor() + RESTART(packageName)() + + DELETE(TMP_FILE_PATH)() + + return RootInstallerResult.SUCCESS + } + + override suspend fun uninstall(packageName: String): RootInstallerResult { + logger.info("Uninstalling $packageName by unmounting") + + UMOUNT(packageName)() + + DELETE(MOUNTED_APK_PATH)(packageName)() + DELETE(MOUNT_SCRIPT_PATH)(packageName)() + DELETE(TMP_FILE_PATH)() // Remove residual. + + KILL(packageName)() + + return RootInstallerResult.SUCCESS + } + + override suspend fun getInstallation(packageName: String): RootInstallation? { + val patchedApkPath = MOUNTED_APK_PATH(packageName) + + val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0 + if (patchedApkExists) return null + + return RootInstallation( + INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, + patchedApkPath, + MOUNT_GREP(patchedApkPath)().exitCode == 0, + ) + } + + /** + * Runs a command on the device. + */ + protected operator fun String.invoke() = shellCommandRunner(this) + + /** + * Moves the given file to the given [targetFilePath]. + * + * @param targetFilePath The target file path. + */ + protected fun File.move(targetFilePath: String) = shellCommandRunner.move(this, targetFilePath) + + /** + * Writes the given [content] to the file. + * + * @param content The content of the file. + */ + protected fun String.write(content: String) = shellCommandRunner.write(content.byteInputStream(), this) + + /** + * Asserts that the package is installed. + * + * @throws FailedToFindInstalledPackageException If the package is not installed. + */ + private fun String.assertInstalled() { + if (INSTALLED_APK_PATH(this)().output.isNotEmpty()) { + throw FailedToFindInstalledPackageException(this) + } + } + + internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : + Exception("Failed to find installed package \"$packageName\" because no activity was found") + + internal class PackageNameRequiredException internal constructor() : Exception("Package name is required") + internal class NoRootPermissionException internal constructor() : Exception("No root permission") +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerResult.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerResult.kt new file mode 100644 index 0000000..f9fe940 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerResult.kt @@ -0,0 +1,20 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.installer.Installer.Apk + +/** + * The result of installing or uninstalling an [Apk] with root permissions using [RootInstaller]. + * + * @see RootInstaller + */ +enum class RootInstallerResult { + /** + * The result of installing an [Apk] successfully. + */ + SUCCESS, + + /** + * The result of installing an [Apk] unsuccessfully. + */ + FAILURE, +} diff --git a/src/commonMain/kotlin/app/revanced/library/installation/installer/Utils.kt b/src/commonMain/kotlin/app/revanced/library/installation/installer/Utils.kt new file mode 100644 index 0000000..3d0e890 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/installation/installer/Utils.kt @@ -0,0 +1,34 @@ +package app.revanced.library.installation.installer + +import se.vidstige.jadb.JadbConnection +import java.util.logging.Logger + +/** + * Gets the device with the given serial. + * + * @param deviceSerial The device serial. If null, the first connected device will be used. + * @param logger The logger. + * @return The device. + * @throws DeviceNotFoundException If no device with the given serial is found. + */ +internal fun getDevice( + deviceSerial: String? = null, + logger: Logger, +) = with(JadbConnection().devices) { + if (isEmpty()) throw DeviceNotFoundException() + + deviceSerial?.let { + firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException( + deviceSerial, + ) + } ?: first().also { + logger.warning("No device serial supplied. Using device with serial ${it.serial}") + } +}!! + +class DeviceNotFoundException internal constructor(deviceSerial: String? = null) : + Exception( + deviceSerial?.let { + "The device with the ADB device serial \"$deviceSerial\" can not be found" + } ?: "No ADB device found", + ) diff --git a/src/main/kotlin/app/revanced/library/logging/Logger.kt b/src/commonMain/kotlin/app/revanced/library/logging/Logger.kt similarity index 91% rename from src/main/kotlin/app/revanced/library/logging/Logger.kt rename to src/commonMain/kotlin/app/revanced/library/logging/Logger.kt index 04669fa..1495d78 100644 --- a/src/main/kotlin/app/revanced/library/logging/Logger.kt +++ b/src/commonMain/kotlin/app/revanced/library/logging/Logger.kt @@ -5,15 +5,17 @@ import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.SimpleFormatter -@Suppress("MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate", "unused") object Logger { /** * Rules for allowed loggers. */ private val allowedLoggersRules = arrayOf Boolean>( - { startsWith("app.revanced") }, // ReVanced loggers. - { this == "" }, // Logs warnings when compiling resources (Logger in class brut.util.OS). + // ReVanced loggers. + { startsWith("app.revanced") }, + // Logs warnings when compiling resources (Logger in class brut.util.OS). + { this == "" }, ) private val rootLogger = java.util.logging.Logger.getLogger("") diff --git a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt b/src/commonTest/kotlin/app/revanced/library/MostCommonCompatibleVersionsTest.kt similarity index 73% rename from src/test/kotlin/app/revanced/library/PatchUtilsTest.kt rename to src/commonTest/kotlin/app/revanced/library/MostCommonCompatibleVersionsTest.kt index da4a609..e025dd2 100644 --- a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt +++ b/src/commonTest/kotlin/app/revanced/library/MostCommonCompatibleVersionsTest.kt @@ -1,27 +1,19 @@ package app.revanced.library -import app.revanced.patcher.PatchSet -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intArrayPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption -import org.junit.jupiter.api.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import app.revanced.patcher.patch.* +import kotlin.test.Test import kotlin.test.assertEquals -internal object PatchUtilsTest { +internal class MostCommonCompatibleVersionsTest { private val patches = arrayOf( - newPatch("some.package", setOf("a")) { stringPatchOption("string", "value") }, + newPatch("some.package", setOf("a")) { stringOption("string", "value") }, newPatch("some.package", setOf("a", "b"), use = false), newPatch("some.package", setOf("a", "b", "c"), use = false), newPatch("some.other.package", setOf("b"), use = false), - newPatch("some.other.package", setOf("b", "c")) { booleanPatchOption("bool", true) }, + newPatch("some.other.package", setOf("b", "c")) { booleanOption("bool", true) }, newPatch("some.other.package", setOf("b", "c", "d")), - newPatch("some.other.other.package") { intArrayPatchOption("intArray", arrayOf(1, 2, 3)) }, + newPatch("some.other.other.package") { intsOption("intArray", listOf(1, 2, 3)) }, newPatch("some.other.other.package", setOf("a")), newPatch("some.other.other.package", setOf("b")), newPatch("some.other.other.other.package", use = false), @@ -141,38 +133,24 @@ internal object PatchUtilsTest { assertEqualsVersion(null, patches, "other.package") } - @Test - fun `serializes to and deserializes from JSON string correctly`() { - val out = ByteArrayOutputStream() - PatchUtils.Json.serialize(patches, outputStream = out) - - val deserialized = - PatchUtils.Json.deserialize( - ByteArrayInputStream(out.toByteArray()), - PatchUtils.Json.FullJsonPatch::class.java, - ) - - assert(patches.size == deserialized.size) - } - private fun assertEqualsVersions( expected: PackageNameMap, - patches: PatchSet, + patches: Set>, compatiblePackageNames: Set?, countUnusedPatches: Boolean = false, ) = assertEquals( expected, - PatchUtils.getMostCommonCompatibleVersions(patches, compatiblePackageNames, countUnusedPatches), + patches.mostCommonCompatibleVersions(compatiblePackageNames, countUnusedPatches), ) private fun assertEqualsVersion( expected: String?, - patches: PatchSet, + patches: Set>, compatiblePackageName: String, ) { assertEquals( expected, - PatchUtils.getMostCommonCompatibleVersions(patches, setOf(compatiblePackageName)) + patches.mostCommonCompatibleVersions(setOf(compatiblePackageName)) .entries.firstOrNull()?.value?.keys?.firstOrNull(), ) } @@ -181,19 +159,23 @@ internal object PatchUtilsTest { packageName: String, versions: Set? = null, use: Boolean = true, - options: Patch<*>.() -> Unit = {}, - ) = object : BytecodePatch( + options: PatchBuilder<*>.() -> Unit = {}, + ) = bytecodePatch( name = "test", - compatiblePackages = setOf(CompatiblePackage(packageName, versions?.toSet())), use = use, ) { - init { - options() + if (versions == null) { + compatibleWith(packageName) + } else { + compatibleWith( + if (versions.isEmpty()) { + packageName() + } else { + packageName(*versions.toTypedArray()) + }, + ) } - override fun execute(context: BytecodeContext) {} - - // Needed to make the patches unique. - override fun equals(other: Any?) = false + options() } } diff --git a/src/commonTest/kotlin/app/revanced/library/OptionsTest.kt b/src/commonTest/kotlin/app/revanced/library/OptionsTest.kt new file mode 100644 index 0000000..ec535c2 --- /dev/null +++ b/src/commonTest/kotlin/app/revanced/library/OptionsTest.kt @@ -0,0 +1,36 @@ +package app.revanced.library + +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringOption +import kotlin.test.Test +import kotlin.test.assertEquals + +class OptionsTest { + @Test + fun `serializes and deserializes`() { + val options = mapOf( + "Test patch" to mapOf("key1" to "test", "key2" to false), + ) + + val patch = bytecodePatch("Test patch") { + stringOption("key1") + booleanOption("key2", true) + } + val duplicatePatch = bytecodePatch("Test patch") { + stringOption("key1") + } + val unnamedPatch = bytecodePatch { + booleanOption("key1") + } + + setOf(patch, duplicatePatch, unnamedPatch).setOptions(options) + + assert(patch.options["key1"].value == "test") + assert(patch.options["key2"].value == false) + + assertEquals(patch.options["key1"].value, duplicatePatch.options["key1"].value) + + assert(unnamedPatch.options["key1"].value == null) + } +} diff --git a/src/commonTest/kotlin/app/revanced/library/SerializationTest.kt b/src/commonTest/kotlin/app/revanced/library/SerializationTest.kt new file mode 100644 index 0000000..7a8727e --- /dev/null +++ b/src/commonTest/kotlin/app/revanced/library/SerializationTest.kt @@ -0,0 +1,55 @@ +package app.revanced.library + +import app.revanced.patcher.patch.* +import kotlinx.serialization.json.* +import java.io.ByteArrayOutputStream +import kotlin.test.Test +import kotlin.test.assertIs + +class SerializationTest { + private val testPatch = bytecodePatch("Test patch") { + compatibleWith("com.example.package"("1.0.0")) + compatibleWith("com.example.package2") + + dependsOn(bytecodePatch(), bytecodePatch()) + + stringOption("key1", null, null, "title1", "description1") + booleanOption("key2", true, null, "title2", "description2") + floatsOption("key3", listOf(1.0f), mapOf("list" to listOf(1f)), "title3", "description3") + } + + private var patches = setOf(testPatch) + + @Test + fun `serializes and deserializes`() { + val serializedJson = ByteArrayOutputStream().apply { patches.serializeTo(this) }.toString() + val deserializedJson = Json.parseToJsonElement(serializedJson) + + // Test patch serialization. + + assertIs(deserializedJson) + + val deserializedPatch = deserializedJson[0].jsonObject + + assert(deserializedPatch["name"]!!.jsonPrimitive.content == "Test patch") + + assert(deserializedPatch["compatiblePackages"]!!.jsonArray.size == 2) { + "The patch should be compatible with two packages." + } + + assert(deserializedPatch["dependencies"]!!.jsonArray.size == 2) { + "Even though the dependencies are named the same, they are different objects." + } + + // Test option serialization. + + val options = deserializedPatch["options"]!!.jsonArray + + assert(options.size == 3) { "The patch should have three options." } + + assert(options[0].jsonObject["title"]!!.jsonPrimitive.content == "title1") + assert(options[0].jsonObject["default"]!!.jsonPrimitive.contentOrNull == null) + assert(options[1].jsonObject["default"]!!.jsonPrimitive.boolean) + assert(options[2].jsonObject["values"]!!.jsonObject["list"]!!.jsonArray[0].jsonPrimitive.float == 1f) + } +} diff --git a/src/main/kotlin/app/revanced/library/Options.kt b/src/main/kotlin/app/revanced/library/Options.kt deleted file mode 100644 index 2233d9e..0000000 --- a/src/main/kotlin/app/revanced/library/Options.kt +++ /dev/null @@ -1,120 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate") - -package app.revanced.library - -import app.revanced.library.Options.Patch.Option -import app.revanced.patcher.PatchSet -import app.revanced.patcher.patch.options.PatchOptionException -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import java.io.File -import java.util.logging.Logger - -@Suppress("unused") -object Options { - private val logger = Logger.getLogger(Options::class.java.name) - - private val mapper = jacksonObjectMapper() - - /** - * Serializes the options for a set of patches. - * - * @param patches The set of patches to serialize. - * @param prettyPrint Whether to pretty print the JSON. - * @return The JSON string containing the options. - */ - fun serialize( - patches: PatchSet, - prettyPrint: Boolean = false, - ): String = - patches - .filter { it.options.any() } - .map { patch -> - Patch( - patch.name!!, - patch.options.values.map { option -> - val optionValue = - try { - option.value - } catch (e: PatchOptionException) { - logger.warning("Using default option value for the ${patch.name} patch: ${e.message}") - option.default - } - - Option(option.key, optionValue) - }, - ) - } - // See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7. - .distinctBy { it.patchName } - .let { - if (prettyPrint) { - mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it) - } else { - mapper.writeValueAsString(it) - } - } - - /** - * Deserializes the options to a set of patches. - * - * @param json The JSON string containing the options. - * @return A set of [Patch]s. - * @see Patch - */ - fun deserialize(json: String): Array = mapper.readValue(json, Array::class.java) - - /** - * Sets the options for a set of patches. - * - * @param json The JSON string containing the options. - */ - fun PatchSet.setOptions(json: String) { - filter { it.options.any() }.let { patches -> - if (patches.isEmpty()) return - - val jsonPatches = - deserialize(json).associate { - it.patchName to it.options.associate { option -> option.key to option.value } - } - - patches.forEach { patch -> - jsonPatches[patch.name]?.let { jsonPatchOptions -> - jsonPatchOptions.forEach { (option, value) -> - try { - patch.options[option] = value - } catch (e: PatchOptionException) { - logger.warning("Could not set option value for the ${patch.name} patch: ${e.message}") - } - } - } - } - } - } - - /** - * Sets the options for a set of patches. - * - * @param file The file containing the JSON string containing the options. - * @see setOptions - */ - fun PatchSet.setOptions(file: File) = setOptions(file.readText()) - - /** - * Data class for a patch and its [Option]s. - * - * @property patchName The name of the patch. - * @property options The [Option]s for the patch. - */ - class Patch internal constructor( - val patchName: String, - val options: List