fix: Refactor of shared module + minor fixes

This commit is contained in:
brosssh
2025-07-08 13:07:09 +02:00
parent e75f8da85a
commit 5f6c7e5333
34 changed files with 131 additions and 143 deletions

View File

@@ -67,20 +67,21 @@ The collection of ReVanced downloaders.
## 🧑‍💻 Usage
- A downloader is managed as Android apps. Download and install the APK file from the releases page.
- Downloaders are managed as Android apps. Download and install the APK file from the [releases page](https://github.com/ReVanced/revanced-manager-downloaders/releases/).
- After installing, restart ReVanced Manager and enable the downloader in the settings.
- The downloader will now be usable.
### APKMirror Downloader
This downloader will open an [APKMirror](https://www.apkmirror.com/) page where you can download the apk as you would normally do.
If the chosen file is a bundle (.apkm file), the downloader will automatically merge it into a .apk file.
If the chosen file is a bundle (`apkm` file), the downloader will automatically merge it into an `apk` file.
### Play Store Downloader
When you get prompted, log in to Google with your account. After that, you will be able to download apps from the Play Store.
> [!WARNING]
> Due to technical limitations, it is only possible to download the latest version of the app. If the suggested version differs from the latest, the installation will fail.
> This plugin is not officially endorsed by or affiliated with Google. All usage is at your own risk.
## 📚 Everything else

View File

@@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
@@ -26,7 +27,7 @@ subprojects {
}
}
if (project.path.endsWith("-downloader")) {
if (project.path.startsWith(":downloaders:")) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.android")
apply(plugin = "maven-publish")
@@ -76,7 +77,7 @@ subprojects {
applicationVariants.all {
outputs.all {
this as ApkVariantOutputImpl
outputFileName = "${project.name}-$version.apk"
outputFileName = "${project.name}-downloader-$version.apk"
}
}
}
@@ -90,7 +91,7 @@ subprojects {
tasks.register("assembleReleaseSignApk") {
dependsOn("assembleRelease")
val apk = layout.buildDirectory.file("outputs/apk/release/${project.name}-$version.apk")
val apk = layout.buildDirectory.file("outputs/apk/release/${project.name}-downloader-$version.apk")
inputs.file(apk).withPropertyName("input")
outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") })

View File

@@ -1,106 +0,0 @@
@file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.apkmirror
import android.net.Uri
import android.os.Parcelable
import android.util.Log
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.download
import app.revanced.manager.plugin.downloader.webview.runWebView
import app.revanced.manager.plugin.utils.Merger
import com.reandroid.apk.APKLogger
import kotlinx.parcelize.Parcelize
import java.io.File
import java.net.URI
import java.nio.file.Files
import java.util.UUID
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.outputStream
@Parcelize
class ApkMirrorApp(
val downloadUrl: DownloadUrl
) : Parcelable
private object ArscLogger : APKLogger {
const val TAG = "ARSCLib"
override fun logMessage(msg: String) {
Log.i(TAG, msg)
}
override fun logError(msg: String, tr: Throwable?) {
Log.e(TAG, msg, tr)
}
override fun logVerbose(msg: String) {
Log.v(TAG, msg)
}
}
@OptIn(ExperimentalPathApi::class)
val ApkMirrorDownloader = Downloader<ApkMirrorApp> {
get { packageName, version ->
ApkMirrorApp(
downloadUrl = runWebView("APKMirror") {
download { url, _, userAgent ->
finish(
DownloadUrl(
url,
mapOf("User-Agent" to userAgent)
)
)
}
Uri.Builder()
.scheme("https")
.authority("www.apkmirror.com")
.appendQueryParameter("post_type", "app_release")
.appendQueryParameter("searchtype", "apk")
.appendQueryParameter("s", version?.let { "$packageName $it" } ?: packageName)
.appendQueryParameter("bundles%5B%5D" /* bundles[] */, "apk_files")
.toString()
}
) to version
}
download { app, outputStream ->
val workingDir = Files.createTempDirectory("apkmirror_dl")
try {
if (URI(app.downloadUrl.url).path.substringAfterLast('/').endsWith(".apk")) {
val (inputStream, size) = app.downloadUrl.toDownloadResult()
inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
} else {
val downloadedFile = workingDir.resolve(UUID.randomUUID().toString()).also {
it.outputStream().use { output ->
app.downloadUrl.toDownloadResult().first.copyTo(output)
}
}
val xapkWorkingDir = workingDir.resolve("xapk").also { it.toFile().mkdirs() }
ZipFile(downloadedFile.toString()).use { zip ->
zip.entries().asSequence().forEach { entry ->
xapkWorkingDir.resolve(entry.name).also { it.parent.toFile().mkdirs() }.also { outputFile ->
zip.getInputStream(entry).use { input ->
File(outputFile.toString()).outputStream().use { output ->
input.copyTo(output)
}
}
}
}
}
Merger.merge(xapkWorkingDir, ArscLogger).writeApk(outputStream)
}
} finally {
workingDir.deleteRecursively()
}
}
}

View File

@@ -10,7 +10,6 @@ android {
}
dependencies {
implementation(project(":arsclib"))
implementation(project(":shared"))
}
}

View File

@@ -0,0 +1,76 @@
@file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.apkmirror
import android.net.Uri
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.download
import app.revanced.manager.plugin.downloader.webview.runWebView
import app.revanced.manager.plugin.shared.Merger
import java.net.URI
import java.nio.file.Files
import java.util.UUID
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.outputStream
@OptIn(ExperimentalPathApi::class)
val ApkMirrorDownloader = Downloader<DownloadUrl> {
get { packageName, version ->
runWebView("APKMirror") {
download { url, _, userAgent ->
finish(
DownloadUrl(
url,
mapOf("User-Agent" to userAgent)
)
)
}
Uri.Builder()
.scheme("https")
.authority("www.apkmirror.com")
.appendQueryParameter("post_type", "app_release")
.appendQueryParameter("searchtype", "apk")
.appendQueryParameter("s", version?.let { "$packageName $it" } ?: packageName)
.appendQueryParameter("bundles%5B%5D" /* bundles[] */, "apk_files")
.toString()
} to version
}
download { downloadUrl, outputStream ->
val workingDir = Files.createTempDirectory("apkmirror_dl")
try {
if (URI(downloadUrl.url).path.substringAfterLast('/').endsWith(".apk")) {
val (inputStream, size) = downloadUrl.toDownloadResult()
inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
} else {
val downloadedFile = workingDir.resolve(UUID.randomUUID().toString()).also {
it.outputStream().use { output ->
downloadUrl.toDownloadResult().first.copyTo(output)
}
}
val xapkWorkingDir = workingDir.resolve("xapk").also { it.toFile().mkdirs() }
ZipFile(downloadedFile.toString()).use { zip ->
zip.entries().asSequence().forEach { entry ->
xapkWorkingDir.resolve(entry.name).also { it.parent.toFile().mkdirs() }.also { outputFile ->
zip.getInputStream(entry).use { input ->
Files.copy(input, outputFile)
}
}
}
}
Merger.merge(xapkWorkingDir).writeApk(outputStream)
}
} finally {
workingDir.deleteRecursively()
}
}
}

View File

@@ -12,7 +12,6 @@ android {
dependencies {
implementation(libs.gplayapi)
implementation(project(":arsclib"))
implementation(project(":shared"))
implementation(libs.ktor.core)

View File

@@ -7,11 +7,10 @@ import app.revanced.manager.plugin.downloader.play.store.data.Credentials
import app.revanced.manager.plugin.downloader.play.store.data.Http
import app.revanced.manager.plugin.downloader.play.store.service.CredentialProviderService
import app.revanced.manager.plugin.downloader.play.store.ui.AuthActivity
import app.revanced.manager.plugin.utils.Merger
import app.revanced.manager.plugin.shared.Merger
import com.aurora.gplayapi.data.models.File as GPlayFile
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.reandroid.apk.APKLogger
import io.ktor.client.request.url
import kotlinx.parcelize.Parcelize
import java.nio.file.Files
@@ -25,22 +24,6 @@ import kotlin.io.path.outputStream
private val allowedFileTypes = arrayOf(GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT)
const val LOG_TAG = "PlayStorePlugin"
private object ArscLogger : APKLogger {
const val TAG = "ARSCLib"
override fun logMessage(msg: String) {
Log.i(TAG, msg)
}
override fun logError(msg: String, tr: Throwable?) {
Log.e(TAG, msg, tr)
}
override fun logVerbose(msg: String) {
Log.v(TAG, msg)
}
}
@Parcelize
class GPlayApp(
val files: List<GPlayFile>
@@ -109,7 +92,7 @@ val playStoreDownloader = Downloader<GPlayApp> {
if (apkFiles.size == 1)
Files.copy(apkFiles.first(), outputStream)
else
Merger.merge(apkDir, ArscLogger).writeApk(outputStream)
Merger.merge(apkDir).writeApk(outputStream)
} finally {
apkDir.deleteRecursively()

View File

@@ -2,7 +2,6 @@
agp = "8.7.3"
kotlin = "2.1.10"
gplayapi = "3.4.4"
arsclib = "1.3.5"
kotlinx-coroutines-core = "1.9.0"
ktor = "2.3.9"
@@ -18,7 +17,6 @@ plugin-api = "1.0.0"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
plugin-api = { group = "app.revanced", name = "revanced-manager-downloader-api", version.ref = "plugin-api" }
gplayapi = { group = "com.auroraoss", name = "gplayapi", version.ref = "gplayapi" }
arsclib = { group = "io.github.reandroid", name = "ARSCLib", version.ref = "arsclib" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }
@@ -31,6 +29,7 @@ ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "k
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.0.21" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@@ -19,6 +19,6 @@ include(":shared")
include(":arsclib")
file("downloaders").listFiles()
?.forEach {
include(":${it.name}")
project(":${it.name}").projectDir = file("downloaders/${it.name}")
include(":downloaders:${it.name}")
project(":downloaders:${it.name}").projectDir = file("downloaders/${it.name}")
}

View File

@@ -1,8 +1,27 @@
plugins {
kotlin("jvm")
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "app.revanced.manager.plugin.shared"
compileSdk = 35
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(project(":arsclib"))
api(project(":arsclib"))
}

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.plugin.utils
package app.revanced.manager.plugin.shared
import android.util.Log
import com.reandroid.apk.APKLogger
import com.reandroid.apk.ApkBundle
import com.reandroid.apk.ApkModule
@@ -9,15 +10,31 @@ import java.nio.file.Path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private object ArscLogger : APKLogger {
const val TAG = "ARSCLib"
override fun logMessage(msg: String) {
Log.i(TAG, msg)
}
override fun logError(msg: String, tr: Throwable?) {
Log.e(TAG, msg, tr)
}
override fun logVerbose(msg: String) {
Log.v(TAG, msg)
}
}
class Merger {
companion object Factory {
suspend fun merge(apkDir: Path, arscLogger: APKLogger): ApkModule {
suspend fun merge(apkDir: Path): ApkModule {
val closeables = mutableSetOf<Closeable>()
try {
// Merge splits
val merged = withContext(Dispatchers.Default) {
with(ApkBundle()) {
setAPKLogger(arscLogger)
setAPKLogger(ArscLogger)
loadApkDirectory(apkDir.toFile())
closeables.addAll(modules)
mergeModules().also(closeables::add)