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:
parent
ad877dd434
commit
b7dd20c21f
31 changed files with 1229 additions and 198 deletions
|
|
@ -53,7 +53,7 @@ APK output: `app/build/outputs/apk/debug/app-debug.apk`.
|
|||
You can fully tailor the appearance:
|
||||
|
||||
- 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.
|
||||
- Glyph sizing: vector drawables support `<group android:scaleX="" android:scaleY="" android:translateX="" android:translateY="">`. To center an element after scaling, compute translation as:
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ plugins {
|
|||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hiddenApi.refine)
|
||||
}
|
||||
|
||||
val isRelease = System.getenv("RELEASE")?.toBoolean() ?: false
|
||||
|
|
@ -186,12 +187,16 @@ dependencies {
|
|||
implementation(libs.binaryResources)
|
||||
implementation(libs.diff)
|
||||
implementation(libs.microg)
|
||||
implementation(libs.bundles.shizuku)
|
||||
implementation(libs.smali)
|
||||
implementation(libs.baksmali)
|
||||
implementation(libs.compose.pipette)
|
||||
implementation(libs.compose.shimmer)
|
||||
implementation(libs.zip)
|
||||
|
||||
compileOnly(libs.hiddenApi.stub)
|
||||
implementation(libs.hiddenApi.refine)
|
||||
implementation(libs.hiddenApi.bypass)
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="com.aliucord.manager.ManagerApplication"
|
||||
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:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
>
|
||||
|
||||
<receiver
|
||||
android:name="com.aliucord.manager.installers.pm.PMIntentReceiver"
|
||||
android:exported="false" />
|
||||
<receiver android:name="com.aliucord.manager.installers.pm.PMIntentReceiver" android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
android:grantUriPermissions="true"
|
||||
>
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" />
|
||||
</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
|
||||
android:name="com.aliucord.manager.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AliucordManager.SplashScreen"
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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.settings.SettingsModel
|
||||
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 org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
|
|
@ -54,11 +56,11 @@ class ManagerApplication : Application() {
|
|||
|
||||
// UI Models
|
||||
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(::AboutModel)
|
||||
factoryOf(::PatchingScreenModel)
|
||||
factoryOf(::SettingsModel)
|
||||
factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) }
|
||||
factoryOf(::PatchOptionsModel)
|
||||
factoryOf(::IconOptionsModel)
|
||||
factoryOf(::LogScreenModel)
|
||||
|
|
@ -80,7 +82,9 @@ class ManagerApplication : Application() {
|
|||
|
||||
// Installers
|
||||
modules(module {
|
||||
singleOf(::PMInstaller)
|
||||
single { PMInstaller(get()) }
|
||||
single { RootInstaller() }
|
||||
singleOf(::ShizukuInstaller)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface Installer {
|
|||
* @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.
|
||||
*/
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.aliucord.manager.installers
|
||||
|
||||
import android.os.Parcelable
|
||||
|
||||
interface InstallerError : Parcelable {
|
||||
val message: String
|
||||
}
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
package com.aliucord.manager.installers.pm
|
||||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.*
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
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 com.aliucord.manager.installers.Installer
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
|
|
@ -32,70 +26,64 @@ class PMInstaller(
|
|||
}
|
||||
}
|
||||
|
||||
override fun install(apks: List<File>, silent: Boolean) {
|
||||
startInstall(createInstallSession(silent), apks, false)
|
||||
override suspend fun install(apks: List<File>, silent: Boolean) {
|
||||
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 ->
|
||||
// Create a new install session
|
||||
val sessionId = createInstallSession(silent)
|
||||
|
||||
// This will receive parsed data forwarded by PMIntentReceiver
|
||||
val relayReceiver = PMResultReceiver(
|
||||
// Create and register a result receiver
|
||||
val relayReceiver = PMUtils.registerRelayReceiver(
|
||||
context = context,
|
||||
sessionId = sessionId,
|
||||
isUninstall = false,
|
||||
onResult = { continuation.resume(it) },
|
||||
onResult = continuation::resume,
|
||||
)
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
/* context = */ context,
|
||||
/* receiver = */ relayReceiver,
|
||||
/* 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 ->
|
||||
// Unregister PMResultReceiver when this coroutine finishes or errors
|
||||
// Explicitly cancel the install session if it did not finish.
|
||||
continuation.invokeOnCancellation {
|
||||
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 ->
|
||||
val callbackIntent = Intent(context, PMIntentReceiver::class.java)
|
||||
.putExtra(PMIntentReceiver.EXTRA_SESSION_ID, -1)
|
||||
.putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, true)
|
||||
|
||||
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(
|
||||
// Create and register a result receiver
|
||||
val relayReceiver = PMUtils.registerRelayReceiver(
|
||||
context = context,
|
||||
sessionId = -1,
|
||||
isUninstall = true,
|
||||
onResult = { continuation.resume(it) },
|
||||
onResult = continuation::resume,
|
||||
)
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
/* context = */ context,
|
||||
/* receiver = */ relayReceiver,
|
||||
/* filter = */ PMResultReceiver.intentFilter,
|
||||
/* flags = */ ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
|
||||
// Unregister PMResultReceiver when this coroutine is cancelled early
|
||||
continuation.invokeOnCancellation { error ->
|
||||
// Unregister PMResultReceiver when this coroutine finishes or errors
|
||||
continuation.invokeOnCancellation {
|
||||
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.
|
||||
*/
|
||||
private fun createInstallSession(silent: Boolean): Int {
|
||||
val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
|
||||
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)
|
||||
}
|
||||
return _packageInstaller.createSession(PMUtils.createInstallSessionParams(silent = silent))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,11 @@ package com.aliucord.manager.manager
|
|||
|
||||
import com.aliucord.manager.installers.Installer
|
||||
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.get
|
||||
import rikka.shizuku.Shizuku
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
|
|
@ -15,15 +18,44 @@ class InstallerManager(
|
|||
fun getActiveInstaller(): Installer =
|
||||
getInstaller(prefs.installer)
|
||||
|
||||
@OptIn(KoinInternalApi::class)
|
||||
fun getInstaller(type: InstallerSetting): Installer =
|
||||
getKoin().scopeRegistry.rootScope.get(clazz = type.installerClass)
|
||||
fun getInstaller(type: InstallerSetting): Installer = when (type) {
|
||||
InstallerSetting.PM -> get<PMInstaller>()
|
||||
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(
|
||||
// @StringRes
|
||||
// private val localizedName: Int,
|
||||
val installerClass: KClass<out Installer>,
|
||||
) {
|
||||
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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
|
|||
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
||||
var showNetworkWarning by booleanPreference("show_network_warning", true)
|
||||
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
|
||||
var rootPopupShown by booleanPreference("root_popup_shown", false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ open class InstallStep(private val options: PatchOptions) : Step(), KoinComponen
|
|||
// Wait until app resumed
|
||||
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
|
||||
if (prefs.showPlayProtectWarning
|
||||
&& !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}")
|
||||
val result = installers.getActiveInstaller().waitInstall(
|
||||
val result = installer.waitInstall(
|
||||
apks = apks,
|
||||
silent = !prefs.devMode,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private fun HomeScreenLoadedPreview(
|
|||
onOpenApp = {},
|
||||
onOpenAppInfo = {},
|
||||
onOpenPlugins = {},
|
||||
onUninstall = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,15 @@ class HomeModel(
|
|||
private val maven: AliucordMavenService,
|
||||
private val wtGithub: ShiggyGithubService,
|
||||
private val rnaTracker: RNATrackerService,
|
||||
private val json: Json,
|
||||
private val json: kotlinx.serialization.json.Json,
|
||||
private val preferences: com.aliucord.manager.manager.PreferencesManager,
|
||||
) : ScreenModel {
|
||||
var installsState by mutableStateOf<InstallsState>(InstallsState.Fetching)
|
||||
private set
|
||||
|
||||
var showRootDetectedDialog by mutableStateOf(false)
|
||||
var rootDetected by mutableStateOf(false)
|
||||
|
||||
private val refreshingLock = Mutex()
|
||||
private var remoteDataJson: BuildInfo? = null
|
||||
private var trackerIndexJson: RNATrackerIndex? = null
|
||||
|
|
@ -52,6 +56,7 @@ class HomeModel(
|
|||
|
||||
init {
|
||||
refresh()
|
||||
checkAndSetRootInstaller()
|
||||
}
|
||||
|
||||
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) {
|
||||
val launchIntent = application.packageManager
|
||||
.getLaunchIntentForPackage(packageName)
|
||||
|
|
@ -223,6 +236,33 @@ class HomeModel(
|
|||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ class HomeScreen : Screen, Parcelable {
|
|||
val scope = rememberCoroutineScope()
|
||||
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
|
||||
LifecycleResumeEffect(Unit) {
|
||||
model.refresh(delay = true)
|
||||
|
|
@ -73,6 +79,7 @@ class HomeScreen : Screen, Parcelable {
|
|||
onOpenApp = model::openApp,
|
||||
onOpenAppInfo = model::openAppInfo,
|
||||
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
|
||||
onUninstall = model::uninstallApp,
|
||||
)
|
||||
|
||||
InstallsState.Fetching -> HomeScreenLoadingContent(padding = padding)
|
||||
|
|
@ -123,6 +130,7 @@ fun HomeScreenLoadedContent(
|
|||
onOpenApp: (packageName: String) -> Unit,
|
||||
onOpenAppInfo: (packageName: String) -> Unit,
|
||||
onOpenPlugins: (packageName: String) -> Unit,
|
||||
onUninstall: (packageName: String) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
|
|
@ -156,6 +164,7 @@ fun HomeScreenLoadedContent(
|
|||
onOpenApp = { onOpenApp(item.packageName) },
|
||||
onOpenInfo = { onOpenAppInfo(item.packageName) },
|
||||
onOpenPlugins = { onOpenPlugins(item.packageName) },
|
||||
onUninstall = { onUninstall(item.packageName) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ fun InstalledItemCard(
|
|||
onOpenApp: () -> Unit,
|
||||
onOpenInfo: () -> Unit,
|
||||
onOpenPlugins: () -> Unit,
|
||||
onUninstall: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
|
|
@ -123,6 +124,13 @@ fun InstalledItemCard(
|
|||
text = stringResource(R.string.action_launch),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.aliucord.manager.ui.screens.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.*
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.InstallerManager
|
||||
import com.aliucord.manager.ui.theme.Theme
|
||||
import com.aliucord.manager.util.*
|
||||
import dev.shiggy.manager.BuildConfig
|
||||
|
|
@ -15,15 +17,19 @@ class SettingsModel(
|
|||
private val application: Application,
|
||||
private val paths: PathManager,
|
||||
val preferences: PreferencesManager,
|
||||
private val installerManager: InstallerManager,
|
||||
) : ScreenModel {
|
||||
val installInfo = InstallInfo
|
||||
|
||||
|
||||
var patchedApkExists by mutableStateOf(paths.patchedApk().exists())
|
||||
private set
|
||||
|
||||
var showThemeDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val availableInstallers = installerManager.getAvailableInstallers()
|
||||
|
||||
fun showThemeDialog() {
|
||||
showThemeDialog = true
|
||||
}
|
||||
|
|
@ -36,6 +42,10 @@ class SettingsModel(
|
|||
preferences.theme = theme
|
||||
}
|
||||
|
||||
fun setInstaller(installer: com.aliucord.manager.manager.InstallerSetting) {
|
||||
preferences.installer = installer
|
||||
}
|
||||
|
||||
fun setKeepPatchedApks(value: Boolean) {
|
||||
preferences.keepPatchedApks = value
|
||||
}
|
||||
|
|
@ -54,35 +64,37 @@ class SettingsModel(
|
|||
application.showToast(R.string.action_copied)
|
||||
}
|
||||
|
||||
fun shareApk() {
|
||||
// TODO(Shiggy): Implement this
|
||||
fun shareApk(uriToShare: android.net.Uri? = null) {
|
||||
val fileUri = uriToShare ?: run {
|
||||
val file = paths.patchedApk()
|
||||
if (!file.exists()) {
|
||||
application.showToast(R.string.settings_export_apk_not_implemented)
|
||||
// val file = paths.patchingWorkingDir().resolve("patched.apk")
|
||||
// val fileUri = FileProvider.getUriForFile(
|
||||
// /* context = */ application,
|
||||
// /* authority = */ "${BuildConfig.APPLICATION_ID}.provider",
|
||||
// /* file = */ file,
|
||||
// /* displayName = */ "Aliucord.apk",
|
||||
// )
|
||||
//
|
||||
// val intent = Intent(Intent.ACTION_SEND)
|
||||
// .setType("application/vnd.android.package-archive")
|
||||
// .putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
// .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
// .let {
|
||||
// Intent.createChooser(
|
||||
// /* target = */ it,
|
||||
// /* title = */ application.getString(R.string.log_action_export_apk),
|
||||
// )
|
||||
// }
|
||||
// .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
//
|
||||
// try {
|
||||
// application.startActivity(intent)
|
||||
// } catch (t: Throwable) {
|
||||
// Log.w(BuildConfig.TAG, "Failed to share APK", t)
|
||||
// application.showToast(R.string.status_failed)
|
||||
// }
|
||||
return
|
||||
}
|
||||
try {
|
||||
androidx.core.content.FileProvider.getUriForFile(
|
||||
application,
|
||||
"${BuildConfig.APPLICATION_ID}.provider",
|
||||
file
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
application.showToast(R.string.status_failed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val intent = android.content.Intent(android.content.Intent.ACTION_SEND)
|
||||
.setType("application/vnd.android.package-archive")
|
||||
.putExtra(android.content.Intent.EXTRA_STREAM, fileUri)
|
||||
.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
try {
|
||||
application.startActivity(intent)
|
||||
} catch (t: Throwable) {
|
||||
android.util.Log.w(BuildConfig.TAG, "Failed to share APK", t)
|
||||
application.showToast(R.string.status_failed)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,22 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
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.MainActionButton
|
||||
import com.aliucord.manager.ui.components.settings.*
|
||||
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 kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import android.content.pm.PackageManager
|
||||
import rikka.shizuku.Shizuku
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
|
||||
@Parcelize
|
||||
class SettingsScreen : Screen, Parcelable {
|
||||
|
|
@ -38,6 +47,7 @@ class SettingsScreen : Screen, Parcelable {
|
|||
val model = koinScreenModel<SettingsModel>()
|
||||
var clearedCache by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
|
@ -70,10 +80,24 @@ class SettingsScreen : Screen, Parcelable {
|
|||
secondaryText = { Text(stringResource(R.string.setting_theme_desc)) }
|
||||
) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Material You theming on Android 12+
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
SettingsSwitch(
|
||||
|
|
@ -87,6 +111,85 @@ class SettingsScreen : Screen, Parcelable {
|
|||
|
||||
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(
|
||||
label = stringResource(R.string.setting_developer_options),
|
||||
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))
|
||||
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.installers.InstallerResult
|
||||
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.KtorDownloadManager
|
||||
import com.aliucord.manager.network.services.AliucordGithubService
|
||||
|
|
@ -91,7 +90,7 @@ class UpdaterViewModel(
|
|||
|
||||
downloadProgress.value = null
|
||||
|
||||
val installer = installers.getInstaller(InstallerSetting.PM)
|
||||
val installer = installers.getActiveInstaller()
|
||||
val installResult = installer.waitInstall(
|
||||
apks = listOf(apkFile),
|
||||
silent = true,
|
||||
|
|
|
|||
12
app/src/main/res/drawable/ic_android.xml
Normal file
12
app/src/main/res/drawable/ic_android.xml
Normal 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>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<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="aliucord" translatable="false">ShiggyCord</string>
|
||||
<string name="aliucord" translatable="false">Aliucord</string>
|
||||
<string name="discord" translatable="false">Discord</string>
|
||||
<string name="github" translatable="false">GitHub</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_dynamic_color">Dynamic color</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_desc">Don\'t enable this unless you know what it does! (yes, really)</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_log">Installation 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="logs_title">Logs</string>
|
||||
|
|
@ -283,6 +286,8 @@
|
|||
<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="permissions_shizuku_denied">Failed to obtain Shizuku permissions</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_2">The refreshed UI inside the settings was made from developer boredom</string>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
[versions]
|
||||
shizuku = "13.1.5"
|
||||
accompanist = "0.37.3"
|
||||
agp = "8.11.0"
|
||||
androidx-activity = "1.10.1"
|
||||
|
|
@ -15,6 +16,9 @@ compose-material3 = "1.3.2"
|
|||
compose-pipette = "1.0.1"
|
||||
desugaring = "2.1.5"
|
||||
diff = "4.16"
|
||||
hiddenApi-bypass = "6.1"
|
||||
hiddenApi-refine = "4.4.0"
|
||||
hiddenApi-stub = "4.4.0"
|
||||
koin = "4.1.0"
|
||||
kotlin = "2.2.0"
|
||||
kotlinx-immutable = "0.4.0"
|
||||
|
|
@ -27,6 +31,13 @@ voyager = "1.1.0-beta03"
|
|||
zip = "2.3.0"
|
||||
|
||||
[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-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version = "0.36.0" }
|
||||
|
|
@ -131,6 +142,10 @@ voyager = [
|
|||
"voyager-navigator",
|
||||
"voyager-transitions",
|
||||
]
|
||||
shizuku = [
|
||||
"shizuku-api",
|
||||
"shizuku-provider",
|
||||
]
|
||||
|
||||
[plugins]
|
||||
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-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", 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" }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pluginManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://maven.rikka.app/releases") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -17,6 +18,10 @@ dependencyResolutionManagement {
|
|||
name = "aliucord"
|
||||
url = uri("https://maven.aliucord.com/snapshots")
|
||||
}
|
||||
maven {
|
||||
name = "jitpack"
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue