feat: Add installer selection and Root support

This commit adds the ability to select the installation method. It also
includes:

- Added root support using the `pm` command. - Integrated the Shizuku
installer. - Added a new Settings UI for choosing an installer.
This commit is contained in:
kmmiio99o 2025-11-10 16:35:14 +01:00
parent ad877dd434
commit b7dd20c21f
31 changed files with 1229 additions and 198 deletions

View file

@ -53,7 +53,7 @@ APK output: `app/build/outputs/apk/debug/app-debug.apk`.
You can fully tailor the appearance: You can fully tailor the appearance:
- App name: edit `app/src/main/res/values/strings.xml` — ensure the UI uses `@string/app_name`. - App name: edit `app/src/main/res/values/strings.xml` — ensure the UI uses `@string/app_name`.
- Icons: swap `app/src/main/res/drawable/ic_rounded_shiggy.xml` and `ic_discord_shiggy.xml` with your vector or PNG. - Icons: swap `app/src/main/res/drawable/ic_rounded_shiggy.xml` and `app/src/main/res/drawable/ic_discord_aliucord.xml` with your vector or PNG.
- Colors: use `colors.xml` to apply a new palette. - Colors: use `colors.xml` to apply a new palette.
- Glyph sizing: vector drawables support `<group android:scaleX="" android:scaleY="" android:translateX="" android:translateY="">`. To center an element after scaling, compute translation as: - Glyph sizing: vector drawables support `<group android:scaleX="" android:scaleY="" android:translateX="" android:translateY="">`. To center an element after scaling, compute translation as:

View file

@ -9,6 +9,7 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hiddenApi.refine)
} }
val isRelease = System.getenv("RELEASE")?.toBoolean() ?: false val isRelease = System.getenv("RELEASE")?.toBoolean() ?: false
@ -186,12 +187,16 @@ dependencies {
implementation(libs.binaryResources) implementation(libs.binaryResources)
implementation(libs.diff) implementation(libs.diff)
implementation(libs.microg) implementation(libs.microg)
implementation(libs.bundles.shizuku)
implementation(libs.smali) implementation(libs.smali)
implementation(libs.baksmali) implementation(libs.baksmali)
implementation(libs.compose.pipette) implementation(libs.compose.pipette)
implementation(libs.compose.shimmer) implementation(libs.compose.shimmer)
implementation(libs.zip) implementation(libs.zip)
compileOnly(libs.hiddenApi.stub)
implementation(libs.hiddenApi.refine)
implementation(libs.hiddenApi.bypass)
coreLibraryDesugaring(libs.desugaring) coreLibraryDesugaring(libs.desugaring)
} }

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -14,45 +13,48 @@
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name="com.aliucord.manager.ManagerApplication" android:name="com.aliucord.manager.ManagerApplication"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar"
>
android:label="@string/app_name" <receiver android:name="com.aliucord.manager.installers.pm.PMIntentReceiver" android:exported="false" />
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar">
<receiver
android:name="com.aliucord.manager.installers.pm.PMIntentReceiver"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true"
<meta-data >
android:name="android.support.FILE_PROVIDER_PATHS" <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" />
android:resource="@xml/provider_paths" />
</provider> </provider>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
/>
<activity <activity
android:name="com.aliucord.manager.MainActivity" android:name="com.aliucord.manager.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.AliucordManager.SplashScreen" android:theme="@style/Theme.AliucordManager.SplashScreen"
tools:ignore="DiscouragedApi,LockedOrientationActivity"> tools:ignore="DiscouragedApi,LockedOrientationActivity"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -20,6 +20,8 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
import com.aliucord.manager.ui.screens.plugins.PluginsModel import com.aliucord.manager.ui.screens.plugins.PluginsModel
import com.aliucord.manager.ui.screens.settings.SettingsModel import com.aliucord.manager.ui.screens.settings.SettingsModel
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
import com.aliucord.manager.installers.root.RootInstaller
import com.aliucord.manager.installers.shizuku.ShizukuInstaller
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
@ -54,11 +56,11 @@ class ManagerApplication : Application() {
// UI Models // UI Models
modules(module { modules(module {
factoryOf(::HomeModel) factory { HomeModel(get<Application>(), get<AliucordGithubService>(), get<AliucordMavenService>(), get<ShiggyGithubService>(), get<RNATrackerService>(), get<kotlinx.serialization.json.Json>(), get<com.aliucord.manager.manager.PreferencesManager>()) }
factoryOf(::PluginsModel) factoryOf(::PluginsModel)
factoryOf(::AboutModel) factoryOf(::AboutModel)
factoryOf(::PatchingScreenModel) factoryOf(::PatchingScreenModel)
factoryOf(::SettingsModel) factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) }
factoryOf(::PatchOptionsModel) factoryOf(::PatchOptionsModel)
factoryOf(::IconOptionsModel) factoryOf(::IconOptionsModel)
factoryOf(::LogScreenModel) factoryOf(::LogScreenModel)
@ -80,7 +82,9 @@ class ManagerApplication : Application() {
// Installers // Installers
modules(module { modules(module {
singleOf(::PMInstaller) single { PMInstaller(get()) }
single { RootInstaller() }
singleOf(::ShizukuInstaller)
}) })
} }

View file

@ -11,7 +11,7 @@ interface Installer {
* @param apks All APKs (including any splits) to merge into a single install. * @param apks All APKs (including any splits) to merge into a single install.
* @param silent If this is an update, then the update will occur without user interaction. * @param silent If this is an update, then the update will occur without user interaction.
*/ */
fun install(apks: List<File>, silent: Boolean = true) suspend fun install(apks: List<File>, silent: Boolean = true)
/** /**
* Starts an installation and waits for it to finish with a result. A toast will be shown for all result states. * Starts an installation and waits for it to finish with a result. A toast will be shown for all result states.

View file

@ -0,0 +1,7 @@
package com.aliucord.manager.installers
import android.os.Parcelable
interface InstallerError : Parcelable {
val message: String
}

View file

@ -1,17 +1,11 @@
package com.aliucord.manager.installers.pm package com.aliucord.manager.installers.pm
import android.app.Application import android.app.Application
import android.app.PendingIntent import android.content.pm.PackageInstaller
import android.content.Intent import android.content.pm.PackageManager
import android.content.pm.*
import android.content.pm.PackageInstaller.SessionParams
import android.os.Build
import android.os.Process
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
import com.aliucord.manager.installers.Installer
import com.aliucord.manager.util.isMiui
import dev.shiggy.manager.BuildConfig import dev.shiggy.manager.BuildConfig
import com.aliucord.manager.installers.Installer
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File import java.io.File
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -32,70 +26,64 @@ class PMInstaller(
} }
} }
override fun install(apks: List<File>, silent: Boolean) { override suspend fun install(apks: List<File>, silent: Boolean) {
startInstall(createInstallSession(silent), apks, false) val sessionId = createInstallSession(silent)
PMUtils.startInstall(
context = context,
session = _packageInstaller.openSession(sessionId),
sessionId = sessionId,
apks = apks,
relay = false,
)
} }
override suspend fun waitInstall(apks: List<File>, silent: Boolean) = suspendCancellableCoroutine { continuation -> override suspend fun waitInstall(apks: List<File>, silent: Boolean) = suspendCancellableCoroutine { continuation ->
// Create a new install session // Create a new install session
val sessionId = createInstallSession(silent) val sessionId = createInstallSession(silent)
// This will receive parsed data forwarded by PMIntentReceiver // Create and register a result receiver
val relayReceiver = PMResultReceiver( val relayReceiver = PMUtils.registerRelayReceiver(
context = context,
sessionId = sessionId, sessionId = sessionId,
isUninstall = false, isUninstall = false,
onResult = { continuation.resume(it) }, onResult = continuation::resume,
) )
ContextCompat.registerReceiver( // Unregister PMResultReceiver when this coroutine finishes or errors
/* context = */ context, // Explicitly cancel the install session if it did not finish.
/* receiver = */ relayReceiver, continuation.invokeOnCancellation {
/* filter = */ PMResultReceiver.intentFilter,
/* flags = */ ContextCompat.RECEIVER_NOT_EXPORTED,
)
// Unregister PMResultReceiver when this coroutine finishes or error occurs
// Additionally, cancel the install session entirely
continuation.invokeOnCancellation { error ->
context.unregisterReceiver(relayReceiver) context.unregisterReceiver(relayReceiver)
context.packageManager.packageInstaller.abandonSession(sessionId) _packageInstaller.abandonSession(sessionId)
} }
startInstall(sessionId, apks, relay = true) PMUtils.startInstall(
context = context,
session = _packageInstaller.openSession(sessionId),
sessionId = sessionId,
apks = apks,
relay = true,
)
} }
override suspend fun waitUninstall(packageName: String) = suspendCancellableCoroutine { continuation -> override suspend fun waitUninstall(packageName: String) = suspendCancellableCoroutine { continuation ->
val callbackIntent = Intent(context, PMIntentReceiver::class.java) // Create and register a result receiver
.putExtra(PMIntentReceiver.EXTRA_SESSION_ID, -1) val relayReceiver = PMUtils.registerRelayReceiver(
.putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, true) context = context,
val pendingIntent = PendingIntent.getBroadcast(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ callbackIntent,
/* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
// This will receive parsed data forwarded by PMIntentReceiver
val relayReceiver = PMResultReceiver(
sessionId = -1, sessionId = -1,
isUninstall = true, isUninstall = true,
onResult = { continuation.resume(it) }, onResult = continuation::resume,
) )
ContextCompat.registerReceiver( // Unregister PMResultReceiver when this coroutine finishes or errors
/* context = */ context, continuation.invokeOnCancellation {
/* receiver = */ relayReceiver,
/* filter = */ PMResultReceiver.intentFilter,
/* flags = */ ContextCompat.RECEIVER_NOT_EXPORTED,
)
// Unregister PMResultReceiver when this coroutine is cancelled early
continuation.invokeOnCancellation { error ->
context.unregisterReceiver(relayReceiver) context.unregisterReceiver(relayReceiver)
} }
_packageInstaller.uninstall(packageName, pendingIntent.intentSender) _packageInstaller.uninstall(
/* packageName = */ packageName,
/* statusReceiver = */ PMUtils.createUninstallRelayingIntent(context).intentSender,
)
} }
/** /**
@ -104,56 +92,6 @@ class PMInstaller(
* @return The open install session id. * @return The open install session id.
*/ */
private fun createInstallSession(silent: Boolean): Int { private fun createInstallSession(silent: Boolean): Int {
val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { return _packageInstaller.createSession(PMUtils.createInstallSessionParams(silent = silent))
setInstallLocation(PackageInfo.INSTALL_LOCATION_AUTO)
if (Build.VERSION.SDK_INT >= 24) setOriginatingUid(Process.myUid())
if (Build.VERSION.SDK_INT >= 26) setInstallReason(PackageManager.INSTALL_REASON_USER)
if (Build.VERSION.SDK_INT >= 31) {
setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST)
// Allegedly MIUI is not happy with silent installs
if (silent && !isMiui()) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
}
}
if (Build.VERSION.SDK_INT >= 34) setPackageSource(PackageInstaller.PACKAGE_SOURCE_OTHER)
}
return context.packageManager.packageInstaller.createSession(params)
}
/**
* Start a [PackageInstaller] session for installation.
* @param apks The apks to install
* @param relay Whether to use the [PMResultReceiver] flow.
* @return The [PendingIntent] that was registered to the [PackageInstaller.Session]
*/
private fun startInstall(sessionId: Int, apks: List<File>, relay: Boolean) {
val callbackIntent = Intent(context, PMIntentReceiver::class.java)
.putExtra(PMIntentReceiver.EXTRA_SESSION_ID, sessionId)
.putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, relay)
val pendingIntent = PendingIntent.getBroadcast(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ callbackIntent,
/* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
_packageInstaller.openSession(sessionId).use { session ->
val bufferSize = 1 * 1024 * 1024 // 1MiB
for (apk in apks) {
session.openWrite(apk.name, 0, apk.length()).use { outStream ->
apk.inputStream().use { it.copyTo(outStream, bufferSize) }
session.fsync(outStream)
}
}
session.commit(pendingIntent.intentSender)
}
} }
} }

View file

@ -0,0 +1,143 @@
package com.aliucord.manager.installers.pm
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.core.content.ContextCompat
import com.aliucord.manager.installers.InstallerResult
import com.aliucord.manager.util.isMiui
import java.io.File
/**
* Shared functionality between all types of installers that directly interface with the [PackageInstaller] API.
*/
object PMUtils {
/**
* Creates install sessions params for [PackageInstaller].
* @param silent If this is an update, then the update will occur without user interaction.
*/
@Suppress("DEPRECATION")
fun createInstallSessionParams(silent: Boolean): SessionParams {
return SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
if (Build.VERSION.SDK_INT >= 24) setOriginatingUid(Process.myUid())
if (Build.VERSION.SDK_INT >= 26) setInstallReason(PackageManager.INSTALL_REASON_USER)
if (Build.VERSION.SDK_INT >= 30) setAutoRevokePermissionsMode(false)
if (Build.VERSION.SDK_INT >= 31) {
setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST)
// Allegedly MIUI is not happy with silent installs
if (silent && !isMiui()) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
}
}
if (Build.VERSION.SDK_INT >= 34) setPackageSource(PackageInstaller.PACKAGE_SOURCE_OTHER)
}
}
/**
* Registers a [PMResultReceiver] to receive results relayed by [PMIntentReceiver], to return the state
* back to the application.
*
* @param context Android context.
* @param sessionId The [PackageInstaller] install session ID to filter for.
* @param isUninstall Whether this operation is an uninstallation.
* @param onResult A callback lambda providing the parsed installation result.
* @return The receiver that was registered. This should be unregistered manually, such as
* upon the cancellation of the registering coroutine.
*/
fun registerRelayReceiver(
context: Context,
sessionId: Int,
isUninstall: Boolean,
onResult: (InstallerResult) -> Unit,
): PMResultReceiver {
// This will receive parsed data forwarded by PMIntentReceiver
val relayReceiver = PMResultReceiver(
sessionId = sessionId,
isUninstall = isUninstall,
onResult = onResult,
)
ContextCompat.registerReceiver(
/* context = */ context,
/* receiver = */ relayReceiver,
/* filter = */ PMResultReceiver.intentFilter,
/* flags = */ ContextCompat.RECEIVER_NOT_EXPORTED,
)
return relayReceiver
}
// TODO: Install progress?
// PackageInstaller.Session#setStagingProgress
// PackageInstaller.SessionCallback#onProgressChanged(int, float)
/**
* Start a [PackageInstaller] session for installation.
* @param context Android context
* @param session A newly opened install session to be written to. Ones binded through Shizuku also work.
* @param sessionId The install session ID of [session].
* @param apks The apks to install
* @param relay Whether to use the [PMResultReceiver] flow.
*/
fun startInstall(
context: Context,
session: PackageInstaller.Session,
sessionId: Int,
apks: List<File>,
relay: Boolean,
) {
val callbackIntent = Intent(context, PMIntentReceiver::class.java)
.putExtra(PMIntentReceiver.EXTRA_SESSION_ID, sessionId)
.putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, relay)
val pendingIntent = PendingIntent.getBroadcast(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ callbackIntent,
/* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
session.use { session ->
val bufferSize = 1 * 1024 * 1024 // 1MiB
for (apk in apks) {
session.openWrite(apk.name, 0, apk.length()).use { outStream ->
apk.inputStream().use { it.copyTo(outStream, bufferSize) }
session.fsync(outStream)
}
}
@SuppressLint("RequestInstallPackagesPolicy")
session.commit(pendingIntent.intentSender)
}
}
/**
* Creates an uninstallation callback [PendingIntent] that will forward events
* to the relaying [PMIntentReceiver]. These events can be captured by registering [PMResultReceiver]
* through [registerRelayReceiver].
*/
fun createUninstallRelayingIntent(context: Context): PendingIntent {
// FIXME: Conflicting pending intents when multiple simultaneous uninstalls are happening.
// The extras will end up being merged into one pending intent, with only the newest one working.
val callbackIntent = Intent(context, PMIntentReceiver::class.java)
.putExtra(PMIntentReceiver.EXTRA_SESSION_ID, -1)
.putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, true)
return PendingIntent.getBroadcast(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ callbackIntent,
/* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
}

View file

@ -0,0 +1,75 @@
package com.aliucord.manager.installers.root
import com.aliucord.manager.installers.Installer
import com.aliucord.manager.installers.InstallerResult
import com.aliucord.manager.installers.InstallerResult.Error
import java.io.File
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.coroutines.runBlocking
class RootInstaller : Installer {
override suspend fun install(apks: List<File>, silent: Boolean) {
waitInstall(apks, silent)
}
override suspend fun waitInstall(apks: List<File>, silent: Boolean): InstallerResult {
// Create install session
val installCommand = StringBuilder("pm install-create")
if (silent) {
installCommand.append(" -r --user 0")
}
val output = execute(installCommand.toString())
val sessionId = output.substringAfter("Success: created install session [").substringBefore("]").toIntOrNull()
if (sessionId == null) {
println("RootInstaller: install-create failed: $output")
return PMInstallerError("Failed to create install session: $output")
}
// Write each APK to the session
for (apk in apks) {
val writeCmd = "pm install-write -S ${apk.length()} $sessionId ${apk.name} - < \"${apk.absolutePath}\""
val writeOutput = execute(writeCmd)
if (!writeOutput.contains("Success")) {
println("RootInstaller: install-write failed: $writeOutput")
execute("pm install-abandon $sessionId")
return PMInstallerError("Failed to write APK: $writeOutput")
}
}
// Commit the session
val commitOutput = execute("pm install-commit $sessionId")
if (!commitOutput.contains("Success")) {
println("RootInstaller: install-commit failed: $commitOutput")
return PMInstallerError("Failed to commit install session: $commitOutput")
}
return InstallerResult.Success
}
override suspend fun waitUninstall(packageName: String): InstallerResult {
val output = execute("pm uninstall $packageName")
if (!output.contains("Success")) {
println("RootInstaller: uninstall failed: $output")
return PMInstallerError("Failed to uninstall: $output")
}
return InstallerResult.Success
}
private fun execute(command: String): String {
return try {
val process = ProcessBuilder("su", "-c", command).start()
val output = process.inputStream.bufferedReader().readText()
process.waitFor()
output
} catch (e: Exception) {
println("RootInstaller: Exception during command execution: ${e.message}")
e.toString()
}
}
}
@Parcelize
data class PMInstallerError(private val reason: String) : Error(), Parcelable {
override fun getDebugReason(): String = reason
}

View file

@ -0,0 +1,278 @@
package com.aliucord.manager.installers.shizuku
import android.content.Context
import android.content.pm.*
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageInstallerHidden.SessionParamsHidden
import android.os.Build
import com.aliucord.manager.installers.Installer
import com.aliucord.manager.installers.InstallerResult
import com.aliucord.manager.installers.pm.PMUtils
import com.aliucord.manager.util.showToast
import dev.rikka.tools.refine.Refine
import dev.shiggy.manager.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.*
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.random.Random
// Based on https://github.com/Tobi823/ffupdater/blob/9830452fe1cb3b77b28175833c68118a63d5ca69/ffupdater/src/main/java/de/marmaro/krt/ffupdater/installer/impl/ShizukuInstaller.kt
class ShizukuInstaller(private val context: Context) : Installer {
private var shizukuPermissionLock = Mutex()
private val shizukuAvailable: StateFlow<Boolean?>
field = MutableStateFlow(null)
// Listeners
private val onBinderReceived = Shizuku.OnBinderReceivedListener {
shizukuAvailable.value = true
}
private val onBinderDead = Shizuku.OnBinderDeadListener {
shizukuAvailable.value = false
if (shizukuPermissionLock.isLocked)
shizukuPermissionLock.unlock()
}
init {
Shizuku.addBinderReceivedListenerSticky(onBinderReceived)
Shizuku.addBinderDeadListener(onBinderDead)
}
fun stop() {
Shizuku.removeBinderReceivedListener(onBinderReceived)
Shizuku.removeBinderDeadListener(onBinderDead)
}
/**
* Determines whether Shizuku is available and the binder has been retrieved.
*/
fun shizukuAvailable(): Boolean = when (shizukuAvailable.value) {
true -> true
false -> false
null -> {
Shizuku.pingBinder().also { shizukuAvailable.value = it }
}
}
/**
* Checks whether Shizuku permissions have been granted to this app.
*/
fun checkPermissions(): Boolean {
if (!shizukuAvailable()) return false
// Old shizuku does not have permission checks
if (Shizuku.isPreV11()) return true
return Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
}
/**
* Requests and waits for Shizuku permissions if they have not already been granted.
*/
suspend fun requestPermissions(): Boolean {
if (!shizukuAvailable()) return false
// Lock and check if the previous holder already obtained permissions
shizukuPermissionLock.lock()
try {
if (checkPermissions()) {
shizukuPermissionLock.unlock()
return true
}
} catch (_: Exception) {
shizukuPermissionLock.unlock()
}
return suspendCancellableCoroutine { continuation ->
val currentRequestCode = Random.nextInt()
val onPermissionRequestResult = Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->
if (requestCode != currentRequestCode)
return@OnRequestPermissionResultListener
if (grantResult == PackageManager.PERMISSION_DENIED)
context.showToast(R.string.permissions_shizuku_denied)
continuation.resume(grantResult == PackageManager.PERMISSION_GRANTED)
}
continuation.invokeOnCancellation {
Shizuku.removeRequestPermissionResultListener(onPermissionRequestResult)
shizukuPermissionLock.unlock()
}
Shizuku.addRequestPermissionResultListener(onPermissionRequestResult)
Shizuku.requestPermission(currentRequestCode)
}
}
/**
* Gets the Shizuku binder for [IPackageInstaller].
*/
private fun getPackageInstallerBinder(): IPackageInstaller {
val iPackageManager = IPackageManager.Stub.asInterface(
ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
)
val iPackageInstaller = IPackageInstaller.Stub.asInterface(
ShizukuBinderWrapper(iPackageManager.packageInstaller.asBinder())
)
return iPackageInstaller
}
/**
* Gets a binded [PackageInstaller] service wrapper through Shizuku.
*/
private fun getPackageInstaller(): PackageInstaller {
val iPackageInstaller = getPackageInstallerBinder()
val hiddenPackageInstaller = if (Build.VERSION.SDK_INT >= 31) {
PackageInstallerHidden(
/* installer = */ iPackageInstaller,
/* installerPackageName = */ PLAY_PACKAGE_NAME,
/* installerAttributionTag = */ null,
/* userId = */ 0,
)
} else if (Build.VERSION.SDK_INT >= 26) {
PackageInstallerHidden(
/* installer = */ iPackageInstaller,
/* installerPackageName = */ PLAY_PACKAGE_NAME,
/* userId = */ 0,
)
} else {
PackageInstallerHidden(
/* context = */ context,
/* pm = */ context.packageManager,
/* installer = */ iPackageInstaller,
/* installerPackageName = */ PLAY_PACKAGE_NAME,
/* userId = */ 0,
)
}
return Refine.unsafeCast(hiddenPackageInstaller)
}
/**
* Opens and binds a [PackageInstaller.Session] wrapper through Shizuku.
*/
private fun openSession(sessionId: Int): PackageInstaller.Session {
val iPackageInstaller = getPackageInstallerBinder()
val iSession = IPackageInstallerSession.Stub.asInterface(
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder())
)
return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession))
}
/**
* Creates the [PackageInstaller] session params to be used with Shizuku installs.
*/
private fun createSessionParams(): SessionParams {
// Required to change hidden SessionParams flags
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !hiddenApiDisabled.getAndSet(true))
HiddenApiBypass.addHiddenApiExemptions("Landroid/content", "Landroid/os")
val params = PMUtils.createInstallSessionParams(silent = true)
// Add INSTALL_REPLACE_EXISTING flag to session params
val paramsHidden = Refine.unsafeCast<SessionParamsHidden>(params)
paramsHidden.installFlags = paramsHidden.installFlags or PackageManagerHidden.INSTALL_REPLACE_EXISTING
return params
}
override suspend fun install(apks: List<File>, silent: Boolean) {
if (!requestPermissions())
throw IllegalStateException("Shizuku is not available!")
// Construct install session and create it
val params = createSessionParams()
val packageInstaller = getPackageInstaller()
val sessionId = packageInstaller.createSession(params)
PMUtils.startInstall(
context = context,
session = openSession(sessionId),
sessionId = sessionId,
apks = apks,
relay = false,
)
}
override suspend fun waitInstall(apks: List<File>, silent: Boolean): InstallerResult {
if (!requestPermissions())
throw IllegalStateException("Shizuku is not available!")
return suspendCancellableCoroutine { continuation ->
// Construct install session and create it
val params = createSessionParams()
val packageInstaller = getPackageInstaller()
val sessionId = packageInstaller.createSession(params)
// Create and register a result receiver
val relayReceiver = PMUtils.registerRelayReceiver(
context = context,
sessionId = sessionId,
isUninstall = false,
onResult = continuation::resume,
)
// Unregister PMResultReceiver when this coroutine finishes or errors
// Explicitly cancel the install session if it did not finish.
continuation.invokeOnCancellation {
context.unregisterReceiver(relayReceiver)
packageInstaller.abandonSession(sessionId)
}
PMUtils.startInstall(
context = context,
session = openSession(sessionId),
sessionId = sessionId,
apks = apks,
relay = true,
)
}
}
override suspend fun waitUninstall(packageName: String): InstallerResult {
if (!requestPermissions())
throw IllegalStateException("Shizuku is not available!")
return suspendCancellableCoroutine { continuation ->
val packageInstaller = getPackageInstaller()
// Create and register a result receiver
val relayReceiver = PMUtils.registerRelayReceiver(
context = context,
sessionId = -1,
isUninstall = true,
onResult = continuation::resume,
)
// Unregister PMResultReceiver when this coroutine finishes or errors
continuation.invokeOnCancellation {
context.unregisterReceiver(relayReceiver)
}
packageInstaller.uninstall(
/* packageName = */ packageName,
/* statusReceiver = */ PMUtils.createUninstallRelayingIntent(context).intentSender,
)
}
}
private companion object {
// We spoof Google Play Store to prevent unnecessary checks
const val PLAY_PACKAGE_NAME = "com.android.vending"
/**
* Whether the hidden API exemptions have already been added for
* use in modifying hidden install session flags.
*/
var hiddenApiDisabled = AtomicBoolean(false)
}
}

View file

@ -0,0 +1,14 @@
package com.aliucord.manager.installers.shizuku
import android.content.Context
import android.os.Parcelable
import com.aliucord.manager.installers.InstallerResult
import kotlinx.parcelize.Parcelize
@Parcelize
data class ShizukuInstallerError(
private val reason: String
) : InstallerResult.Error(), Parcelable {
override fun getDebugReason(): String = reason
override fun getLocalizedReason(context: Context): String? = null
}

View file

@ -0,0 +1,50 @@
package com.aliucord.manager.installers.shizuku
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Bundle
import android.util.Log
import com.aliucord.manager.installers.shizuku.ShizukuInstallerError
import com.aliucord.manager.installers.InstallerResult
import dev.shiggy.manager.BuildConfig
class ShizukuIntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras = intent.extras ?: return
val status = extras.getInt(PackageInstaller.EXTRA_STATUS)
val message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Unknown"
val sessionId = extras.getInt(EXTRA_SESSION_ID)
if (BuildConfig.DEBUG) {
Log.d(
BuildConfig.TAG,
"Received install result for session $sessionId: " +
"status=$status, message=$message"
)
}
val result = when (status) {
PackageInstaller.STATUS_SUCCESS -> InstallerResult.Success
else -> ShizukuInstallerError("status=$status, message=$message")
}
if (extras.getBoolean(EXTRA_RELAY_ENABLED)) {
val resultIntent = Intent(context, ShizukuResultReceiver::class.java).apply {
putExtras(Bundle().apply {
putInt(EXTRA_SESSION_ID, sessionId)
putParcelable(EXTRA_RESULT, result)
})
}
context.sendBroadcast(resultIntent)
}
}
companion object {
const val EXTRA_SESSION_ID = "com.aliucord.manager.intent.extra.SESSION_ID"
const val EXTRA_RELAY_ENABLED = "com.aliucord.manager.intent.extra.RELAY_ENABLED"
const val EXTRA_RESULT = "com.aliucord.manager.intent.extra.RESULT"
}
}

View file

@ -0,0 +1,29 @@
package com.aliucord.manager.installers.shizuku
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.aliucord.manager.installers.InstallerResult
class ShizukuResultReceiver(
private val sessionId: Int,
private val isUninstall: Boolean,
private val onResult: (InstallerResult) -> Unit
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras = intent.extras ?: return
val receivedSessionId = extras.getInt(ShizukuIntentReceiver.EXTRA_SESSION_ID)
if (receivedSessionId == sessionId) {
val result = extras.getParcelable<InstallerResult>(ShizukuIntentReceiver.EXTRA_RESULT)
if (result != null) {
onResult(result)
}
}
}
companion object {
val intentFilter = IntentFilter("com.aliucord.manager.INSTALL_RESULT")
}
}

View file

@ -2,8 +2,11 @@ package com.aliucord.manager.manager
import com.aliucord.manager.installers.Installer import com.aliucord.manager.installers.Installer
import com.aliucord.manager.installers.pm.PMInstaller import com.aliucord.manager.installers.pm.PMInstaller
import org.koin.core.annotation.KoinInternalApi import com.aliucord.manager.installers.root.RootInstaller
import com.aliucord.manager.installers.shizuku.ShizukuInstaller
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import rikka.shizuku.Shizuku
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -15,15 +18,44 @@ class InstallerManager(
fun getActiveInstaller(): Installer = fun getActiveInstaller(): Installer =
getInstaller(prefs.installer) getInstaller(prefs.installer)
@OptIn(KoinInternalApi::class) fun getInstaller(type: InstallerSetting): Installer = when (type) {
fun getInstaller(type: InstallerSetting): Installer = InstallerSetting.PM -> get<PMInstaller>()
getKoin().scopeRegistry.rootScope.get(clazz = type.installerClass) InstallerSetting.ROOT -> get<RootInstaller>()
InstallerSetting.Shizuku -> get<ShizukuInstaller>()
}
fun getAvailableInstallers(): List<InstallerSetting> {
val installers = mutableListOf(InstallerSetting.PM)
try {
if (Shizuku.pingBinder()) {
installers.add(InstallerSetting.Shizuku)
}
} catch (e: Exception) {
// Shizuku not available
}
try {
val process = ProcessBuilder("su", "-c", "echo").start()
process.waitFor()
if (process.exitValue() == 0) {
installers.add(InstallerSetting.ROOT)
}
} catch (e: Exception) {
// Root not available
}
return installers
}
} }
enum class InstallerSetting( enum class InstallerSetting(
// @StringRes
// private val localizedName: Int,
val installerClass: KClass<out Installer>, val installerClass: KClass<out Installer>,
) { ) {
PM(PMInstaller::class), PM(PMInstaller::class),
ROOT(RootInstaller::class),
Shizuku(ShizukuInstaller::class),
}
fun InstallerSetting.toDisplayName(): String = when (this) {
InstallerSetting.PM -> "Package Manager"
InstallerSetting.ROOT -> "Root"
InstallerSetting.Shizuku -> "Shizuku"
} }

View file

@ -14,4 +14,5 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var keepPatchedApks by booleanPreference("keep_patched_apks", false) var keepPatchedApks by booleanPreference("keep_patched_apks", false)
var showNetworkWarning by booleanPreference("show_network_warning", true) var showNetworkWarning by booleanPreference("show_network_warning", true)
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true) var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
var rootPopupShown by booleanPreference("root_popup_shown", false)
} }

View file

@ -53,6 +53,10 @@ open class InstallStep(private val options: PatchOptions) : Step(), KoinComponen
// Wait until app resumed // Wait until app resumed
ProcessLifecycleOwner.get().lifecycle.withResumed {} ProcessLifecycleOwner.get().lifecycle.withResumed {}
// Retrieve configured installer
container.log("Retrieving configured installer ${prefs.installer}")
val installer = installers.getActiveInstaller()
// Show [PlayProtectDialog] and wait until it gets dismissed // Show [PlayProtectDialog] and wait until it gets dismissed
if (prefs.showPlayProtectWarning if (prefs.showPlayProtectWarning
&& !prefs.devMode && !prefs.devMode
@ -67,7 +71,7 @@ open class InstallStep(private val options: PatchOptions) : Step(), KoinComponen
} }
container.log("Installing ${apks.joinToString(", ") { it.name }}, silent: ${!prefs.devMode}") container.log("Installing ${apks.joinToString(", ") { it.name }}, silent: ${!prefs.devMode}")
val result = installers.getActiveInstaller().waitInstall( val result = installer.waitInstall(
apks = apks, apks = apks,
silent = !prefs.devMode, silent = !prefs.devMode,
) )

View file

@ -35,6 +35,7 @@ private fun HomeScreenLoadedPreview(
onOpenApp = {}, onOpenApp = {},
onOpenAppInfo = {}, onOpenAppInfo = {},
onOpenPlugins = {}, onOpenPlugins = {},
onUninstall = {},
) )
} }
} }

View file

@ -39,11 +39,15 @@ class HomeModel(
private val maven: AliucordMavenService, private val maven: AliucordMavenService,
private val wtGithub: ShiggyGithubService, private val wtGithub: ShiggyGithubService,
private val rnaTracker: RNATrackerService, private val rnaTracker: RNATrackerService,
private val json: Json, private val json: kotlinx.serialization.json.Json,
private val preferences: com.aliucord.manager.manager.PreferencesManager,
) : ScreenModel { ) : ScreenModel {
var installsState by mutableStateOf<InstallsState>(InstallsState.Fetching) var installsState by mutableStateOf<InstallsState>(InstallsState.Fetching)
private set private set
var showRootDetectedDialog by mutableStateOf(false)
var rootDetected by mutableStateOf(false)
private val refreshingLock = Mutex() private val refreshingLock = Mutex()
private var remoteDataJson: BuildInfo? = null private var remoteDataJson: BuildInfo? = null
private var trackerIndexJson: RNATrackerIndex? = null private var trackerIndexJson: RNATrackerIndex? = null
@ -52,6 +56,7 @@ class HomeModel(
init { init {
refresh() refresh()
checkAndSetRootInstaller()
} }
fun refresh(delay: Boolean = false) = screenModelScope.launchIO { fun refresh(delay: Boolean = false) = screenModelScope.launchIO {
@ -82,6 +87,14 @@ class HomeModel(
} }
} }
fun uninstallApp(packageName: String) {
val uninstallIntent = Intent(Intent.ACTION_DELETE).apply {
data = "package:$packageName".toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(uninstallIntent)
}
fun openApp(packageName: String) { fun openApp(packageName: String) {
val launchIntent = application.packageManager val launchIntent = application.packageManager
.getLaunchIntentForPackage(packageName) .getLaunchIntentForPackage(packageName)
@ -216,13 +229,40 @@ class HomeModel(
) )
}, },
).joinAll() ).joinAll()
if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestShiggyXposedVersion == null) { if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestShiggyXposedVersion == null) {
mainThread { application.showToast(R.string.home_network_fail) } mainThread { application.showToast(R.string.home_network_fail) }
} }
} }
private fun isRootAvailable(): Boolean {
return try {
val process = ProcessBuilder("su", "-c", "echo rooted").start()
val output = process.inputStream.bufferedReader().readText().trim()
process.waitFor()
output == "rooted"
} catch (e: Exception) {
false
}
}
private fun checkAndSetRootInstaller() {
if (isRootAvailable() && !rootDetected) {
rootDetected = true
// Only show dialog and auto-switch if the popup hasn't been shown before
if (!preferences.rootPopupShown) {
showRootDetectedDialog = true
preferences.rootPopupShown = true
// Switch installer to ROOT only the first time
if (preferences.installer != com.aliucord.manager.manager.InstallerSetting.ROOT) {
preferences.installer = com.aliucord.manager.manager.InstallerSetting.ROOT
}
}
}
}
/** /**
* Obtains all installed packages on the device that are an Aliucord installation. * Obtains all installed packages on the device that are an Aliucord installation.
*/ */

View file

@ -49,6 +49,12 @@ class HomeScreen : Screen, Parcelable {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val model = koinScreenModel<HomeModel>() val model = koinScreenModel<HomeModel>()
if (model.showRootDetectedDialog) {
com.aliucord.manager.ui.screens.settings.components.RootDetectedDialog(
onDismissRequest = { model.showRootDetectedDialog = false }
)
}
// Refresh installations list when the screen changes or activity resumes // Refresh installations list when the screen changes or activity resumes
LifecycleResumeEffect(Unit) { LifecycleResumeEffect(Unit) {
model.refresh(delay = true) model.refresh(delay = true)
@ -73,6 +79,7 @@ class HomeScreen : Screen, Parcelable {
onOpenApp = model::openApp, onOpenApp = model::openApp,
onOpenAppInfo = model::openAppInfo, onOpenAppInfo = model::openAppInfo,
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
onUninstall = model::uninstallApp,
) )
InstallsState.Fetching -> HomeScreenLoadingContent(padding = padding) InstallsState.Fetching -> HomeScreenLoadingContent(padding = padding)
@ -123,6 +130,7 @@ fun HomeScreenLoadedContent(
onOpenApp: (packageName: String) -> Unit, onOpenApp: (packageName: String) -> Unit,
onOpenAppInfo: (packageName: String) -> Unit, onOpenAppInfo: (packageName: String) -> Unit,
onOpenPlugins: (packageName: String) -> Unit, onOpenPlugins: (packageName: String) -> Unit,
onUninstall: (packageName: String) -> Unit,
) { ) {
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
@ -156,6 +164,7 @@ fun HomeScreenLoadedContent(
onOpenApp = { onOpenApp(item.packageName) }, onOpenApp = { onOpenApp(item.packageName) },
onOpenInfo = { onOpenAppInfo(item.packageName) }, onOpenInfo = { onOpenAppInfo(item.packageName) },
onOpenPlugins = { onOpenPlugins(item.packageName) }, onOpenPlugins = { onOpenPlugins(item.packageName) },
onUninstall = { onUninstall(item.packageName) },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }

View file

@ -27,6 +27,7 @@ fun InstalledItemCard(
onOpenApp: () -> Unit, onOpenApp: () -> Unit,
onOpenInfo: () -> Unit, onOpenInfo: () -> Unit,
onOpenPlugins: () -> Unit, onOpenPlugins: () -> Unit,
onUninstall: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ElevatedCard( ElevatedCard(
@ -123,6 +124,13 @@ fun InstalledItemCard(
text = stringResource(R.string.action_launch), text = stringResource(R.string.action_launch),
onClick = onOpenApp, onClick = onOpenApp,
) )
SegmentedButton(
icon = painterResource(R.drawable.ic_delete_forever),
text = stringResource(R.string.action_uninstall), // Assuming R.string.action_uninstall exists or will be added
iconColor = MaterialTheme.colorScheme.error,
textColor = MaterialTheme.colorScheme.error,
onClick = onUninstall,
)
} }
} }
} }

View file

@ -1,11 +1,13 @@
package com.aliucord.manager.ui.screens.settings package com.aliucord.manager.ui.screens.settings
import android.app.Application import android.app.Application
import android.net.Uri
import androidx.compose.runtime.* import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.manager.PathManager import com.aliucord.manager.manager.PathManager
import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.manager.InstallerManager
import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.ui.theme.Theme
import com.aliucord.manager.util.* import com.aliucord.manager.util.*
import dev.shiggy.manager.BuildConfig import dev.shiggy.manager.BuildConfig
@ -15,15 +17,19 @@ class SettingsModel(
private val application: Application, private val application: Application,
private val paths: PathManager, private val paths: PathManager,
val preferences: PreferencesManager, val preferences: PreferencesManager,
private val installerManager: InstallerManager,
) : ScreenModel { ) : ScreenModel {
val installInfo = InstallInfo val installInfo = InstallInfo
var patchedApkExists by mutableStateOf(paths.patchedApk().exists()) var patchedApkExists by mutableStateOf(paths.patchedApk().exists())
private set private set
var showThemeDialog by mutableStateOf(false) var showThemeDialog by mutableStateOf(false)
private set private set
val availableInstallers = installerManager.getAvailableInstallers()
fun showThemeDialog() { fun showThemeDialog() {
showThemeDialog = true showThemeDialog = true
} }
@ -36,6 +42,10 @@ class SettingsModel(
preferences.theme = theme preferences.theme = theme
} }
fun setInstaller(installer: com.aliucord.manager.manager.InstallerSetting) {
preferences.installer = installer
}
fun setKeepPatchedApks(value: Boolean) { fun setKeepPatchedApks(value: Boolean) {
preferences.keepPatchedApks = value preferences.keepPatchedApks = value
} }
@ -54,35 +64,37 @@ class SettingsModel(
application.showToast(R.string.action_copied) application.showToast(R.string.action_copied)
} }
fun shareApk() { fun shareApk(uriToShare: android.net.Uri? = null) {
// TODO(Shiggy): Implement this val fileUri = uriToShare ?: run {
application.showToast(R.string.settings_export_apk_not_implemented) val file = paths.patchedApk()
// val file = paths.patchingWorkingDir().resolve("patched.apk") if (!file.exists()) {
// val fileUri = FileProvider.getUriForFile( application.showToast(R.string.settings_export_apk_not_implemented)
// /* context = */ application, return
// /* authority = */ "${BuildConfig.APPLICATION_ID}.provider", }
// /* file = */ file, try {
// /* displayName = */ "Aliucord.apk", androidx.core.content.FileProvider.getUriForFile(
// ) application,
// "${BuildConfig.APPLICATION_ID}.provider",
// val intent = Intent(Intent.ACTION_SEND) file
// .setType("application/vnd.android.package-archive") )
// .putExtra(Intent.EXTRA_STREAM, fileUri) } catch (t: Throwable) {
// .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) application.showToast(R.string.status_failed)
// .let { return
// Intent.createChooser( }
// /* target = */ it, }
// /* title = */ application.getString(R.string.log_action_export_apk),
// ) val intent = android.content.Intent(android.content.Intent.ACTION_SEND)
// } .setType("application/vnd.android.package-archive")
// .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(android.content.Intent.EXTRA_STREAM, fileUri)
// .addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
// try { .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
// application.startActivity(intent)
// } catch (t: Throwable) { try {
// Log.w(BuildConfig.TAG, "Failed to share APK", t) application.startActivity(intent)
// application.showToast(R.string.status_failed) } catch (t: Throwable) {
// } android.util.Log.w(BuildConfig.TAG, "Failed to share APK", t)
application.showToast(R.string.status_failed)
}
} }
companion object { companion object {

View file

@ -20,13 +20,22 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.manager.InstallerManager
import com.aliucord.manager.ui.components.BackButton import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.components.MainActionButton import com.aliucord.manager.ui.components.MainActionButton
import com.aliucord.manager.ui.components.settings.* import com.aliucord.manager.ui.components.settings.*
import com.aliucord.manager.ui.screens.settings.components.ThemeDialog import com.aliucord.manager.ui.screens.settings.components.ThemeDialog
import com.aliucord.manager.ui.screens.settings.components.InstallerDialog
import com.aliucord.manager.ui.screens.settings.components.ShizukuInfoDialog
import com.aliucord.manager.manager.InstallerSetting
import com.aliucord.manager.manager.toDisplayName
import dev.shiggy.manager.R import dev.shiggy.manager.R
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import android.content.pm.PackageManager
import rikka.shizuku.Shizuku
import androidx.compose.ui.platform.LocalContext
@Parcelize @Parcelize
class SettingsScreen : Screen, Parcelable { class SettingsScreen : Screen, Parcelable {
@ -38,6 +47,7 @@ class SettingsScreen : Screen, Parcelable {
val model = koinScreenModel<SettingsModel>() val model = koinScreenModel<SettingsModel>()
var clearedCache by rememberSaveable { mutableStateOf(false) } var clearedCache by rememberSaveable { mutableStateOf(false) }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -70,10 +80,24 @@ class SettingsScreen : Screen, Parcelable {
secondaryText = { Text(stringResource(R.string.setting_theme_desc)) } secondaryText = { Text(stringResource(R.string.setting_theme_desc)) }
) { ) {
FilledTonalButton(onClick = model::showThemeDialog) { FilledTonalButton(onClick = model::showThemeDialog) {
Icon(
painter = painterResource(
when (preferences.theme) {
com.aliucord.manager.ui.theme.Theme.Dark -> R.drawable.ic_night
com.aliucord.manager.ui.theme.Theme.Light -> R.drawable.ic_light
com.aliucord.manager.ui.theme.Theme.System -> R.drawable.ic_sync
}
),
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(18.dp),
)
Text(preferences.theme.toDisplayName()) Text(preferences.theme.toDisplayName())
} }
} }
// Material You theming on Android 12+ // Material You theming on Android 12+
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
SettingsSwitch( SettingsSwitch(
@ -87,6 +111,85 @@ class SettingsScreen : Screen, Parcelable {
SettingsHeader(stringResource(R.string.settings_header_advanced)) SettingsHeader(stringResource(R.string.settings_header_advanced))
var showInstallerDialog by remember { mutableStateOf(false) }
var showShizukuInfoDialog by remember { mutableStateOf(false) }
var shouldRequestShizuku by remember { mutableStateOf(false) }
val context = LocalContext.current
if (shouldRequestShizuku) {
LaunchedEffect(shouldRequestShizuku) {
val SHIZUKU_REQUEST_CODE = 1001
try {
rikka.shizuku.Shizuku.requestPermission(SHIZUKU_REQUEST_CODE)
kotlinx.coroutines.delay(800)
if (rikka.shizuku.Shizuku.checkSelfPermission() != 0) {
showShizukuInfoDialog = true
}
} catch (_: Exception) {
showShizukuInfoDialog = true
}
shouldRequestShizuku = false
}
}
if (showInstallerDialog) {
InstallerDialog(
currentInstaller = preferences.installer,
onDismissRequest = { showInstallerDialog = false },
onConfirm = { installer ->
model.setInstaller(installer)
if (installer == InstallerSetting.Shizuku) {
shouldRequestShizuku = true
}
}
)
}
if (showShizukuInfoDialog) {
ShizukuInfoDialog(onDismissRequest = { showShizukuInfoDialog = false })
}
SettingsItem(
modifier = Modifier.clickable { showInstallerDialog = true },
icon = { Icon(painterResource(R.drawable.ic_tools), null) },
text = { Text(stringResource(R.string.setting_installer)) },
secondaryText = { Text(stringResource(R.string.setting_installer_desc)) }
) {
FilledTonalButton(onClick = { showInstallerDialog = true }) {
Icon(
painter = painterResource(
when (preferences.installer) {
InstallerSetting.PM -> R.drawable.ic_app_shortcut
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
}
),
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(18.dp),
)
Text(preferences.installer.toDisplayName())
}
}
MainActionButton(
text = stringResource(R.string.settings_clear_cache),
icon = painterResource(R.drawable.ic_delete_forever),
enabled = !clearedCache,
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.error,
),
onClick = {
clearedCache = true
model.clearCache()
},
modifier = Modifier
.padding(horizontal = 18.dp, vertical = 10.dp)
.fillMaxWidth()
)
SettingsHeader("Development")
SettingsSwitch( SettingsSwitch(
label = stringResource(R.string.setting_developer_options), label = stringResource(R.string.setting_developer_options),
secondaryLabel = stringResource(R.string.setting_developer_options_desc), secondaryLabel = stringResource(R.string.setting_developer_options_desc),
@ -116,22 +219,6 @@ class SettingsScreen : Screen, Parcelable {
) )
} }
MainActionButton(
text = stringResource(R.string.settings_clear_cache),
icon = painterResource(R.drawable.ic_delete_forever),
enabled = !clearedCache,
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.error,
),
onClick = {
clearedCache = true
model.clearCache()
},
modifier = Modifier
.padding(horizontal = 18.dp, vertical = 10.dp)
.fillMaxWidth()
)
SettingsHeader(stringResource(R.string.settings_header_info)) SettingsHeader(stringResource(R.string.settings_header_info))
Row( Row(

View file

@ -0,0 +1,97 @@
package com.aliucord.manager.ui.screens.settings.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aliucord.manager.manager.InstallerSetting
import com.aliucord.manager.manager.toDisplayName
import dev.shiggy.manager.R
@Composable
fun InstallerDialog(
currentInstaller: InstallerSetting,
onDismissRequest: () -> Unit,
onConfirm: (InstallerSetting) -> Unit,
) {
var selectedInstaller by rememberSaveable { mutableStateOf(currentInstaller) }
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
painter = painterResource(R.drawable.ic_tools),
contentDescription = stringResource(R.string.setting_installer),
modifier = Modifier.size(32.dp),
)
},
title = { Text(stringResource(R.string.setting_installer)) },
text = {
Column {
for (installer in InstallerSetting.values()) key(installer) {
val interactionSource = remember(::MutableInteractionSource)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = null,
interactionSource = interactionSource,
onClick = { selectedInstaller = installer },
)
.clip(MaterialTheme.shapes.medium)
.padding(horizontal = 6.dp, vertical = 8.dp),
) {
// Optionally, you can add a unique icon for each installer type
Icon(
painter = painterResource(
when (installer) {
InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
}
),
contentDescription = null,
modifier = Modifier
.padding(end = 14.dp)
.size(26.dp),
)
Text(
text = installer.toDisplayName(),
style = MaterialTheme.typography.labelLarge
.copy(fontSize = 14.sp)
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = installer == selectedInstaller,
onClick = { selectedInstaller = installer },
interactionSource = interactionSource,
)
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(selectedInstaller)
onDismissRequest()
},
) {
Text(stringResource(R.string.action_apply))
}
},
)
}

View file

@ -0,0 +1,63 @@
package com.aliucord.manager.ui.screens.settings.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.shiggy.manager.R
@Composable
fun RootDetectedDialog(
onDismissRequest: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
painter = painterResource(R.drawable.ic_code),
contentDescription = "Root",
modifier = Modifier
.size(48.dp)
.padding(bottom = 8.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
"Root Access Detected",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 4.dp)
)
},
text = {
Column(
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
) {
Text(
"Root permissions were found on your device.",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
Text(
"The installer has been automatically switched to Root for the best compatibility and performance.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
},
confirmButton = {
Button(
onClick = onDismissRequest,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text("Okay", style = MaterialTheme.typography.titleMedium)
}
},
dismissButton = {}
)
}

View file

@ -0,0 +1,95 @@
package com.aliucord.manager.ui.screens.settings.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.shiggy.manager.R
@Composable
fun ShizukuInfoDialog(
onDismissRequest: () -> Unit
) {
val context = androidx.compose.ui.platform.LocalContext.current
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
painter = painterResource(R.drawable.ic_tools),
contentDescription = "Shizuku",
modifier = Modifier
.size(48.dp)
.padding(bottom = 8.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
"Shizuku Permission Required",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 4.dp)
)
},
text = {
Column(
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
) {
Text(
"To use Shizuku, you must manually grant permission:",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
Text(
"1. Open the Shizuku app\n" +
"2. Grant permissions in the Shizuku app\n" +
"3. Relaunch Shiggy Manager to apply changes\n\n",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
},
confirmButton = {
Column {
Button(
onClick = {
val intent = android.content.Intent().apply {
setClassName(
"moe.shizuku.manager.authorization.RequestPermissionActivity",
"moe.shizuku.privileged.api"
)
addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (_: Exception) {
val fallback = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
if (fallback != null) {
context.startActivity(fallback)
}
}
onDismissRequest()
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text("Open Shizuku App")
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onDismissRequest,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text("Okay")
}
}
},
dismissButton = {}
)
}

View file

@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aliucord.manager.installers.InstallerResult import com.aliucord.manager.installers.InstallerResult
import com.aliucord.manager.manager.InstallerManager import com.aliucord.manager.manager.InstallerManager
import com.aliucord.manager.manager.InstallerSetting
import com.aliucord.manager.manager.download.IDownloadManager import com.aliucord.manager.manager.download.IDownloadManager
import com.aliucord.manager.manager.download.KtorDownloadManager import com.aliucord.manager.manager.download.KtorDownloadManager
import com.aliucord.manager.network.services.AliucordGithubService import com.aliucord.manager.network.services.AliucordGithubService
@ -91,7 +90,7 @@ class UpdaterViewModel(
downloadProgress.value = null downloadProgress.value = null
val installer = installers.getInstaller(InstallerSetting.PM) val installer = installers.getActiveInstaller()
val installResult = installer.waitInstall( val installResult = installer.waitInstall(
apks = listOf(apkFile), apks = listOf(apkFile),
silent = true, silent = true,

View file

@ -0,0 +1,12 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
>
<path
android:fillColor="#FF3DDB85"
android:pathData="M40,720q9,-107 65.5,-197T256,380l-74,-128q-6,-9 -3,-19t13,-15q8,-5 18,-2t16,12l74,128q86,-36 180,-36t180,36l74,-128q6,-9 16,-12t18,2q10,5 13,15t-3,19l-74,128q94,53 150.5,143T920,720L40,720ZM280,610q21,0 35.5,-14.5T330,560q0,-21 -14.5,-35.5T280,510q-21,0 -35.5,14.5T230,560q0,21 14.5,35.5T280,610ZM680,610q21,0 35.5,-14.5T730,560q0,-21 -14.5,-35.5T680,510q-21,0 -35.5,14.5T630,560q0,21 14.5,35.5T680,610Z"
/>
</vector>

View file

@ -2,7 +2,7 @@
<string name="app_name">Shiggy Manager</string> <string name="app_name">Shiggy Manager</string>
<string name="app_description">A Discord mod built with simplicity and a seamless experience in mind.</string> <string name="app_description">A Discord mod built with simplicity and a seamless experience in mind.</string>
<string name="aliucord" translatable="false">ShiggyCord</string> <string name="aliucord" translatable="false">Aliucord</string>
<string name="discord" translatable="false">Discord</string> <string name="discord" translatable="false">Discord</string>
<string name="github" translatable="false">GitHub</string> <string name="github" translatable="false">GitHub</string>
<string name="support_server">Support Server</string> <string name="support_server">Support Server</string>
@ -53,6 +53,8 @@
<string name="setting_theme_desc">The theme to apply to this app</string> <string name="setting_theme_desc">The theme to apply to this app</string>
<string name="setting_dynamic_color">Dynamic color</string> <string name="setting_dynamic_color">Dynamic color</string>
<string name="setting_dynamic_color_desc">Enables Material You theming on Android 12+</string> <string name="setting_dynamic_color_desc">Enables Material You theming on Android 12+</string>
<string name="setting_installer">Installer</string>
<string name="setting_installer_desc">The method to use for installing Shiggy</string>
<string name="setting_developer_options">Developer options</string> <string name="setting_developer_options">Developer options</string>
<string name="setting_developer_options_desc">Don\'t enable this unless you know what it does! (yes, really)</string> <string name="setting_developer_options_desc">Don\'t enable this unless you know what it does! (yes, really)</string>
<string name="setting_keep_patched_apks">Keep patched APKs</string> <string name="setting_keep_patched_apks">Keep patched APKs</string>
@ -255,7 +257,8 @@
<string name="log_section_error">Error Stacktrace</string> <string name="log_section_error">Error Stacktrace</string>
<string name="log_section_log">Installation Log</string> <string name="log_section_log">Installation Log</string>
<string name="log_action_export">Export log</string> <string name="log_action_export">Export log</string>
<string name="log_action_export_apk">Export APK</string> <string name="log_action_export_apk">Export Patched APK</string>
<string name="settings_share_custom_apk">Share Custom APK</string>
<string name="log_action_share">Share log</string> <string name="log_action_share">Share log</string>
<string name="logs_title">Logs</string> <string name="logs_title">Logs</string>
@ -283,6 +286,8 @@
<string name="custom_component_picker_title">Custom Component</string> <string name="custom_component_picker_title">Custom Component</string>
<string name="custom_component_picker_desc">A custom local version for a patching component (%s) has been found. Would you like to use it?</string> <string name="custom_component_picker_desc">A custom local version for a patching component (%s) has been found. Would you like to use it?</string>
<string name="permissions_shizuku_denied">Failed to obtain Shizuku permissions</string>
<string name="fun_fact_prefix">Fun Fact: %s</string> <string name="fun_fact_prefix">Fun Fact: %s</string>
<string name="fun_fact_1">ShiggyCord on first release was meant to be custom Kettu build!</string> <string name="fun_fact_1">ShiggyCord on first release was meant to be custom Kettu build!</string>
<string name="fun_fact_2">The refreshed UI inside the settings was made from developer boredom</string> <string name="fun_fact_2">The refreshed UI inside the settings was made from developer boredom</string>

View file

@ -1,4 +1,5 @@
[versions] [versions]
shizuku = "13.1.5"
accompanist = "0.37.3" accompanist = "0.37.3"
agp = "8.11.0" agp = "8.11.0"
androidx-activity = "1.10.1" androidx-activity = "1.10.1"
@ -15,6 +16,9 @@ compose-material3 = "1.3.2"
compose-pipette = "1.0.1" compose-pipette = "1.0.1"
desugaring = "2.1.5" desugaring = "2.1.5"
diff = "4.16" diff = "4.16"
hiddenApi-bypass = "6.1"
hiddenApi-refine = "4.4.0"
hiddenApi-stub = "4.4.0"
koin = "4.1.0" koin = "4.1.0"
kotlin = "2.2.0" kotlin = "2.2.0"
kotlinx-immutable = "0.4.0" kotlinx-immutable = "0.4.0"
@ -27,6 +31,13 @@ voyager = "1.1.0-beta03"
zip = "2.3.0" zip = "2.3.0"
[libraries] [libraries]
# Shizuku + Utils
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
hiddenApi-bypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApi-bypass" }
hiddenApi-refine = { module = "dev.rikka.tools.refine:runtime", version.ref = "hiddenApi-refine" }
hiddenApi-stub = { module = "dev.rikka.hidden:stub", version.ref = "hiddenApi-stub" }
# Accompanist # Accompanist
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version = "0.36.0" } accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version = "0.36.0" }
@ -131,6 +142,10 @@ voyager = [
"voyager-navigator", "voyager-navigator",
"voyager-transitions", "voyager-transitions",
] ]
shizuku = [
"shizuku-api",
"shizuku-provider",
]
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
@ -138,3 +153,4 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
hiddenApi-refine = { id = "dev.rikka.tools.refine", version = "4.3.0" }

View file

@ -6,6 +6,7 @@ pluginManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://maven.rikka.app/releases") }
} }
} }
@ -17,6 +18,10 @@ dependencyResolutionManagement {
name = "aliucord" name = "aliucord"
url = uri("https://maven.aliucord.com/snapshots") url = uri("https://maven.aliucord.com/snapshots")
} }
maven {
name = "jitpack"
url = uri("https://jitpack.io")
}
} }
} }