Skip to content
Open
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 gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
android.experimental.enableNewResourceShrinker.preciseShrinking=true
android.enableAppCompileTimeRClass=true
android.useAndroidX=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
2 changes: 2 additions & 0 deletions manager/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ afterEvaluate {

dependencies {
implementation(projects.patch)
implementation(projects.apkzlib)
implementation("vector:axml")
implementation("vector:daemon-service")
implementation(projects.share.android)
implementation(projects.share.java)
Expand Down
3 changes: 2 additions & 1 deletion manager/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".LSPApplication"
Expand Down Expand Up @@ -39,7 +40,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 @@ -9,6 +9,7 @@ import kotlinx.coroutines.launch
import org.lsposed.hiddenapibypass.HiddenApiBypass
import org.lsposed.lspatch.manager.AppBroadcastReceiver
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.ManagerMigrate
import org.lsposed.lspatch.util.ShizukuApi
import java.io.File

Expand All @@ -26,6 +27,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() }
prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE)
ShizukuApi.init(this)
Expand Down
5 changes: 4 additions & 1 deletion manager/src/main/java/org/lsposed/lspatch/Patcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ object Patcher {
add("-o"); add(lspApp.tmpApkDir.absolutePath)
if (config.debuggable) add("-d")
add("-l"); add(config.sigBypassLevel.toString())
if (config.useManager) add("--manager")
if (config.useManager) {
add("--manager")
add("--manager-package"); add(config.resolveManagerPackageName())
}
if (config.overrideVersionCode) add("-r")
if (Configs.detailPatchLogs) add("-v")
embeddedModules?.forEach {
Expand Down
313 changes: 280 additions & 33 deletions manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.PatchConfig
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
Expand Down Expand Up @@ -92,7 +93,15 @@ class NewPatchViewModel : ViewModel() {
if (useManager) embeddedModules = emptyList()
patchOptions = Patcher.Options(
injectDex = injectDex,
config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null),
config = PatchConfig(
useManager,
debuggable,
overrideVersionCode,
sigBypassLevel,
null,
null,
if (useManager) lspApp.packageName else null
),
apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()),
embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,16 @@ class AppManageViewModel : ViewModel() {
}
}
}
Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths))
val updatedConfig = PatchConfig(
config.useManager,
config.debuggable,
config.overrideVersionCode,
config.sigBypassLevel,
config.originalSignature,
config.appComponentFactory,
if (config.useManager) lspApp.packageName else null
)
Patcher.patch(logger, Patcher.Options(false, updatedConfig, patchPaths, embeddedModulePaths))
val (status, message) = LSPPackageManager.install()
if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
}
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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ object LSPPackageManager {

suspend fun install(): Pair<Int, String?> {
Log.i(TAG, "Perform install patched apks")
return withContext(Dispatchers.IO) {
runCatching {
val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null")
val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null")
val files = root.listFiles().mapNotNull { file ->
if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) != true) return@mapNotNull null
val input = lspApp.contentResolver.openInputStream(file.uri)
?: throw IOException("Cannot open input stream")
val tmp = lspApp.tmpApkDir.resolve(file.name!!)
input.use { inp -> tmp.outputStream().use { out -> inp.copyTo(out) } }
tmp
}
if (files.isEmpty()) throw IOException("No patched apks to install")
installApks(files)
}.getOrElse {
PackageInstaller.STATUS_FAILURE to (it.message + "\n" + it.stackTraceToString())
}
}
}

suspend fun installApks(apks: List<File>): Pair<Int, String?> {
Log.i(TAG, "Perform install ${apks.size} apk(s)")
var status = PackageInstaller.STATUS_FAILURE
var message: String? = null
withContext(Dispatchers.IO) {
Expand All @@ -90,15 +112,10 @@ object LSPPackageManager {
flags = flags or PackageManagerHidden.INSTALL_ALLOW_TEST or PackageManagerHidden.INSTALL_REPLACE_EXISTING
Refine.unsafeCast<SessionParamsHidden>(params).installFlags = flags
ShizukuApi.createPackageInstallerSession(params).use { session ->
val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null")
val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null")
root.listFiles().forEach { file ->
if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) != true) return@forEach
apks.forEach { file ->
Log.d(TAG, "Add ${file.name}")
val input = lspApp.contentResolver.openInputStream(file.uri)
?: throw IOException("Cannot open input stream")
input.use {
session.openWrite(file.name!!, 0, input.available().toLong()).use { output ->
file.inputStream().use { input ->
session.openWrite(file.name, 0, file.length()).use { output ->
input.copyTo(output)
session.fsync(output)
}
Expand Down
101 changes: 101 additions & 0 deletions manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.lsposed.lspatch.util

import android.content.pm.PackageInstaller
import android.util.Base64
import android.util.Log
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.share.PatchConfig
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import org.lsposed.patch.util.Logger
import java.io.FileNotFoundException
import java.util.zip.ZipFile

object LocalAppsUpdater {

private const val TAG = "LocalAppsUpdater"

private val logger = object : Logger() {
override fun d(msg: String) {
if (verbose) Log.d(TAG, msg)
}

override fun i(msg: String) {
Log.i(TAG, msg)
}

override fun e(msg: String) {
Log.e(TAG, msg)
}
}

fun listLocalApps(): List<Pair<AppInfo, PatchConfig>> {
return LSPPackageManager.appList.mapNotNull { appInfo ->
appInfo.app.metaData?.getString("lspatch")?.let {
val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)
val config = Gson().fromJson(json, PatchConfig::class.java)
if (config.useManager) appInfo to config else null
}
}
}

/**
* Re-patches every Local-mode app so their config points at [managerPackageName].
*/
suspend fun updateAllForManager(managerPackageName: String) = withContext(Dispatchers.IO) {
val apps = listLocalApps()
Log.i(TAG, "Updating ${apps.size} Local apps for manager $managerPackageName")
for ((appInfo, config) in apps) {
updateOne(appInfo, config, managerPackageName)
}
}

private suspend fun updateOne(appInfo: AppInfo, config: PatchConfig, managerPackageName: String) {
Log.i(TAG, "Update loader for ${appInfo.app.packageName}")
LSPPackageManager.cleanTmpApkDir()
val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray())
val patchPaths = mutableListOf<String>()
val embeddedModulePaths = mutableListOf<String>()
for (apk in apkPaths) {
ZipFile(apk).use { zip ->
var entry = zip.getEntry(Constants.ORIGINAL_APK_ASSET_PATH)
if (entry == null) entry = zip.getEntry("assets/lspatch/origin_apk.bin")
if (entry == null) throw FileNotFoundException("Original apk entry not found for $apk")
zip.getInputStream(entry).use { input ->
val dst = lspApp.tmpApkDir.resolve(apk.substringAfterLast('/'))
patchPaths.add(dst.absolutePath)
dst.outputStream().use { output -> input.copyTo(output) }
}
}
}
ZipFile(appInfo.app.sourceDir).use { zip ->
zip.entries().iterator().forEach { entry ->
if (entry.name.startsWith(Constants.EMBEDDED_MODULES_ASSET_PATH)) {
val dst = lspApp.tmpApkDir.resolve(entry.name.substringAfterLast('/'))
embeddedModulePaths.add(dst.absolutePath)
zip.getInputStream(entry).use { input ->
dst.outputStream().use { output -> input.copyTo(output) }
}
}
}
}
val updated = PatchConfig(
config.useManager,
config.debuggable,
config.overrideVersionCode,
config.sigBypassLevel,
config.originalSignature,
config.appComponentFactory,
managerPackageName
)
Patcher.patch(logger, Patcher.Options(false, updated, patchPaths, embeddedModulePaths))
val (status, message) = LSPPackageManager.install()
if (status != PackageInstaller.STATUS_SUCCESS) {
throw RuntimeException("Failed to install updated ${appInfo.app.packageName}: $message")
}
}
}
Loading