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
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
holder.appVersion.setVisibility(View.VISIBLE);
holder.appVersion.setText(item.versionName);
holder.appVersion.setText(formatVersion(item));
holder.appVersion.setSelected(true);
} else {
holder.itemView.setTag(item);
Expand All @@ -652,6 +652,19 @@ public void onLoadCleared(@Nullable Drawable placeholder) {
}
}

/**
* The module version line, with the Xposed API the module was built against. Which API a
* module targets decides how the framework loads it, so it belongs next to the version
* rather than only in the warning that fires when it is out of range.
*/
private String formatVersion(ModuleUtil.InstalledModule item) {
if (item.targetVersion <= 0) return item.versionName;
var api = item.minVersion > 0 && item.minVersion != item.targetVersion
? getString(R.string.module_api_version_range, item.minVersion, item.targetVersion)
: getString(R.string.module_api_version, item.targetVersion);
return item.versionName + " · " + api;
}

@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
holder.itemView.setTag(null);
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
<item quantity="other">%d 个模块可更新</item>
</plurals>
<string name="about_view_source_code"><![CDATA[在 %1$s 查看源码<br/>加入我们的 %2$s 频道<br/>加入我们的 <b><a href="mqqguild://guild/share?inviteCode=Xz9dJ&from=246610">QQ 频道</a></b>]]></string>
<string name="translators" comment="Dear translators, please add your home page here following the format. There can be more than one translator. So feel free to append yourself.">LSPosed
JingMatrix</string>
<string name="translators" comment="Dear translators, please add your home page here following the format. There can be more than one translator. So feel free to append yourself.">LSPosed、JingMatrix</string>
<string name="install">安装</string>
<string name="install_summary">点击安装 LSPosed</string>
<string name="not_installed">未安装</string>
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<item quantity="other">%d 個模組可更新</item>
</plurals>
<string name="about_view_source_code"><![CDATA[在 %1$s 檢視原始碼<br/>加入我們的 %2$s 頻道]]></string>
<string name="translators" comment="Dear translators, please add your home page here following the format. There can be more than one translator. So feel free to append yourself.">孟武. 尼德霍格. 龍、david082321、beigua87</string>
<string name="translators" comment="Dear translators, please add your home page here following the format. There can be more than one translator. So feel free to append yourself.">孟武. 尼德霍格. 龍、david082321、beigua87、HSSkyBoy</string>
<string name="install">安裝</string>
<string name="install_summary">點選安裝 LSPosed</string>
<string name="not_installed">未安裝</string>
Expand Down Expand Up @@ -138,9 +138,13 @@
<string name="menu_show_modules">模組</string>
<string name="failed_to_save_scope_list">作用域列表儲存失敗</string>
<string name="app_version">版本:%1$s</string>
<string name="menu_select">選擇</string>
<string name="use_recommended">推薦程式</string>
<string name="no_scope_selected_has_recommended">未選擇任何程式,選擇推薦的程式?</string>
<string name="use_recommended_message">選擇推薦的程式?</string>
<string name="menu_select_all">全部</string>
<string name="menu_select_none">無</string>
<string name="menu_auto_include">自動加入</string>
<string name="module_is_not_activated_yet">Xposed 模組尚未啟用</string>
<string name="requested_by_module">推薦啟用</string>
<string name="update_available">可用更新:%1$s</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<string name="developer_warning_title">Tips for module developer</string>
<string name="developer_warning_summary">Please disable deploy optimizations on Android Studio, or use `gradlew installDebug` command to install. Otherwise the module apk will not be updated.</string>
<string name="info_api_version">API version</string>
<string name="module_api_version">API %1$d</string>
<string name="module_api_version_range">API %1$d–%2$d</string>
<string name="info_framework_version">Framework version</string>
<string name="info_manager_package_name">Manager package name</string>
<string name="info_system_version">System version</string>
Expand Down
3 changes: 3 additions & 0 deletions daemon/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@
public static *** v(...);
public static *** d(...);
}
# The libxposed annotations are compile-only metadata and are not packaged
-dontwarn io.github.libxposed.annotation.**

-repackageclasses
-allowaccessmodification
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import org.lsposed.lspd.models.Application
import org.lsposed.lspd.models.Module
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.VectorDaemon
import org.matrix.vector.daemon.ipc.ApplicationService
import org.matrix.vector.daemon.ipc.InjectedModuleService
import org.matrix.vector.daemon.ipc.ModuleService
import org.matrix.vector.daemon.system.*
import org.matrix.vector.daemon.utils.InstallerVerifier
import org.matrix.vector.daemon.utils.applySqliteHelperWorkaround
Expand Down Expand Up @@ -187,6 +189,7 @@ object ConfigCache {
packageName = pkgName
this.apkPath = apkPath
appId = appInfo.uid
versionCode = pkgInfo?.longVersionCode ?: 0L
applicationInfo = appInfo
service = oldModule?.service ?: InjectedModuleService(pkgName)
file = preLoadedApk
Expand Down Expand Up @@ -259,6 +262,16 @@ object ConfigCache {
state = oldState.copy(modules = newModules, scopes = newScopes)

Log.d(TAG, "Cache Update Complete. Map Swap successful.")

// After the swap, so a target is only dropped once the module is really gone.
(oldState.modules.keys - newModules.keys).forEach {
ApplicationService.forgetHotReloadTargets(it)
}

ApplicationService.backfillLoadedVersions()

// After the swap, so targets are asked to load the generation that was just installed.
newModules.values.forEach { ModuleService.autoHotReload(it) }
// Log.d(TAG, "cached modules:")
// newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") }

Expand Down Expand Up @@ -382,6 +395,7 @@ object ConfigCache {
runCatching {
@Suppress("DEPRECATION")
val pkg = PackageParser().parsePackage(File(apkPath), 0, false)
// A raw parse carries no version; backfillLoadedVersions supplies it later.
module.applicationInfo = pkg.applicationInfo
}
.onFailure {
Expand Down
38 changes: 27 additions & 11 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import java.nio.file.attribute.PosixFilePermissions
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Properties
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
Expand Down Expand Up @@ -160,6 +161,10 @@ object FileSystem {
return memory
}

// Matches ModuleUtil.extractIntPart in the manager, so the two cannot disagree on "101.0".
private fun leadingInt(value: String?): Int =
value?.trim()?.takeWhile { it.isDigit() }?.toIntOrNull() ?: 0

/** Parses the module APK, extracts init lists, and loads DEXes into SharedMemory. */
fun loadModule(apkPath: String, obfuscate: Boolean): PreLoadedApk? {
val file = File(apkPath)
Expand All @@ -170,23 +175,31 @@ object FileSystem {
val moduleClassNames = mutableListOf<String>()
val moduleLibraryNames = mutableListOf<String>()
var isLegacy = false
var targetApiVersion = 0
var minApiVersion = 0
var autoHotReload = false

runCatching {
ZipFile(file).use { zip ->
// Parse module.prop to get targetApiVersion
// module.prop is specified as Java Properties format. Parsing it by hand mishandles
// ':' as a separator, '!' comments, escapes and line continuations, and the manager
// app already reads the same file with Properties.load.
val props =
zip.getEntry("META-INF/xposed/module.prop")?.let { entry ->
zip.getInputStream(entry).bufferedReader().useLines { lines ->
lines
.filter { it.contains("=") }
.associate {
val parts = it.split("=", limit = 2)
parts[0].trim() to parts[1].trim()
}
Properties().apply {
zip.getEntry("META-INF/xposed/module.prop")?.let { entry ->
// Properties.load rejects a malformed \uXXXX escape, which the old hand-rolled
// parser tolerated. Keep that tolerance: a bad module.prop must not make the
// APK unloadable, not least because a legacy module is selected by
// assets/xposed_init and needs no module.prop at all.
runCatching { zip.getInputStream(entry).use { load(it) } }
.onFailure { Log.w(TAG, "Malformed module.prop in $apkPath", it) }
}
} ?: emptyMap()
}

val targetApi = props["targetApiVersion"]?.toIntOrNull() ?: 0
val targetApi = leadingInt(props.getProperty("targetApiVersion"))
targetApiVersion = targetApi
minApiVersion = leadingInt(props.getProperty("minApiVersion"))
autoHotReload = props.getProperty("autoHotReload")?.trim().toBoolean()
val hasLegacyFile = zip.getEntry("assets/xposed_init") != null

// Determine Loading Strategy based on Priority: API 101+ > Legacy > API 100
Expand Down Expand Up @@ -263,6 +276,9 @@ object FileSystem {
this.moduleClassNames = moduleClassNames
this.moduleLibraryNames = moduleLibraryNames
this.legacy = isLegacy
this.targetApiVersion = targetApiVersion
this.minApiVersion = minApiVersion
this.autoHotReload = autoHotReload
}

return preLoadedApk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import android.os.ParcelFileDescriptor
import android.os.Process
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.HookedProcess
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.IHotReloadTarget
import org.lsposed.lspd.service.ILSPApplicationService
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.FileSystem
Expand All @@ -30,8 +34,30 @@ object ApplicationService : ILSPApplicationService.Stub() {

private val processes = ConcurrentHashMap<ProcessKey, ProcessInfo>()

/** One module generation loaded into one process: what a hot reload request addresses. */
class HotReloadTarget(
val id: Long,
val modulePackageName: String,
val processName: String,
val uid: Int,
val pid: Int,
@Volatile var loadedVersionCode: Long,
val hotReloadable: Boolean,
) {
val state = AtomicInteger(HookedProcess.TARGET_STATE_UP_TO_DATE)
}

private val hotReloadTargets = ConcurrentHashMap<Long, HotReloadTarget>()

// Ids are framework-assigned and never reused, as HookedProcess.targetId requires.
private val nextHotReloadTargetId = AtomicLong(1)

private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) :
IBinder.DeathRecipient {
val targetIds = ConcurrentHashMap<String, Long>()

@Volatile var hotReloadBinder: IHotReloadTarget? = null

init {
heartBeat.linkToDeath(this, 0)
processes[key] = this
Expand All @@ -40,9 +66,114 @@ object ApplicationService : ILSPApplicationService.Stub() {
override fun binderDied() {
heartBeat.unlinkToDeath(this, 0)
processes.remove(key)
targetIds.values.forEach { hotReloadTargets.remove(it) }
}
}

private fun recordHotReloadTargets(info: ProcessInfo, modules: List<Module>) {
for (module in modules) {
info.targetIds.computeIfAbsent(module.packageName) {
val id = nextHotReloadTargetId.getAndIncrement()
hotReloadTargets[id] =
HotReloadTarget(
id = id,
modulePackageName = module.packageName,
processName = info.processName,
uid = info.key.uid,
pid = info.key.pid,
loadedVersionCode = module.versionCode,
// Hot reload is specified only for modules with exactly one Java entry class.
hotReloadable = module.file.moduleClassNames.size == 1,
)
id
}
}
}

// Not filtered to hot-reloadable targets: the AIDL documents this as hooked processes, and one
// that cannot be reloaded answers UNSUPPORTED rather than disappearing.
fun getHotReloadTargets(modulePackageName: String): List<HookedProcess> {
val installedVersion = ConfigCache.state.modules[modulePackageName]?.versionCode
return hotReloadTargets.values
.filter { it.modulePackageName == modulePackageName }
.map { target ->
HookedProcess().apply {
targetId = target.id
uid = target.uid
pid = target.pid
processName = target.processName
state = reportedState(target, installedVersion)
loadedVersionCode = target.loadedVersionCode
}
}
}

// RELOADING and FAILED describe the last attempt and outrank a version comparison.
private fun reportedState(target: HotReloadTarget, installedVersion: Long?): Int {
val state = target.state.get()
if (state != HookedProcess.TARGET_STATE_UP_TO_DATE) return state
// Zero means unknown, not old; claiming STALE would never be satisfiable by a reload.
if (target.loadedVersionCode == 0L) return state
return if (installedVersion != null && installedVersion != target.loadedVersionCode) {
HookedProcess.TARGET_STATE_STALE
} else {
state
}
}

// system_server records its targets before PMS exists, so they start without a version.
fun backfillLoadedVersions() {
hotReloadTargets.values
.filter { it.loadedVersionCode == 0L }
.forEach { target ->
ConfigCache.state.modules[target.modulePackageName]
?.versionCode
?.takeIf { it != 0L }
?.let { target.loadedVersionCode = it }
}
}

fun forgetHotReloadTargets(modulePackageName: String) {
hotReloadTargets.values.removeIf { it.modulePackageName == modulePackageName }
processes.values.forEach { it.targetIds.remove(modulePackageName) }
}

fun staleHotReloadTargets(modulePackageName: String): List<HotReloadTarget> {
val installedVersion = ConfigCache.state.modules[modulePackageName]?.versionCode ?: return emptyList()
return hotReloadTargets.values.filter {
it.modulePackageName == modulePackageName &&
it.hotReloadable &&
it.loadedVersionCode != 0L &&
it.loadedVersionCode != installedVersion
}
}

fun getHotReloadTarget(targetId: Long, modulePackageName: String): HotReloadTarget? =
hotReloadTargets[targetId]?.takeIf { it.modulePackageName == modulePackageName }

// Reloads are serialized per target, so check and transition must be one atomic step.
fun beginHotReload(target: HotReloadTarget): Boolean {
while (true) {
val current = target.state.get()
if (current == HookedProcess.TARGET_STATE_RELOADING) return false
if (target.state.compareAndSet(current, HookedProcess.TARGET_STATE_RELOADING)) return true
}
}

fun endHotReload(target: HotReloadTarget, state: Int, loadedVersionCode: Long? = null) {
loadedVersionCode?.let { target.loadedVersionCode = it }
target.state.set(state)
}

fun getHotReloadBinder(target: HotReloadTarget): IHotReloadTarget? =
processes[ProcessKey(target.uid, target.pid)]?.hotReloadBinder

override fun registerHotReloadTarget(target: IHotReloadTarget) {
val info = ensureRegistered()
info.hotReloadBinder = target
Log.d(TAG, "Hot reload target registered for ${info.processName} (pid=${info.key.pid})")
}

override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
when (code) {
DEX_TRANSACTION_CODE -> {
Expand Down Expand Up @@ -98,7 +229,8 @@ object ApplicationService : ILSPApplicationService.Stub() {
return ConfigCache.getModulesForProcess(info.processName, info.key.uid)
}

override fun getModulesList() = getAllModules().filter { !it.file.legacy }
override fun getModulesList() =
getAllModules().filter { !it.file.legacy }.also { recordHotReloadTargets(ensureRegistered(), it) }

override fun getLegacyModulesList() = getAllModules().filter { it.file.legacy }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import org.lsposed.lspd.models.Application
import io.github.libxposed.service.IXposedService
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.CliRequest
import org.matrix.vector.daemon.CliResponse
Expand Down Expand Up @@ -40,6 +41,7 @@ object CliHandler {
return mapOf(
"Framework Version" to BuildConfig.VERSION_NAME,
"Version Code" to BuildConfig.VERSION_CODE,
"API Version" to IXposedService.LIB_API,
"Enabled Modules" to ConfigCache.state.modules.size,
"Status Notification" to PreferenceStore.isStatusNotificationEnabled())
}
Expand Down
Loading