Skip to content

Commit 22287cd

Browse files
committed
feat(Shizuku): Bootstrap implementation
1 parent 219fc9c commit 22287cd

15 files changed

Lines changed: 816 additions & 116 deletions

File tree

app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import app.revanced.manager.R
88
enum class InstallType(val stringResource: Int) {
99
DEFAULT(R.string.default_install),
1010
MOUNT(R.string.mount_install),
11-
SHIZUKU(R.string.shizuku_install)
11+
SHIZUKU(R.string.shizuku_install),
12+
ADB(R.string.adb_install)
1213
}
1314

1415
@Entity(tableName = "installed_app")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
11
package app.revanced.manager.di
22

3+
import app.revanced.library.installation.installer.ShizukuAdbInstaller
4+
import app.revanced.manager.R
35
import app.revanced.manager.domain.installer.RootInstaller
46
import app.revanced.manager.domain.installer.ShizukuInstaller
7+
import app.revanced.shizukulibrary.adb.AdbConnectionManager
8+
import org.koin.android.ext.koin.androidContext
59
import org.koin.core.module.dsl.singleOf
610
import org.koin.dsl.module
711

812
val rootModule = module {
913
singleOf(::RootInstaller)
1014
singleOf(::ShizukuInstaller)
15+
single { AdbConnectionManager.getInstance(androidContext()) }
16+
single {
17+
val app = androidContext()
18+
ShizukuAdbInstaller(
19+
context = app,
20+
adbConnectionManager = get(),
21+
mapErrorMessage = { output ->
22+
when {
23+
output.contains("INSTALL_FAILED_UPDATE_INCOMPATIBLE") ||
24+
output.contains("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES") ||
25+
output.contains("INSTALL_FAILED_VERSION_DOWNGRADE") ->
26+
app.getString(R.string.installation_conflict_description)
27+
28+
output.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") ->
29+
app.getString(R.string.installation_storage_issue_description)
30+
31+
output.contains("INSTALL_FAILED_INVALID_APK") ||
32+
output.contains("INSTALL_PARSE_FAILED_NOT_APK") ||
33+
output.contains("INSTALL_FAILED_INVALID_URI") ->
34+
app.getString(R.string.installation_invalid_description)
35+
36+
output.contains("INSTALL_FAILED_INCOMPATIBLE_ABI") ||
37+
output.contains("INSTALL_FAILED_OLDER_SDK") ->
38+
app.getString(R.string.installation_incompatible_description)
39+
40+
output.contains("INSTALL_FAILED_ABORTED") ->
41+
app.getString(R.string.installation_aborted_description)
42+
43+
output.contains("INSTALL_FAILED_VERIFICATION_TIMEOUT") ->
44+
app.getString(R.string.installation_timeout_description)
45+
46+
output.contains("INSTALL_FAILED_VERIFICATION_FAILURE") ||
47+
output.contains("INSTALL_FAILED_REJECTED_BY_BUILDER") ->
48+
app.getString(R.string.installation_blocked_description)
49+
50+
output.contains("INSTALL_FAILED_USER_RESTRICTED") ->
51+
app.getString(R.string.installation_restricted_description)
52+
53+
else -> app.getString(R.string.installation_failed_description) + " ($output)"
54+
}
55+
},
56+
mapUninstallErrorMessage = { output ->
57+
app.getString(R.string.uninstall_app_fail, output)
58+
}
59+
)
60+
}
1161
}

app/src/main/java/app/revanced/manager/domain/installer/ShizukuInstaller.kt

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import android.annotation.SuppressLint
44
import android.content.ComponentName
55
import android.content.Context
66
import android.content.ServiceConnection
7+
import android.content.pm.PackageInstaller
78
import android.content.pm.PackageManager
89
import android.os.Binder
910
import android.os.IBinder
1011
import android.os.Parcel
1112
import android.os.ParcelFileDescriptor
12-
import android.util.Log
1313
import app.revanced.manager.R
1414
import kotlinx.coroutines.Dispatchers
1515
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -26,6 +26,37 @@ class ShizukuInstaller(
2626
) {
2727
fun isAvailable(): Boolean = Shizuku.pingBinder()
2828

29+
fun mapStatus(output: String): Int = when {
30+
output.contains("INSTALL_FAILED_UPDATE_INCOMPATIBLE") ||
31+
output.contains("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES") ||
32+
output.contains("INSTALL_FAILED_VERSION_DOWNGRADE") ->
33+
PackageInstaller.STATUS_FAILURE_CONFLICT
34+
35+
output.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") ->
36+
PackageInstaller.STATUS_FAILURE_STORAGE
37+
38+
output.contains("INSTALL_FAILED_INVALID_APK") ||
39+
output.contains("INSTALL_PARSE_FAILED_NOT_APK") ||
40+
output.contains("INSTALL_FAILED_INVALID_URI") ->
41+
PackageInstaller.STATUS_FAILURE_INVALID
42+
43+
output.contains("INSTALL_FAILED_INCOMPATIBLE_ABI") ||
44+
output.contains("INSTALL_FAILED_OLDER_SDK") ->
45+
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
46+
47+
output.contains("INSTALL_FAILED_ABORTED") ->
48+
PackageInstaller.STATUS_FAILURE_ABORTED
49+
50+
output.contains("INSTALL_FAILED_VERIFICATION_TIMEOUT") ->
51+
PackageInstaller.STATUS_FAILURE_TIMEOUT
52+
53+
output.contains("INSTALL_FAILED_VERIFICATION_FAILURE") ||
54+
output.contains("INSTALL_FAILED_REJECTED_BY_BUILDER") ->
55+
PackageInstaller.STATUS_FAILURE_BLOCKED
56+
57+
else -> PackageInstaller.STATUS_FAILURE
58+
}
59+
2960
fun hasPermission(): Boolean {
3061
if (!isAvailable()) return false
3162
return if (Shizuku.getVersion() >= 11) {
@@ -38,7 +69,8 @@ class ShizukuInstaller(
3869
private fun mapErrorMessage(output: String, exitCode: Int): String {
3970
return when {
4071
output.contains("INSTALL_FAILED_UPDATE_INCOMPATIBLE") ||
41-
output.contains("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES") ->
72+
output.contains("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES") ||
73+
output.contains("INSTALL_FAILED_VERSION_DOWNGRADE") ->
4274
context.getString(R.string.installation_conflict_description)
4375

4476
output.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") ->
@@ -166,10 +198,49 @@ class ShizukuInstaller(
166198
}
167199
}
168200

201+
suspend fun bootstrapAdb(publicKey: String) = withContext(Dispatchers.IO) {
202+
val serviceArgs = Shizuku.UserServiceArgs(ComponentName(context.packageName, UserService::class.java.name))
203+
.daemon(false)
204+
.processNameSuffix("shizuku_adb_bootstrap")
205+
206+
suspendCancellableCoroutine { continuation ->
207+
val connection = object : ServiceConnection {
208+
private val resumed = AtomicBoolean(false)
209+
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
210+
if (service == null || resumed.get() || !continuation.isActive) return
211+
thread {
212+
val data = Parcel.obtain()
213+
val reply = Parcel.obtain()
214+
try {
215+
data.writeInt(UserService.TRANSACTION_BOOTSTRAP_ADB)
216+
data.writeString(context.packageName)
217+
data.writeString(publicKey)
218+
service.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0)
219+
val result = reply.readInt()
220+
if (resumed.compareAndSet(false, true)) {
221+
if (result == 0) continuation.resume(Unit)
222+
else continuation.resumeWithException(Exception("Bootstrap failed ($result)"))
223+
}
224+
} catch (e: Exception) {
225+
if (resumed.compareAndSet(false, true)) continuation.resumeWithException(e)
226+
} finally {
227+
data.recycle()
228+
reply.recycle()
229+
Shizuku.unbindUserService(serviceArgs, this, true)
230+
}
231+
}
232+
}
233+
override fun onServiceDisconnected(name: ComponentName?) {}
234+
}
235+
Shizuku.bindUserService(serviceArgs, connection)
236+
}
237+
}
238+
169239
class UserService : Binder() {
170240
companion object {
171241
const val TRANSACTION_INSTALL = 1
172242
const val TRANSACTION_UNINSTALL = 2
243+
const val TRANSACTION_BOOTSTRAP_ADB = 3
173244
}
174245

175246
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
@@ -178,6 +249,7 @@ class ShizukuInstaller(
178249
return when (actionCode) {
179250
TRANSACTION_INSTALL -> handleInstall(data, reply)
180251
TRANSACTION_UNINSTALL -> handleUninstall(data, reply)
252+
TRANSACTION_BOOTSTRAP_ADB -> handleBootstrapAdb(data, reply)
181253
else -> false
182254
}
183255
}
@@ -220,5 +292,27 @@ class ShizukuInstaller(
220292
}
221293
return true
222294
}
295+
296+
private fun handleBootstrapAdb(data: Parcel, reply: Parcel?): Boolean {
297+
val packageName = data.readString() ?: return false
298+
val publicKey = data.readString() ?: return false
299+
try {
300+
// Grant WRITE_SECURE_SETTINGS to the app
301+
ProcessBuilder("pm", "grant", packageName, "android.permission.WRITE_SECURE_SETTINGS")
302+
.redirectErrorStream(true).start().waitFor()
303+
// Append the RSA public key to ADB trusted keys
304+
ProcessBuilder("sh", "-c", "echo \"$publicKey\" >> /data/misc/adb/adb_keys")
305+
.redirectErrorStream(true).start().waitFor()
306+
// Enable ADB TCP on port 5555
307+
ProcessBuilder("setprop", "service.adb.tcp.port", "5555")
308+
.redirectErrorStream(true).start().waitFor()
309+
ProcessBuilder("stop", "adbd").redirectErrorStream(true).start().waitFor()
310+
ProcessBuilder("start", "adbd").redirectErrorStream(true).start().waitFor()
311+
reply?.writeInt(0)
312+
} catch (e: Exception) {
313+
reply?.writeInt(-1)
314+
}
315+
return true
316+
}
223317
}
224318
}

app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ class PreferencesManager(
3939
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
4040

4141
val pinnedApps = stringSetPreference("pinned_apps", emptySet())
42+
43+
val adbPort = intPreference("adb_port", 5555)
44+
val adbPairingPort = stringPreference("adb_pairing_port", "")
45+
val adbPairingCode = stringPreference("adb_pairing_code", "")
46+
val shizukuAutoSetup = booleanPreference("shizuku_auto_setup", true)
4247
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package app.revanced.manager.ui.component.patcher
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.text.KeyboardOptions
9+
import androidx.compose.material3.Button
10+
import androidx.compose.material3.ButtonDefaults
11+
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.material3.OutlinedTextField
15+
import androidx.compose.material3.Scaffold
16+
import androidx.compose.material3.Text
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.LaunchedEffect
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.res.stringResource
21+
import androidx.compose.ui.text.input.KeyboardType
22+
import androidx.compose.ui.unit.dp
23+
import app.revanced.manager.R
24+
import app.revanced.manager.ui.component.AppTopBar
25+
import app.revanced.manager.ui.component.ColumnWithScrollbar
26+
import app.revanced.manager.ui.component.FullscreenDialog
27+
import app.revanced.manager.ui.component.settings.ExpandableSettingsListItem
28+
29+
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
30+
@Composable
31+
fun AdbSetupDialog(
32+
isShizukuAuthorized: Boolean,
33+
isAdbConnected: Boolean,
34+
isPairing: Boolean,
35+
adbPort: String,
36+
adbPairingPort: String,
37+
adbPairingCode: String,
38+
onPortChange: (String) -> Unit,
39+
onPairingPortChange: (String) -> Unit,
40+
onPairingCodeChange: (String) -> Unit,
41+
onBootstrapAdb: () -> Unit,
42+
onConnectAdb: () -> Unit,
43+
onPairAdb: () -> Unit,
44+
onDismiss: () -> Unit
45+
) {
46+
LaunchedEffect(isAdbConnected) {
47+
if (isAdbConnected) onDismiss()
48+
}
49+
50+
FullscreenDialog(onDismissRequest = onDismiss) {
51+
Scaffold(
52+
topBar = {
53+
AppTopBar(
54+
title = stringResource(R.string.adb_setup),
55+
onBackClick = onDismiss
56+
)
57+
}
58+
) { paddingValues ->
59+
ColumnWithScrollbar(
60+
modifier = Modifier
61+
.fillMaxSize()
62+
.padding(paddingValues)
63+
.padding(16.dp),
64+
verticalArrangement = Arrangement.spacedBy(16.dp)
65+
) {
66+
Text(
67+
text = stringResource(R.string.adb_pairing_description),
68+
style = MaterialTheme.typography.bodyMedium
69+
)
70+
71+
if (isShizukuAuthorized) {
72+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
73+
Text(
74+
text = stringResource(R.string.adb_setup_auto_title),
75+
style = MaterialTheme.typography.titleMedium
76+
)
77+
Text(
78+
text = stringResource(R.string.adb_setup_auto_description),
79+
style = MaterialTheme.typography.bodySmall
80+
)
81+
Button(
82+
onClick = onBootstrapAdb,
83+
modifier = Modifier.fillMaxWidth(),
84+
shapes = ButtonDefaults.shapes()
85+
) {
86+
Text(stringResource(R.string.adb_bootstrap_shizuku))
87+
}
88+
}
89+
}
90+
91+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
92+
Text(
93+
text = stringResource(R.string.adb_setup_manual_title),
94+
style = MaterialTheme.typography.titleMedium
95+
)
96+
97+
ExpandableSettingsListItem(
98+
headlineContent = stringResource(R.string.adb_pair),
99+
supportingContent = stringResource(R.string.adb_pairing_description_code),
100+
expandableContent = {
101+
Column(
102+
modifier = Modifier.padding(horizontal = 8.dp),
103+
verticalArrangement = Arrangement.spacedBy(8.dp)
104+
) {
105+
OutlinedTextField(
106+
value = adbPairingPort,
107+
onValueChange = onPairingPortChange,
108+
label = { Text(stringResource(R.string.adb_port)) },
109+
modifier = Modifier.fillMaxWidth(),
110+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
111+
)
112+
OutlinedTextField(
113+
value = adbPairingCode,
114+
onValueChange = onPairingCodeChange,
115+
label = { Text(stringResource(R.string.adb_pairing_code)) },
116+
modifier = Modifier.fillMaxWidth(),
117+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
118+
)
119+
Button(
120+
onClick = onPairAdb,
121+
modifier = Modifier.fillMaxWidth(),
122+
enabled = adbPairingPort.isNotEmpty() && adbPairingCode.isNotEmpty() && !isPairing,
123+
shapes = ButtonDefaults.shapes()
124+
) {
125+
Text(if (isPairing) stringResource(R.string.adb_pairing_in_progress) else stringResource(R.string.adb_pair))
126+
}
127+
}
128+
}
129+
)
130+
131+
ExpandableSettingsListItem(
132+
headlineContent = stringResource(R.string.connect),
133+
supportingContent = stringResource(R.string.adb_connect_description),
134+
expandableContent = {
135+
Column(
136+
modifier = Modifier.padding(horizontal = 8.dp),
137+
verticalArrangement = Arrangement.spacedBy(8.dp)
138+
) {
139+
OutlinedTextField(
140+
value = adbPort,
141+
onValueChange = onPortChange,
142+
label = { Text(stringResource(R.string.adb_port)) },
143+
modifier = Modifier.fillMaxWidth(),
144+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
145+
)
146+
Button(
147+
onClick = onConnectAdb,
148+
modifier = Modifier.fillMaxWidth(),
149+
enabled = adbPort.isNotEmpty(),
150+
shapes = ButtonDefaults.shapes()
151+
) {
152+
Text(stringResource(R.string.connect))
153+
}
154+
}
155+
}
156+
)
157+
}
158+
}
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)