Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions manager/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ afterEvaluate {

dependencies {
implementation(projects.patch)
implementation(projects.apkzlib)
implementation("vector:axml")
implementation("vector:daemon-service")
implementation("vector:manager-ui")
implementation(projects.share.android)
Expand Down
2 changes: 1 addition & 1 deletion manager/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@

<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="org.lsposed.lspatch.shizuku"
android:authorities="${applicationId}.shizuku"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
Expand Down
3 changes: 3 additions & 0 deletions manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.lsposed.lspatch.data.repository.PatchRequestStore
import org.lsposed.lspatch.manager.AppBroadcastReceiver
import org.lsposed.lspatch.service.LogCollectorService
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.ManagerMigrate
import org.lsposed.lspatch.util.ShizukuApi
import java.io.File

Expand Down Expand Up @@ -40,6 +41,8 @@ class LSPApplication : Application() {
HiddenApiBypass.addHiddenApiExemptions("")
lspApp = this
filesDir.mkdir()
// Restore settings/db/keystore from a cloaked APK before opening prefs or Room.
ManagerMigrate.importIfNeeded(this)
tmpApkDir = cacheDir.resolve("apk").also { it.mkdir() }
patchedDir = noBackupFilesDir.resolve("patched").also { it.mkdirs() }
prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE)
Expand Down
3 changes: 3 additions & 0 deletions manager/src/main/java/org/lsposed/lspatch/Patcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ object Patcher {
.forceOverwrite(true)
.verbose(Configs.detailPatchLogs)
.modules(effectiveModules.map { File(it.apkPath) })
// Record the manager's own current package so a manager-mode app keeps reaching it after
// the manager is reinstalled under a cloaked package name; integrated apps bind nothing.
.managerPackageName(if (mode.useManager) lspApp.packageName else null)
.manifestOverrides(
ManifestOverrides.builder()
.versionCode(versionCodeOverride)
Expand Down
135 changes: 135 additions & 0 deletions manager/src/main/java/org/lsposed/lspatch/ui/page/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Layers
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Badge
import androidx.compose.material.icons.rounded.Terminal
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
Expand Down Expand Up @@ -106,7 +110,10 @@ import org.matrix.vector.ui.RepoStatsRow
import org.matrix.vector.ui.theme.Mono
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.viewmodel.HomeViewModel
import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.ManagerCloakFlow
import org.lsposed.lspatch.util.PackageNameValidator
import org.lsposed.lspatch.util.ShizukuApi
import org.lsposed.lspatch.util.findActivity
import org.matrix.vector.ui.StatusHeader
Expand Down Expand Up @@ -414,6 +421,16 @@ private fun SystemPropertiesCard(navigator: DestinationsNavigator) {
apps.filter { it.isModule }
.mapNotNull { runCatching { LSPPackageManager.getIcon(it) }.getOrNull() }

// The manager's own installed package. Surfaced here, next to the environment facts, because it
// is the one identity a detector enumerates the device for -- and because that is the guiding
// place to offer changing it. Tapping opens the cloak flow (or the revert, once cloaked).
var showPackageDialog by remember { mutableStateOf(false) }
val packageProp = SystemProperty(
Icons.Rounded.Badge,
stringResource(R.string.home_package),
lspApp.packageName,
) { showPackageDialog = true }

val shizukuProp = SystemProperty(Icons.Rounded.Terminal, "Shizuku", shizukuValue, openShizuku)
val androidProp = SystemProperty(Icons.Rounded.Android, "Android", androidAndAbi)
val deviceProp = SystemProperty(Icons.Rounded.Smartphone, stringResource(R.string.home_device), deviceName)
Expand Down Expand Up @@ -499,12 +516,130 @@ private fun SystemPropertiesCard(navigator: DestinationsNavigator) {
Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
)
PropertyRow(packageProp)
PropertyRow(shizukuProp)
PropertyRow(vectorProp)
PropertyRow(androidProp)
PropertyRow(deviceProp)
}
}

if (showPackageDialog) {
ManagerPackageDialog(onDismiss = { showPackageDialog = false })
}
}

/**
* Configures the manager's own package name.
*
* On the stock package it offers to cloak: reinstall under a custom or random id so a detector
* enumerating for `org.lsposed.lspatch` finds nothing. Once cloaked it offers the reverse. Both are
* driven by [ManagerCloakFlow], which reports progress as it installs, retargets manager-mode apps,
* and removes the old package -- so the dialog stays open, showing that progress, until it finishes.
*
* This only defeats known-package-name checks; the signing certificate, app label and exported
* components are unchanged, and it does nothing for Play Integrity. Manager mode only -- integrated
* patches bind no manager and are unaffected.
*/
@Composable
private fun ManagerPackageDialog(onDismiss: () -> Unit) {
val scope = rememberCoroutineScope()
val isCloaked = lspApp.packageName != Constants.MANAGER_PACKAGE_NAME
val granted = ShizukuApi.isPermissionGranted

var running by remember { mutableStateOf(false) }
var status by remember { mutableStateOf<String?>(null) }
var newName by remember { mutableStateOf(PackageNameValidator.randomPackageName()) }
var invalid by remember { mutableStateOf(false) }

AlertDialog(
onDismissRequest = { if (!running) onDismiss() },
title = {
Text(stringResource(
if (isCloaked) R.string.settings_revert_dialog_title
else R.string.settings_cloak_dialog_title
))
},
text = {
Column {
Text(stringResource(
if (isCloaked) R.string.settings_revert_warning
else R.string.settings_cloak_warning
))
if (!isCloaked) {
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = newName,
onValueChange = { newName = it; invalid = false },
singleLine = true,
isError = invalid,
enabled = !running,
label = { Text(stringResource(R.string.settings_cloak_package)) },
trailingIcon = {
TextButton(
enabled = !running,
onClick = { newName = PackageNameValidator.randomPackageName(); invalid = false },
) { Text(stringResource(R.string.settings_cloak_randomize)) }
},
)
if (invalid) Text(
stringResource(R.string.settings_cloak_invalid),
color = MaterialTheme.colorScheme.error,
)
}
if (!granted) Text(
stringResource(R.string.settings_cloak_need_shizuku),
color = MaterialTheme.colorScheme.error,
)
status?.let {
Spacer(Modifier.height(12.dp))
Text(it)
}
}
},
confirmButton = {
TextButton(
enabled = !running && granted,
onClick = {
if (!isCloaked && !PackageNameValidator.isValid(newName)) {
invalid = true
return@TextButton
}
running = true
status = null
val onProgress: (ManagerCloakFlow.Progress) -> Unit = { p ->
when (p) {
is ManagerCloakFlow.Progress.Message -> status = p.text
is ManagerCloakFlow.Progress.Error -> {
status = p.message
running = false
}
// On success the flow launches the new package and this process is about
// to be uninstalled; close the dialog so nothing lingers behind it.
is ManagerCloakFlow.Progress.Success -> {
running = false
onDismiss()
}
}
}
scope.launch {
if (isCloaked) ManagerCloakFlow.revertToOriginal(onProgress)
else ManagerCloakFlow.run(newName, onProgress)
}
},
) {
Text(stringResource(
if (isCloaked) R.string.settings_revert_confirm
else R.string.settings_cloak_confirm
))
}
},
dismissButton = {
TextButton(enabled = !running, onClick = onDismiss) {
Text(stringResource(android.R.string.cancel))
}
},
)
}

private data class SystemProperty(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.lsposed.lspatch.util

import android.util.Log
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL

object GithubReleaseDownloader {

private const val TAG = "GithubReleaseDownloader"
private const val RELEASES_API =
"https://api.github.com/repos/JingMatrix/LSPatch/releases/latest"
private const val USER_AGENT = "LSPatch-Manager"

data class Result(val tagName: String, val assetName: String, val file: File)

/**
* Downloads the latest manager APK from GitHub Releases.
* Prefers `manager.apk`, falls back to `manager-debug.apk`.
*/
fun downloadLatestManager(dest: File): Result {
dest.parentFile?.mkdirs()
if (dest.exists()) dest.delete()

val releaseJson = httpGetString(RELEASES_API)
val root = JSONObject(releaseJson)
val tag = root.optString("tag_name", "unknown")
val assets = root.getJSONArray("assets")

var preferredUrl: String? = null
var preferredName: String? = null
var fallbackUrl: String? = null
var fallbackName: String? = null

for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
val name = asset.getString("name")
val url = asset.getString("browser_download_url")
when {
name.equals("manager.apk", ignoreCase = true) -> {
preferredUrl = url
preferredName = name
}
name.equals("manager-debug.apk", ignoreCase = true) -> {
fallbackUrl = url
fallbackName = name
}
}
}

val downloadUrl = preferredUrl ?: fallbackUrl
?: throw IllegalStateException("No manager APK found in latest GitHub release")
val assetName = preferredName ?: fallbackName!!

Log.i(TAG, "Downloading $assetName ($tag) from $downloadUrl")
httpDownload(downloadUrl, dest)
if (!dest.isFile || dest.length() == 0L) {
throw IllegalStateException("Downloaded APK is empty")
}
return Result(tag, assetName, dest)
}

private fun httpGetString(url: String): String {
val conn = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
setRequestProperty("Accept", "application/vnd.github+json")
setRequestProperty("User-Agent", USER_AGENT)
connectTimeout = 30_000
readTimeout = 60_000
instanceFollowRedirects = true
}
try {
val code = conn.responseCode
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
val body = stream?.bufferedReader()?.use { it.readText() }.orEmpty()
if (code !in 200..299) {
throw IllegalStateException("GitHub API HTTP $code: $body")
}
return body
} finally {
conn.disconnect()
}
}

private fun httpDownload(url: String, dest: File) {
var current = url
// Follow a few redirects manually for CDN links if needed
repeat(5) {
val conn = (URL(current).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
setRequestProperty("User-Agent", USER_AGENT)
setRequestProperty("Accept", "application/octet-stream")
connectTimeout = 30_000
readTimeout = 300_000
instanceFollowRedirects = false
}
try {
val code = conn.responseCode
when (code) {
in 200..299 -> {
conn.inputStream.use { input ->
FileOutputStream(dest).use { output -> input.copyTo(output) }
}
return
}
HttpURLConnection.HTTP_MOVED_PERM,
HttpURLConnection.HTTP_MOVED_TEMP,
HttpURLConnection.HTTP_SEE_OTHER,
307, 308 -> {
current = conn.getHeaderField("Location")
?: throw IllegalStateException("Redirect without Location")
}
else -> {
val err = conn.errorStream?.bufferedReader()?.use { it.readText() }
throw IllegalStateException("Download HTTP $code: $err")
}
}
} finally {
conn.disconnect()
}
}
throw IllegalStateException("Too many redirects while downloading release APK")
}
}
Loading
Loading