Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: android 5 support #74

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ android {
compileSdk = 35

defaultConfig {
minSdk = 24
minSdk = 21
targetSdk = 35
versionCode = 1
versionName = "0.0.1"
Expand Down Expand Up @@ -91,21 +91,24 @@ android {
excludes += "/org/bouncycastle/**"
}
jniLibs {
// For extractNativeLibs=false
useLegacyPackaging = false

// x86 is dead
excludes += "/lib/x86/*.so"
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
Comment on lines -100 to +104
Copy link

@kitadai31 kitadai31 Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to downgrade Java version
Even VERSION_17 can be used with minSdk 21

kotlinOptions.jvmTarget is too

}

kotlinOptions {
val reportsDir = layout.buildDirectory.asFile.get()
.resolve("reports").absolutePath

jvmTarget = "11"
jvmTarget = "1.8"
freeCompilerArgs += listOf(
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package com.aliucord.manager.di

import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.manager.download.*
import com.aliucord.manager.patcher.util.Signer.getKoin
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
import org.koin.core.annotation.KoinInternalApi
import org.koin.core.component.KoinComponent
import kotlin.reflect.KClass

/**
* Handle providing the correct install manager based on preferences and device type.
*/
class DownloadManagerProvider(private val prefs: PreferencesManager) {
class DownloadManagerProvider(private val prefs: PreferencesManager) : KoinComponent {
fun getActiveDownloader(): IDownloadManager =
getDownloader(prefs.downloader)

Expand Down
30 changes: 18 additions & 12 deletions app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.aliucord.manager.manager

import android.content.Context
import android.os.Environment
import android.util.Log
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.network.utils.SemVer
import java.io.File

Expand Down Expand Up @@ -30,23 +32,27 @@ class PathManager(context: Context) {
*/
val keystoreFile = aliucordDir.resolve("ks.keystore")

private val externalCacheDir = context.externalCacheDir
?: throw Error("External cache directory isn't supported")
private val cacheDir = run {
if (context.externalCacheDir == null)
Log.i(BuildConfig.TAG, "No external cache directory, resorting to internal cache!")

context.externalCacheDir ?: context.cacheDir
}

/**
* Standard path: `~/Android/data/com.aliucord.manager/cache`
*/
private val discordApkCache = externalCacheDir
private val discordApkCache = cacheDir
.resolve("discord")

/**
* Delete the entire cache dir and recreate it.
*/
fun clearCache() {
if (!externalCacheDir.deleteRecursively())
if (!cacheDir.deleteRecursively())
throw IllegalStateException("Failed to delete cache")

externalCacheDir.mkdirs()
cacheDir.mkdirs()
}

/**
Expand All @@ -59,45 +65,45 @@ class PathManager(context: Context) {
/**
* Resolve a specific path for a cached injector.
*/
fun cachedInjectorDex(version: SemVer, custom: Boolean = false) = externalCacheDir
fun cachedInjectorDex(version: SemVer, custom: Boolean = false) = cacheDir
.resolve("injector").apply { mkdirs() }
.resolve("$version${if (custom) ".custom" else ""}.dex")

/**
* Get all the versions of custom injector builds.
*/
fun customInjectorDexs() = listCustomFiles(externalCacheDir.resolve("injector"))
fun customInjectorDexs() = listCustomFiles(cacheDir.resolve("injector"))

/**
* Resolve a specific path for a versioned cached Aliuhook build
*/
fun cachedAliuhookAAR(version: String) = externalCacheDir
fun cachedAliuhookAAR(version: String) = cacheDir
.resolve("aliuhook").apply { mkdirs() }
.resolve("$version.aar")

/**
* Resolve a specific path for a versioned smali patches archive.
*/
fun cachedSmaliPatches(version: SemVer, custom: Boolean = false) = externalCacheDir
fun cachedSmaliPatches(version: SemVer, custom: Boolean = false) = cacheDir
.resolve("patches").apply { mkdirs() }
.resolve("$version${if (custom) ".custom" else ""}.zip")

/**
* Get all the versions of custom smali bundles.
*/
fun customSmaliPatches() = listCustomFiles(externalCacheDir.resolve("patches"))
fun customSmaliPatches() = listCustomFiles(cacheDir.resolve("patches"))

/**
* Singular Kotlin file of the most up-to-date version
* since the stdlib is backwards compatible.
*/
fun cachedKotlinDex() = externalCacheDir
fun cachedKotlinDex() = cacheDir
.resolve("kotlin.dex")

/**
* The temporary working directory of a currently executing patching process.
*/
fun patchingWorkingDir() = externalCacheDir
fun patchingWorkingDir() = cacheDir
.resolve("patched")

private companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.aliucord.manager.patcher.signing

import com.aliucord.manager.manager.PathManager
import com.android.apksig.ApkSigner
import com.android.apksig.KeyConfig
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.File
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.util.Date
import java.util.Locale

object ApkSigSigner : KoinComponent {
private val paths = get<PathManager>()
private val password = "password".toCharArray()

private val signerConfig: ApkSigner.SignerConfig by lazy {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())

// Create new keystore if it doesn't exist
if (!paths.keystoreFile.exists()) {
paths.aliucordDir.mkdirs()
newKeystore(paths.keystoreFile)
}

paths.keystoreFile.inputStream()
.use { keyStore.load(it, /* password = */ null) }

val alias = keyStore.aliases().nextElement()
val certificate = keyStore.getCertificate(alias) as X509Certificate

ApkSigner.SignerConfig.Builder(
"Aliucord Manager signer",
KeyConfig.Jca(keyStore.getKey(alias, password) as PrivateKey),
listOf(certificate)
).build()
}

private fun newKeystore(out: File) {
val key = createKey()

with(KeyStore.getInstance(KeyStore.getDefaultType())) {
load(null, password)
setKeyEntry("alias", key.privateKey, password, arrayOf<Certificate>(key.publicKey))
store(out.outputStream(), password)
}
}

private fun createKey(): KeySet {
var serialNumber: BigInteger

do serialNumber = SecureRandom().nextInt().toBigInteger()
while (serialNumber < BigInteger.ZERO)

val x500Name = X500Name("CN=Aliucord Manager")
val pair = KeyPairGenerator.getInstance("RSA").run {
initialize(2048)
generateKeyPair()
}
val builder = X509v3CertificateBuilder(
/* issuer = */ x500Name,
/* serial = */ serialNumber,
/* notBefore = */ Date(0),
/* notAfter = */ Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), // 30 years
/* dateLocale = */ Locale.ENGLISH,
/* subject = */ x500Name,
/* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)

return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private)
}

fun signApk(apkFile: File) {
val tmpApk = apkFile.resolveSibling(apkFile.name + ".tmp")

ApkSigner.Builder(listOf(signerConfig))
.setV1SigningEnabled(false) // TODO: enable so api <24 devices can work, however zip-alignment breaks
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
.setInputApk(apkFile)
.setOutputApk(tmpApk)
.build()
.sign()

tmpApk.renameTo(apkFile)
}

private class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aliucord.manager.patcher.signing

import java.security.PrivateKey
import java.security.cert.X509Certificate

data class KeySet(
val publicKey: X509Certificate,
val privateKey: PrivateKey,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.aliucord.manager.patcher.steps.install

import com.aliucord.manager.R
import com.aliucord.manager.patcher.StepRunner
import com.aliucord.manager.patcher.signing.ApkSigSigner
import com.aliucord.manager.patcher.steps.StepGroup
import com.aliucord.manager.patcher.steps.base.Step
import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep
import com.aliucord.manager.patcher.util.Signer
import org.koin.core.component.KoinComponent

/**
Expand All @@ -18,6 +18,6 @@ class SigningStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk

Signer.signApk(apk)
ApkSigSigner.signApk(apk)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.aliucord.manager.patcher.steps.base.IDexProvider
import com.aliucord.manager.patcher.steps.base.Step
import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep
import com.aliucord.manager.patcher.steps.download.DownloadAliuhookStep
import com.github.diamondminer88.zip.ZipCompression
import com.github.diamondminer88.zip.ZipReader
import com.github.diamondminer88.zip.ZipWriter
import org.koin.core.component.KoinComponent
Expand All @@ -31,7 +32,7 @@ class AddAliuhookLibsStep : Step(), KoinComponent {
val bytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read()
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")

patchedApk.writeEntry("lib/$currentDeviceArch/$libFile", bytes)
patchedApk.writeEntry("lib/$currentDeviceArch/$libFile", bytes, ZipCompression.NONE)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.aliucord.manager.patcher.steps.StepGroup
import com.aliucord.manager.patcher.steps.base.IDexProvider
import com.aliucord.manager.patcher.steps.base.Step
import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep
import com.github.diamondminer88.zip.ZipCompression
import com.github.diamondminer88.zip.ZipReader
import com.github.diamondminer88.zip.ZipWriter
import org.koin.core.component.KoinComponent
Expand Down Expand Up @@ -59,7 +60,7 @@ class ReorganizeDexStep : Step(), KoinComponent {
if (dexProvider.dexPriority <= 0) continue

for (dexBytes in dexProvider.getDexFiles()) {
zip.writeEntry(getDexName(idx++), dexBytes)
zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE)
}
}

Expand All @@ -70,7 +71,7 @@ class ReorganizeDexStep : Step(), KoinComponent {

val file = paths.patchingWorkingDir().resolve(getDexName(idx))
val bytes = file.readBytes()
zip.writeEntry(getDexName(dexCount + idx), bytes)
zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE)
}

dexCount += idx
Expand All @@ -80,7 +81,7 @@ class ReorganizeDexStep : Step(), KoinComponent {
if (dexProvider.dexPriority > 0) continue

for (dexBytes in dexProvider.getDexFiles()) {
zip.writeEntry(getDexName(dexCount++), dexBytes)
zip.writeEntry(getDexName(dexCount++), dexBytes, ZipCompression.NONE)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ object ManifestPatcher {
val nv = super.child(ns, name)

// Add MANAGE_EXTERNAL_STORAGE when necessary
if (addExternalStoragePerm) {
if (addExternalStoragePerm && Build.VERSION.SDK_INT >= 30) {
super
.child(null, "uses-permission")
.attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, Manifest.permission.MANAGE_EXTERNAL_STORAGE)
Expand All @@ -76,16 +76,12 @@ object ManifestPatcher {
}
}

"uses-sdk" -> object : NodeVisitor(nv) {
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
if (name == "targetSdkVersion") {
val version = if (Build.VERSION.SDK_INT >= 31) 30 else 28
super.attr(ns, name, resourceId, type, version)
} else {
super.attr(ns, name, resourceId, type, value)
}
}
}
"uses-sdk" -> ReplaceAttrsVisitor(
nv,
mapOf(
"targetSdkVersion" to if (Build.VERSION.SDK_INT >= 31) 30 else 28,
)
)

"application" -> object : ReplaceAttrsVisitor(
nv,
Expand Down Expand Up @@ -158,7 +154,7 @@ object ManifestPatcher {
super.attr(ANDROID_NAMESPACE, VM_SAFE_MODE, android.R.attr.vmSafeMode, TYPE_INT_BOOLEAN, 1)
}

if (addExtractNativeLibs) super.attr(
if (addExtractNativeLibs && Build.VERSION.SDK_INT >= 23) super.attr(
ANDROID_NAMESPACE,
EXTRACT_NATIVE_LIBS,
android.R.attr.extractNativeLibs,
Expand All @@ -179,24 +175,6 @@ object ManifestPatcher {
return writer.toByteArray()
}

fun renamePackage(
manifestBytes: ByteArray,
packageName: String,
): ByteArray {
val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter()

reader.accept(
object : AxmlVisitor(writer) {
override fun child(ns: String?, name: String?): ReplaceAttrsVisitor {
return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName))
}
}
)

return writer.toByteArray()
}

private open class ReplaceAttrsVisitor(
nv: NodeVisitor,
private val attrs: Map<String, Any>,
Expand Down
Loading
Loading