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:
|
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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AppCompat.NoActionBar">
|
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||||
|
>
|
||||||
|
|
||||||
<receiver
|
<receiver android:name="com.aliucord.manager.installers.pm.PMIntentReceiver" android:exported="false" />
|
||||||
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" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ private fun HomeScreenLoadedPreview(
|
||||||
onOpenApp = {},
|
onOpenApp = {},
|
||||||
onOpenAppInfo = {},
|
onOpenAppInfo = {},
|
||||||
onOpenPlugins = {},
|
onOpenPlugins = {},
|
||||||
|
onUninstall = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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.
|
* Obtains all installed packages on the device that are an Aliucord installation.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
val file = paths.patchedApk()
|
||||||
|
if (!file.exists()) {
|
||||||
application.showToast(R.string.settings_export_apk_not_implemented)
|
application.showToast(R.string.settings_export_apk_not_implemented)
|
||||||
// val file = paths.patchingWorkingDir().resolve("patched.apk")
|
return
|
||||||
// val fileUri = FileProvider.getUriForFile(
|
}
|
||||||
// /* context = */ application,
|
try {
|
||||||
// /* authority = */ "${BuildConfig.APPLICATION_ID}.provider",
|
androidx.core.content.FileProvider.getUriForFile(
|
||||||
// /* file = */ file,
|
application,
|
||||||
// /* displayName = */ "Aliucord.apk",
|
"${BuildConfig.APPLICATION_ID}.provider",
|
||||||
// )
|
file
|
||||||
//
|
)
|
||||||
// val intent = Intent(Intent.ACTION_SEND)
|
} catch (t: Throwable) {
|
||||||
// .setType("application/vnd.android.package-archive")
|
application.showToast(R.string.status_failed)
|
||||||
// .putExtra(Intent.EXTRA_STREAM, fileUri)
|
return
|
||||||
// .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
}
|
||||||
// .let {
|
}
|
||||||
// Intent.createChooser(
|
|
||||||
// /* target = */ it,
|
val intent = android.content.Intent(android.content.Intent.ACTION_SEND)
|
||||||
// /* title = */ application.getString(R.string.log_action_export_apk),
|
.setType("application/vnd.android.package-archive")
|
||||||
// )
|
.putExtra(android.content.Intent.EXTRA_STREAM, fileUri)
|
||||||
// }
|
.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
// .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
//
|
|
||||||
// try {
|
try {
|
||||||
// application.startActivity(intent)
|
application.startActivity(intent)
|
||||||
// } catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
// Log.w(BuildConfig.TAG, "Failed to share APK", t)
|
android.util.Log.w(BuildConfig.TAG, "Failed to share APK", t)
|
||||||
// application.showToast(R.string.status_failed)
|
application.showToast(R.string.status_failed)
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 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,
|
||||||
|
|
|
||||||
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_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>
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue