Skip to content
This repository has been archived by the owner on Jul 7, 2024. It is now read-only.

Commit

Permalink
feat: shizuku installer (#55)
Browse files Browse the repository at this point in the history
* feat: add installer selection UI

* feat: add shizuku dependencies

* refactor: installer directory structure

* refactor: installer structure

* feat: shizuku binding and permission handling

* feat: shizuku user service

* refactor: move service binding to installer

* chore: cleanup

* chore: more cleanup

* feat: downgrade shizuku and finish installer

* chore: cleanup

* fix: correct session handling for split package

* chore(i18n): sync translations

* chore(i18n): sync translations

* Improve code style and use more reliable install command

---------

Co-authored-by: Wing <[email protected]>
  • Loading branch information
Adrian Castro and wingio authored Jan 8, 2024
1 parent c5ce09d commit c60414b
Show file tree
Hide file tree
Showing 17 changed files with 223 additions and 65 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")

val shizukuVersion = "13.1.0"

implementation("dev.rikka.shizuku:api:$shizukuVersion")
implementation("dev.rikka.shizuku:provider:$shizukuVersion")

implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")
implementation("io.github.diamondminer88:zip-android:2.1.0@aar")
implementation(files("libs/lspatch.jar"))
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</intent-filter>
</activity>

<service android:name=".installer.service.InstallService" />
<service android:name=".installer.session.InstallService" />

<receiver android:name=".domain.receiver.InstallReceiver"
android:exported="true">
Expand All @@ -66,6 +66,15 @@
</receiver>

<receiver android:name=".updatechecker.reciever.UpdateBroadcastReceiver" android:exported="false" />

<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import android.os.Build
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.beefers.vendetta.manager.installer.service.InstallService
import dev.beefers.vendetta.manager.installer.session.InstallService

class InstallManager(
private val context: Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class PreferenceManager(context: Context) :

var moduleLocation by filePreference("module_location", DEFAULT_MODULE_LOCATION)

var installMethod by enumPreference("install_method", InstallMethod.DEFAULT)

init {
// Will be removed next update
if(mirror == Mirror.VENDETTA_ROCKS) mirror = Mirror.VENDETTA_ROCKS_ALT
Expand Down Expand Up @@ -72,4 +74,9 @@ enum class Mirror(val baseUrl: String) {
VENDETTA_ROCKS_ALT("https://proxy.vendetta.rocks"),
K6("https://vd.k6.tf"),
NEXPID("https://vd.k6.tf.nexpid.xyz")
}

enum class InstallMethod(@StringRes val labelRes: Int) {
DEFAULT(R.string.default_installer),
SHIZUKU(R.string.shizuku_installer)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.beefers.vendetta.manager.installer

import java.io.File

interface Installer {
suspend fun installApks(silent: Boolean = false, vararg apks: File)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.beefers.vendetta.manager.installer.session

import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager
import android.os.Build
import dev.beefers.vendetta.manager.installer.Installer
import java.io.File

internal class SessionInstaller(private val context: Context) : Installer {

private val packageManager: PackageManager = context.packageManager

override suspend fun installApks(silent: Boolean, vararg apks: File) {
val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
if (Build.VERSION.SDK_INT >= 31) {
setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST)

if (silent) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
}
}
}

val packageInstaller = packageManager.packageInstaller
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)

apks.forEach { apk ->
session.openWrite(apk.name, 0, apk.length()).use {
it.write(apk.readBytes())
session.fsync(it)
}
}

val callbackIntent = Intent(context, InstallService::class.java).apply {
action = "vendetta.actions.ACTION_INSTALL"
}

@SuppressLint("UnspecifiedImmutableFlag")
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getService(context, 0, callbackIntent, 0)
}

session.commit(contentIntent.intentSender)
session.close()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.beefers.vendetta.manager.installer.service
package dev.beefers.vendetta.manager.installer.session

import android.app.Service
import android.content.Intent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.beefers.vendetta.manager.installer.shizuku

import android.content.Context
import dev.beefers.vendetta.manager.installer.Installer
import rikka.shizuku.Shizuku
import java.io.File
import java.util.UUID

class ShizukuInstaller(private val context: Context) : Installer {

companion object {
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
}

override suspend fun installApks(silent: Boolean, vararg apks: File) {
val tempDir = File("/data/local/tmp")
val movedApks = mutableListOf<File>()

// Copy each split to tmp
apks.forEach {
val moveCommand = "cp ${it.absolutePath} ${tempDir.absolutePath}"
val moveResult = executeShellCommand(moveCommand)

if(moveResult.isBlank())
movedApks.add(File(tempDir.absolutePath, it.name))
else
throw RuntimeException("Failed to move ${it.absolutePath} to temp dir")
}

val installCommand = "pm install ${movedApks.joinToString(" ") { it.absolutePath }}"
executeShellCommand(installCommand)

movedApks.forEach {
it.delete()
}
}

private fun executeShellCommand(command: String): String {
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)

Check warning on line 39 in app/src/main/java/dev/beefers/vendetta/manager/installer/shizuku/ShizukuInstaller.kt

View workflow job for this annotation

GitHub Actions / build

'newProcess(Array<(out) String!>, Array<(out) String!>?, String?): ShizukuRemoteProcess!' is deprecated. Deprecated in Java

val errorStr = process.errorStream.bufferedReader().use { it.readText().trim() }
if(errorStr.isNotBlank()) throw RuntimeException("Failed to execute $command:\n\n$errorStr")

return process.inputStream.bufferedReader().use { it.readText().trim() }
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,29 @@ import dev.beefers.vendetta.manager.ui.screen.main.MainScreen
import dev.beefers.vendetta.manager.ui.theme.VendettaManagerTheme
import dev.beefers.vendetta.manager.utils.DiscordVersion
import dev.beefers.vendetta.manager.utils.Intents
import rikka.shizuku.Shizuku


class MainActivity : ComponentActivity(), Shizuku.OnRequestPermissionResultListener {

private val acRequestCode = 1
private val REQUEST_PERMISSION_RESULT_LISTENER = this::onRequestPermissionResult

override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
checkAndRequestPermission()
}
}

private fun checkAndRequestPermission() {
if (Shizuku.pingBinder()) {
Shizuku.addRequestPermissionResultListener(REQUEST_PERMISSION_RESULT_LISTENER)
if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) {
Shizuku.requestPermission(acRequestCode)
}
}
}

class MainActivity : ComponentActivity() {
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
Expand All @@ -31,10 +52,13 @@ class MainActivity : ComponentActivity() {
)
}

val screen = if (intent.action == Intents.Actions.INSTALL && version != null)
checkAndRequestPermission()

val screen = if (intent.action == Intents.Actions.INSTALL && version != null) {
InstallerScreen(DiscordVersion.fromVersionCode(version)!!)
else
} else {
MainScreen()
}

setContent {
VendettaManagerTheme {
Expand All @@ -44,4 +68,10 @@ class MainActivity : ComponentActivity() {
}
}
}
}

override fun onDestroy() {
Shizuku.removeRequestPermissionResultListener(REQUEST_PERMISSION_RESULT_LISTENER)
super.onDestroy()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ inline fun <reified E : Enum<E>> SettingsItemChoice(
}

SettingsItem(
modifier = Modifier.clickable { opened.value = true },
modifier = Modifier.clickable(enabled = !disabled) { opened.value = true },
text = { Text(text = label) },
) {
SettingsChoiceDialog(
Expand All @@ -47,4 +47,4 @@ inline fun <reified E : Enum<E>> SettingsItemChoice(
Text(choiceLabel)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.beefers.vendetta.manager.ui.screen.settings

import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
Expand All @@ -11,6 +12,7 @@ import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -39,6 +41,7 @@ import dev.beefers.vendetta.manager.utils.ManagerTab
import dev.beefers.vendetta.manager.utils.TabOptions
import dev.beefers.vendetta.manager.utils.navigate
import org.koin.androidx.compose.get
import rikka.shizuku.Shizuku
import java.io.File

class SettingsScreen : ManagerTab {
Expand All @@ -55,6 +58,17 @@ class SettingsScreen : ManagerTab {
val prefs: PreferenceManager = get()
val installManager: InstallManager = get()
val ctx = LocalContext.current
var shizukuAvailable by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
val shizukuAlive = Shizuku.pingBinder()
val shizukuPermissionsGranted = if (shizukuAlive) {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
} else {
false
}
shizukuAvailable = shizukuAlive && shizukuPermissionsGranted
}

Column(
modifier = Modifier.verticalScroll(rememberScrollState())
Expand Down Expand Up @@ -128,6 +142,17 @@ class SettingsScreen : ManagerTab {
prefs.mirror = it
}
)
SettingsItemChoice(
label = stringResource(R.string.install_method),
pref = prefs.installMethod,
labelFactory = {
ctx.getString(it.labelRes)
},
disabled = !shizukuAvailable,
onPrefChange = {
prefs.installMethod = it
}
)
SettingsSwitch(
label = stringResource(R.string.settings_auto_clear_cache),
secondaryLabel = stringResource(R.string.settings_auto_clear_cache_description),
Expand Down
Loading

0 comments on commit c60414b

Please sign in to comment.