diff --git a/.gitignore b/.gitignore index c0af92eb..40c3c574 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,132 @@ +### 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/ + +# 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 -/local.properties -/.idea -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties +**/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 + +# Potentially copyrighted test APK +*.apk + +# Ignore vscode config +.vscode/ + +# Dependency directories +node_modules/ + +# Ignore IDEA files +.idea/ .kotlin/ + +local.properties + +.cxx \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 00000000..40b3142a --- /dev/null +++ b/.releaserc @@ -0,0 +1,49 @@ +{ + "branches": [ + "main", + { + "name": "dev", + "prerelease": true + } + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", { + "releaseRules": [ + { "type": "build", "scope": "Needs bump", "release": "patch" } + ] + } + ], + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + "gradle-semantic-release-plugin", + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md", + "gradle.properties", + ], + "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + [ + "@semantic-release/github", + { + "assets": [ + { + "path": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)" + }, + ], + successComment: false + } + ], + [ + "@saithodev/semantic-release-backmerge", + { + backmergeBranches: [{"from": "main", "to": "dev"}], + clearWorkspace: true + } + ] + ] +} diff --git a/api/api/api.api b/api/api/api.api new file mode 100644 index 00000000..eccdb145 --- /dev/null +++ b/api/api/api.api @@ -0,0 +1,182 @@ +public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope { +} + +public final class app/revanced/manager/plugin/downloader/ConstantsKt { + public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String; +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getHeaders ()Ljava/util/Map; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public final fun toDownloadResult ()Lkotlin/Pair; + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Downloader { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/DownloaderKt { + public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; +} + +public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope { + public static final field $stable I + public final fun download (Lkotlin/jvm/functions/Function3;)V + public final fun get (Lkotlin/jvm/functions/Function4;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/ExtensionsKt { + public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V +} + +public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { +} + +public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { + public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation { +} + +public abstract interface class app/revanced/manager/plugin/downloader/Scope { + public abstract fun getHostPackageName ()Ljava/lang/String; + public abstract fun getPluginPackageName ()Ljava/lang/String; +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception { + public static final field $stable I + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException { + public static final field $stable I + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { + public static final field $stable I + public final fun getIntent ()Landroid/content/Intent; + public final fun getResultCode ()I +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/webview/APIKt { + public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; + public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public fun finish ()V + public fun load (Ljava/lang/String;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun pageLoad (Ljava/lang/String;)V + public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope { + public static final field $stable I + public final fun download (Lkotlin/jvm/functions/Function5;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V +} + diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 00000000..137ab978 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,150 @@ +import java.io.IOException + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.binary.compatibility.validator) + `maven-publish` + signing +} + +group = "app.revanced" + +dependencies { + implementation(libs.androidx.ktx) + implementation(libs.runtime.ktx) + implementation(libs.activity.compose) + implementation(libs.appcompat) +} + +fun String.runCommand(): String { + val process = ProcessBuilder(split("\\s".toRegex())) + .redirectErrorStream(true) + .directory(rootDir) + .start() + + val output = StringBuilder() + val reader = process.inputStream.bufferedReader() + + val thread = Thread { + reader.forEachLine { + output.appendLine(it) + } + } + thread.start() + + if (!process.waitFor(10, TimeUnit.SECONDS)) { + process.destroy() + throw IOException("Command timed out: $this") + } + + thread.join() + return output.toString().trim() +} + +val projectPath: String = projectDir.relativeTo(rootDir).path +val lastTag = "git describe --tags --abbrev=0".runCommand() +val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any { + it.startsWith(projectPath) +} + +tasks.matching { it.name.startsWith("publish") }.configureEach { + onlyIf { + hasChangesInThisModule + } +} + +android { + namespace = "app.revanced.manager.plugin.downloader" + compileSdk = 35 + + defaultConfig { + minSdk = 26 + + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + aidl = true + } +} + +apiValidation { + nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi" +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-manager") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String? + password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String? + } + } + } + + publications { + create("Api") { + afterEvaluate { + from(components["release"]) + } + + groupId = "app.revanced" + artifactId = "revanced-manager-api" + version = project.version.toString() + + pom { + name = "ReVanced Manager API" + description = "API for ReVanced Manager." + url = "https://revanced.app" + + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-manager.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git" + url = "https://github.com/revanced/revanced-manager" + } + } + } + } +} + +signing { + useGpgCmd() + sign(publishing.publications["Api"]) +} \ No newline at end of file diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..74b7379f --- /dev/null +++ b/api/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl new file mode 100644 index 00000000..d657fcc3 --- /dev/null +++ b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl @@ -0,0 +1,8 @@ +// IWebView.aidl +package app.revanced.manager.plugin.downloader.webview; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebView { + void load(String url); + void finish(); +} \ No newline at end of file diff --git a/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl new file mode 100644 index 00000000..b0237de2 --- /dev/null +++ b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl @@ -0,0 +1,11 @@ +// IWebViewEvents.aidl +package app.revanced.manager.plugin.downloader.webview; + +import app.revanced.manager.plugin.downloader.webview.IWebView; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebViewEvents { + void ready(IWebView iface); + void pageLoad(String url); + void download(String url, String mimetype, String userAgent); +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt new file mode 100644 index 00000000..469daaae --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.plugin.downloader + +/** + * The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission. + * Plugin UI activities and internal services can be protected using this permission. + */ +const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST" \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt new file mode 100644 index 00000000..bf0a219b --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt @@ -0,0 +1,165 @@ +package app.revanced.manager.plugin.downloader + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.app.Activity +import android.os.Parcelable +import kotlinx.coroutines.withTimeout +import java.io.InputStream +import java.io.OutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This API is only intended for plugin hosts, don't use it in a plugin.", +) +@Retention(AnnotationRetention.BINARY) +annotation class PluginHostApi + +/** + * The base interface for all DSL scopes. + */ +interface Scope { + /** + * The package name of ReVanced Manager. + */ + val hostPackageName: String + + /** + * The package name of the plugin. + */ + val pluginPackageName: String +} + +/** + * The scope of [DownloaderScope.get]. + */ +interface GetScope : Scope { + /** + * Ask the user to perform some required interaction in the activity specified by the provided [Intent]. + * This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK]. + * + * @throws UserInteractionException.RequestDenied User decided to skip this plugin. + * @throws UserInteractionException.Activity.Cancelled The activity was cancelled. + * @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code. + */ + suspend fun requestStartActivity(intent: Intent): Intent? +} + +interface BaseDownloadScope : Scope + +/** + * The scope for [DownloaderScope.download]. + */ +interface InputDownloadScope : BaseDownloadScope + +typealias Size = Long +typealias DownloadResult = Pair + +typealias Version = String +typealias GetResult = Pair + +class DownloaderScope internal constructor( + private val scopeImpl: Scope, + internal val context: Context +) : Scope by scopeImpl { + // Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases. + // It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins. + internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null + internal var get: (suspend GetScope.(String, String?) -> GetResult?)? = null + private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {} + + /** + * Define the download block of the plugin. + */ + fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) { + download = { app, outputStream -> + val (inputStream, size) = inputDownloadScopeImpl.block(app) + + inputStream.use { + if (size != null) reportSize(size) + it.copyTo(outputStream) + } + } + } + + /** + * Define the get block of the plugin. + * The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null. + */ + fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) { + get = block + } + + /** + * Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends. + */ + suspend fun useService(intent: Intent, block: suspend (IBinder) -> R): R { + var onBind: ((IBinder) -> Unit)? = null + val serviceConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = + onBind!!(service!!) + + override fun onServiceDisconnected(name: ComponentName?) {} + } + + return try { + val binder = withTimeout(10000L) { + suspendCoroutine { continuation -> + onBind = continuation::resume + context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE) + } + } + block(binder) + } finally { + onBind = null + context.unbindService(serviceConn) + } + } +} + +class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { + @PluginHostApi + fun build(scopeImpl: Scope, context: Context) = + with(DownloaderScope(scopeImpl, context)) { + block() + + Downloader( + download = download!!, + get = get!! + ) + } +} + +class Downloader internal constructor( + @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult?, + @property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit +) + +/** + * Define a downloader plugin. + */ +fun Downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block) + +/** + * @see GetScope.requestStartActivity + */ +sealed class UserInteractionException(message: String) : Exception(message) { + class RequestDenied @PluginHostApi constructor() : + UserInteractionException("Request denied by user") + + sealed class Activity(message: String) : UserInteractionException(message) { + class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled") + + /** + * @param resultCode The result code of the activity. + * @param intent The [Intent] of the activity. + */ + class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) : + Activity("Unexpected activity result code: $resultCode") + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt new file mode 100644 index 00000000..a1e6bf79 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt @@ -0,0 +1,42 @@ +package app.revanced.manager.plugin.downloader + +import android.app.Activity +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.Parcelable +import java.io.OutputStream + +/** + * The scope of the [OutputStream] version of [DownloaderScope.download]. + */ +interface OutputDownloadScope : BaseDownloadScope { + suspend fun reportSize(size: Long) +} + +/** + * A replacement for [DownloaderScope.download] that uses [OutputStream]. + * The provided [OutputStream] does not need to be closed manually. + */ +fun DownloaderScope.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) { + download = block +} + +/** + * Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY]. + * @see [GetScope.requestStartActivity] + */ +suspend inline fun GetScope.requestStartActivity() = + requestStartActivity( + Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) } + ) + +/** + * Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE]. + * @see [DownloaderScope.useService] + */ +suspend inline fun DownloaderScope<*>.useService( + noinline block: suspend (IBinder) -> R +) = useService( + Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block +) \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt new file mode 100644 index 00000000..414ad889 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.plugin.downloader + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.net.HttpURLConnection +import java.net.URI + +/** + * A simple parcelable data class for storing a package name and version. + * This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function. + * + * @param name The package name. + * @param version The version. + */ +@Parcelize +data class Package(val name: String, val version: String) : Parcelable + +/** + * A data class for storing a download URL. + * + * @param url The download URL. + * @param headers The headers to use for the request. + */ +@Parcelize +data class DownloadUrl(val url: String, val headers: Map = emptyMap()) : Parcelable { + /** + * Converts this into a [DownloadResult]. + */ + fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) { + useCaches = false + allowUserInteraction = false + headers.forEach(::setRequestProperty) + + connectTimeout = 10_000 + connect() + + inputStream to getHeaderField("Content-Length").toLong() + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt new file mode 100644 index 00000000..2e5034e1 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt @@ -0,0 +1,176 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.content.Intent +import app.revanced.manager.plugin.downloader.DownloadUrl +import app.revanced.manager.plugin.downloader.DownloaderScope +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.Scope +import app.revanced.manager.plugin.downloader.Downloader +import app.revanced.manager.plugin.downloader.PluginHostApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlin.properties.Delegates + +typealias InitialUrl = String +typealias PageLoadCallback = suspend WebViewCallbackScope.(url: String) -> Unit +typealias DownloadCallback = suspend WebViewCallbackScope.(url: String, mimeType: String, userAgent: String) -> Unit + +interface WebViewCallbackScope : Scope { + /** + * Finishes the activity and returns the [result]. + */ + suspend fun finish(result: T) + + /** + * Tells the WebView to load the specified [url]. + */ + suspend fun load(url: String) +} + +@OptIn(PluginHostApi::class) +class WebViewScope internal constructor( + coroutineScope: CoroutineScope, + private val scopeImpl: Scope, + setResult: (T) -> Unit +) : Scope by scopeImpl { + private var onPageLoadCallback: PageLoadCallback = {} + private var onDownloadCallback: DownloadCallback = { _, _, _ -> } + + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher = Dispatchers.Default.limitedParallelism(1) + private lateinit var webView: IWebView + internal lateinit var initialUrl: String + + internal val binder = object : IWebViewEvents.Stub() { + override fun ready(iface: IWebView?) { + coroutineScope.launch(dispatcher) { + webView = iface!!.also { + it.load(initialUrl) + } + } + } + + override fun pageLoad(url: String?) { + coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) } + } + + override fun download(url: String?, mimetype: String?, userAgent: String?) { + coroutineScope.launch(dispatcher) { + onDownloadCallback( + callbackScope, + url!!, + mimetype!!, + userAgent!! + ) + } + } + } + + private val callbackScope = object : WebViewCallbackScope, Scope by scopeImpl { + override suspend fun finish(result: T) { + setResult(result) + // Tell the WebViewActivity to finish + webView.let { withContext(Dispatchers.IO) { it.finish() } } + } + + override suspend fun load(url: String) { + webView.let { withContext(Dispatchers.IO) { it.load(url) } } + } + + } + + /** + * Called when the WebView attempts to download a file to disk. + */ + fun download(block: DownloadCallback) { + onDownloadCallback = block + } + + /** + * Called when the WebView finishes loading a page. + */ + fun pageLoad(block: PageLoadCallback) { + onPageLoadCallback = block + } +} + +@JvmInline +private value class Container(val value: U) + +/** + * Run a [android.webkit.WebView] Activity controlled by the provided code block. + * The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish]. + * The [block] defines the event handlers and returns the initial URL. + * + * @param title The string displayed in the action bar. + * @param block The control block. + */ +@OptIn(PluginHostApi::class) +suspend fun GetScope.runWebView( + title: String, + block: suspend WebViewScope.() -> InitialUrl +) = supervisorScope { + var result by Delegates.notNull>() + + val scope = WebViewScope(this@supervisorScope, this@runWebView) { result = Container(it) } + scope.initialUrl = scope.block() + + // Start the webview activity and wait until it finishes. + requestStartActivity(Intent().apply { + putExtra( + WebViewActivity.KEY, + WebViewActivity.Parameters(title, scope.binder) + ) + setClassName( + hostPackageName, + WebViewActivity::class.qualifiedName!! + ) + }) + + // Return the result and cancel any leftover coroutines. + coroutineContext.cancelChildren() + result.value +} + +/** + * Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView]. + * Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get]. + * + * @see runWebView + */ +fun WebViewDownloader(block: suspend WebViewScope.(packageName: String, version: String?) -> InitialUrl?) = + Downloader { + val label = context.applicationInfo.loadLabel( + context.packageManager + ).toString() + + get { packageName, version -> + class ReturnNull : Exception() + + try { + runWebView(label) { + download { url, _, userAgent -> + finish( + DownloadUrl( + url, + mapOf("User-Agent" to userAgent) + ) + ) + } + + block(this@runWebView, packageName, version) ?: throw ReturnNull() + } to version + } catch (_: ReturnNull) { + null + } + } + + download { + it.toDownloadResult() + } + } \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt new file mode 100644 index 00000000..aff01337 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt @@ -0,0 +1,161 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.annotation.SuppressLint +import android.os.Bundle +import android.os.IBinder +import android.os.Parcelable +import android.view.MenuItem +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.viewModelScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.R +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@OptIn(PluginHostApi::class) +@PluginHostApi +class WebViewActivity : ComponentActivity() { + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val vm by viewModels() + enableEdgeToEdge() + setContentView(R.layout.activity_webview) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + val webView = findViewById(R.id.webview) + onBackPressedDispatcher.addCallback { + if (webView.canGoBack()) webView.goBack() + else cancelActivity() + } + + val params = intent.getParcelableExtra(KEY)!! + actionBar?.apply { + title = params.title + setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel) + setDisplayHomeAsUpEnabled(true) + } + + val events = IWebViewEvents.Stub.asInterface(params.events)!! + vm.setup(events) + + webView.apply { + settings.apply { + cacheMode = WebSettings.LOAD_NO_CACHE + allowContentAccess = false + domStorageEnabled = true + javaScriptEnabled = true + } + + webViewClient = vm.webViewClient + setDownloadListener { url, userAgent, _, mimetype, _ -> + vm.onDownload(url, mimetype, userAgent) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + vm.commands.collect { + when (it) { + is WebViewModel.Command.Finish -> { + setResult(RESULT_OK) + finish() + } + + is WebViewModel.Command.Load -> webView.loadUrl(it.url) + } + } + } + } + } + + private fun cancelActivity() { + setResult(RESULT_CANCELED) + finish() + } + + override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) { + cancelActivity() + + true + } else super.onOptionsItemSelected(item) + + @Parcelize + internal class Parameters( + val title: String, val events: IBinder + ) : Parcelable + + internal companion object { + const val KEY = "params" + } +} + +@OptIn(PluginHostApi::class) +internal class WebViewModel : ViewModel() { + init { + CookieManager.getInstance().apply { + removeAllCookies(null) + setAcceptCookie(true) + } + } + + private val commandChannel = Channel() + val commands = commandChannel.receiveAsFlow() + + private var eventBinder: IWebViewEvents? = null + private val ctrlBinder = object : IWebView.Stub() { + override fun load(url: String?) { + viewModelScope.launch { + commandChannel.send(Command.Load(url!!)) + } + } + + override fun finish() { + viewModelScope.launch { + commandChannel.send(Command.Finish) + } + } + } + + val webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + eventBinder!!.pageLoad(url) + } + } + + fun onDownload(url: String, mimeType: String, userAgent: String) { + eventBinder!!.download(url, mimeType, userAgent) + } + + fun setup(binder: IWebViewEvents) { + if (eventBinder != null) return + eventBinder = binder + binder.ready(ctrlBinder) + } + + sealed interface Command { + data class Load(val url: String) : Command + data object Finish : Command + } +} \ No newline at end of file diff --git a/api/src/main/res/layout/activity_webview.xml b/api/src/main/res/layout/activity_webview.xml new file mode 100644 index 00000000..51f761d9 --- /dev/null +++ b/api/src/main/res/layout/activity_webview.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/api/src/main/res/values/strings.xml b/api/src/main/res/values/strings.xml new file mode 100644 index 00000000..73862c41 --- /dev/null +++ b/api/src/main/res/values/strings.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/api/src/main/res/values/themes.xml b/api/src/main/res/values/themes.xml new file mode 100644 index 00000000..495cde8e --- /dev/null +++ b/api/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8286111f..d3d7c3eb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,109 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.devtools) alias(libs.plugins.about.libraries) + signing +} + +val outputApkFileName = "${rootProject.name}-$version.apk" + +dependencies { + // AndroidX Core + implementation(libs.androidx.ktx) + implementation(libs.runtime.ktx) + implementation(libs.runtime.compose) + implementation(libs.splash.screen) + implementation(libs.activity.compose) + implementation(libs.work.runtime.ktx) + implementation(libs.preferences.datastore) + implementation(libs.appcompat) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.preview) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.livedata) + implementation(libs.compose.material.icons.extended) + implementation(libs.compose.material3) + implementation(libs.navigation.compose) + + // Accompanist + implementation(libs.accompanist.drawablepainter) + + // Placeholder + implementation(libs.placeholder.material3) + + // HTML Scraper + implementation(libs.skrapeit.dsl) + implementation(libs.skrapeit.parser) + + // Coil (async image loading, network image) + implementation(libs.coil.compose) + implementation(libs.coil.appiconloader) + + // KotlinX + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.collection.immutable) + implementation(libs.kotlinx.datetime) + + // Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + annotationProcessor(libs.room.compiler) + ksp(libs.room.compiler) + + // ReVanced + implementation(libs.revanced.patcher) + implementation(libs.revanced.library) + + // Downloader plugins + implementation(project(":api")) + + // Native processes + implementation(libs.kotlin.process) + + // HiddenAPI + compileOnly(libs.hidden.api.stub) + + // LibSU + implementation(libs.libsu.core) + implementation(libs.libsu.service) + implementation(libs.libsu.nio) + + // Koin + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.navigation) + implementation(libs.koin.workmanager) + + // Licenses + implementation(libs.about.libraries) + + // Ktor + implementation(libs.ktor.core) + implementation(libs.ktor.logging) + implementation(libs.ktor.okhttp) + implementation(libs.ktor.content.negotiation) + implementation(libs.ktor.serialization) + + // Markdown + implementation(libs.markdown.renderer) + + // Fading Edges + implementation(libs.fading.edges) + + // Scrollbars + implementation(libs.scrollbars) + + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) + + // Reorderable lists + implementation(libs.reorderable) + + // Compose Icons + implementation(libs.compose.icons.fontawesome) } android { @@ -58,6 +161,15 @@ android { } buildConfigField("long", "BUILD_ID", "0L") + signingConfig = signingConfigs.getByName("debug") + } + } + + applicationVariants.all { + outputs.all { + this as com.android.build.gradle.internal.api.ApkVariantOutputImpl + + outputFileName = outputApkFileName } } @@ -120,103 +232,26 @@ kotlin { jvmToolchain(17) } -dependencies { +tasks { + // Needed by gradle-semantic-release-plugin. + // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. + val publish by registering { + group = "publishing" + description = "Build the release APK" - // AndroidX Core - implementation(libs.androidx.ktx) - implementation(libs.runtime.ktx) - implementation(libs.runtime.compose) - implementation(libs.splash.screen) - implementation(libs.activity.compose) - implementation(libs.work.runtime.ktx) - implementation(libs.preferences.datastore) - implementation(libs.appcompat) + dependsOn("assembleRelease") - // Compose - implementation(platform(libs.compose.bom)) - implementation(libs.compose.ui) - implementation(libs.compose.ui.preview) - implementation(libs.compose.ui.tooling) - implementation(libs.compose.livedata) - implementation(libs.compose.material.icons.extended) - implementation(libs.compose.material3) - implementation(libs.navigation.compose) + val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}") + val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") } - // Accompanist - implementation(libs.accompanist.drawablepainter) + inputs.file(apk).withPropertyName("inputApk") + outputs.file(ascFile).withPropertyName("outputAsc") - // Placeholder - implementation(libs.placeholder.material3) - - // HTML Scraper - implementation(libs.skrapeit.dsl) - implementation(libs.skrapeit.parser) - - // Coil (async image loading, network image) - implementation(libs.coil.compose) - implementation(libs.coil.appiconloader) - - // KotlinX - implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.collection.immutable) - implementation(libs.kotlinx.datetime) - - // Room - implementation(libs.room.runtime) - implementation(libs.room.ktx) - annotationProcessor(libs.room.compiler) - ksp(libs.room.compiler) - - // ReVanced - implementation(libs.revanced.patcher) - implementation(libs.revanced.library) - - // Downloader plugins - implementation(libs.plugin.api) - - // Native processes - implementation(libs.kotlin.process) - - // HiddenAPI - compileOnly(libs.hidden.api.stub) - - // LibSU - implementation(libs.libsu.core) - implementation(libs.libsu.service) - implementation(libs.libsu.nio) - - // Koin - implementation(libs.koin.android) - implementation(libs.koin.compose) - implementation(libs.koin.compose.navigation) - implementation(libs.koin.workmanager) - - // Licenses - implementation(libs.about.libraries) - - // Ktor - implementation(libs.ktor.core) - implementation(libs.ktor.logging) - implementation(libs.ktor.okhttp) - implementation(libs.ktor.content.negotiation) - implementation(libs.ktor.serialization) - - // Markdown - implementation(libs.markdown.renderer) - - // Fading Edges - implementation(libs.fading.edges) - - // Scrollbars - implementation(libs.scrollbars) - - // EnumUtil - implementation(libs.enumutil) - ksp(libs.enumutil.ksp) - - // Reorderable lists - implementation(libs.reorderable) - - // Compose Icons - implementation(libs.compose.icons.fontawesome) + doLast { + signing { + useGpgCmd() + sign(apk.get().asFile) + } + } + } } diff --git a/gradle.properties b/gradle.properties index 36d069af..3b64c452 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,5 +22,7 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.nonFinalResIds=false -org.gradle.configuration-cache=true +# Task :app:assembleReleaseSignApk fails if this is set to true. +org.gradle.configuration-cache=false org.gradle.caching=true +version=1.25.0-dev.1 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dae1b2ef..7b7ab048 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,6 @@ datetime = "0.6.1" room-version = "2.7.1" revanced-patcher = "21.0.0" revanced-library = "3.0.2" -plugin-api = "1.0.0" koin = "3.5.3" ktor = "2.3.9" markdown-renderer = "0.30.0" @@ -37,6 +36,7 @@ enumutil = "1.1.1" compose-icons = "1.2.4" kotlin-process = "1.5.1" hidden-api-stub = "4.3.3" +binary-compatibility-validator = "0.17.0" [libraries] # AndroidX Core @@ -83,9 +83,6 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" } revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" } -# Plugin API -plugin-api = { group = "app.revanced", name = "revanced-manager-downloader-api", version.ref = "plugin-api" } - # Koin koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } @@ -145,4 +142,5 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } -about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } \ No newline at end of file +about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } +binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } diff --git a/package.json b/package.json new file mode 100644 index 00000000..246f579a --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "@anolilab/multi-semantic-release": "^1.1.10", + "@revanced/gradle-semantic-release-plugin": "^1.10.1", + "@saithodev/semantic-release-backmerge": "^4.0.1", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^6.0.3", + "@semantic-release/git": "^10.0.1", + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 43af9038..2392181c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,5 +22,5 @@ dependencyResolutionManagement { } } -rootProject.name = "ReVanced Manager" -include(":app") +rootProject.name = "revanced-manager" +include(":app", ":api")