Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
06ed8e7
Keep the interceptor chain consistent after a protective recovery
JingMatrix Jul 26, 2026
98f2637
Do not fail a hook or an invoker on an executable with no hooks
JingMatrix Jul 26, 2026
80eb1e2
Parse module.prop as the Java Properties file it is specified to be
JingMatrix Jul 26, 2026
889e42f
Stop one malformed module from emptying the manager's module list
JingMatrix Jul 26, 2026
60a3af2
Honour the clear flag of a remote preference diff
JingMatrix Jul 26, 2026
8a3eb12
Always answer a scope request through its callback
JingMatrix Jul 26, 2026
dbdf45f
Give hookers the executable's own exception from an invoker chain
JingMatrix Jul 26, 2026
3da31f3
Resolve ExceptionMode.DEFAULT from module.prop
JingMatrix Jul 26, 2026
56be396
Stop reporting the android:ui process as the system package
JingMatrix Jul 26, 2026
f0010c4
Report a missing or forbidden remote file as FileNotFoundException
JingMatrix Jul 26, 2026
f07c704
Load modules into a late-injected system server
JingMatrix Jul 27, 2026
f152873
Invoke an unhooked constructor through the non-virtual path
JingMatrix Jul 27, 2026
d905c29
Stop hookClassInitializer from aborting the process
JingMatrix Jul 27, 2026
087f0ff
Implement hookClassInitializer without initializing the class
JingMatrix Jul 27, 2026
ec703e0
Drop the JNI static initializer lookup
JingMatrix Jul 27, 2026
59a93b4
Find a static initializer by the gap in the method run
JingMatrix Jul 27, 2026
423b094
Hand the hooker a Method for a static initializer
JingMatrix Jul 27, 2026
9a0fcd5
Report an invoker's target exception the way Method.invoke does
JingMatrix Jul 27, 2026
e757b77
Snapshot the arguments Chain.getArgs hands out
JingMatrix Jul 27, 2026
7c1090d
Send a remote preference update only to the user that made it
JingMatrix Jul 27, 2026
5428b33
Undo two parsing changes made only to match the other side
JingMatrix Jul 27, 2026
4f61719
Survive a module.prop that Properties.load rejects
JingMatrix Jul 27, 2026
091908d
Show the API version each module targets
JingMatrix Jul 27, 2026
606eacf
Keep an already-enabled app visible under a static scope
JingMatrix Jul 27, 2026
6300802
Enforce staticScope in the daemon, not just in the picker
JingMatrix Jul 27, 2026
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
28 changes: 28 additions & 0 deletions app/src/main/java/org/lsposed/manager/adapters/ScopeAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.ItemMasterSwitchBinding;
import org.lsposed.manager.databinding.ItemModuleBinding;
import org.lsposed.manager.databinding.ItemScopeFooterBinding;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.fragment.AppListFragment;
import org.lsposed.manager.ui.fragment.CompileDialogFragment;
Expand Down Expand Up @@ -126,6 +127,28 @@ public int getItemCount() {
}
};

/**
* staticScope says the module's scope list is the whole of it, so the list above shows only the
* apps it claims and this rules it off. Nothing follows for a module that does not claim one.
*/
public RecyclerView.Adapter<RecyclerView.ViewHolder> footerAdaptor = new RecyclerView.Adapter<>() {
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new RecyclerView.ViewHolder(ItemScopeFooterBinding.inflate(activity.getLayoutInflater(), parent, false).getRoot()) {
};
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
}

@Override
public int getItemCount() {
return module.staticScope ? 1 : 0;
}
};

private final OnMainSwitchChangeListener switchBarOnCheckedChangeListener = new OnMainSwitchChangeListener() {
@Override
public void onSwitchChanged(Switch view, boolean isChecked) {
Expand Down Expand Up @@ -531,6 +554,11 @@ public void refresh(boolean force) {
synchronized (tmpRecList) {
tmpRecList.add(application);
}
} else if (module.staticScope && !tmpChkList.contains(application)) {
// The module says its scope list is the whole of it, so nothing outside that
// list is offered. An app already enabled outside it still has to be shown, or
// it would keep the module loaded from a row nobody can see to switch off.
return;
} else if (shouldHideApp(info, application, tmpChkList)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
var concatAdapter = new ConcatAdapter();
concatAdapter.addAdapter(scopeAdapter.switchAdaptor);
concatAdapter.addAdapter(scopeAdapter);
concatAdapter.addAdapter(scopeAdapter.footerAdaptor);
binding.recyclerView.setAdapter(concatAdapter);
binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.SparseArray;
Expand All @@ -51,6 +52,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.TooltipCompat;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.MenuProvider;
Expand Down Expand Up @@ -558,6 +560,7 @@ public void onLoadCleared(@Nullable Drawable placeholder) {
sb = new SpannableStringBuilder();

int installXposedVersion = ConfigManager.getXposedApiVersion();
bindApiBadge(holder.apiBadge, item, installXposedVersion);
String warningText = null;
if (item.minVersion == 0) {
warningText = getString(R.string.no_min_version_specified);
Expand Down Expand Up @@ -748,12 +751,84 @@ public boolean isLoaded() {
return isLoaded && moduleUtil.isModulesLoaded();
}

/**
* The API version a module targets, printed under its icon. The word stays in the ordinary
* secondary colour and the version itself carries the colour, so the eye lands on the one
* part that differs between modules. The whole statement goes in the content description
* and the tooltip, so the colour is never the only thing carrying it.
*/
private void bindApiBadge(TextView badge, ModuleUtil.InstalledModule item, int frameworkApi) {
final String label;
final String status;
final String version;
final int color;

if (item.legacy) {
// A legacy module reports the original Xposed API, which is numbered separately
// from the one this framework implements, so it is never compared against it.
version = item.minVersion > 0 ? String.valueOf(item.minVersion) : null;
label = version != null
? getString(R.string.module_api_badge_legacy, item.minVersion)
: getString(R.string.module_api_badge_legacy_unknown);
status = getString(R.string.module_api_status_legacy);
color = android.R.attr.textColorSecondary;
} else if (item.minVersion <= 0 || item.targetVersion <= 0 || frameworkApi <= 0) {
// Both keys are required of a module. Reporting the one it did declare would put a
// version on screen for a module that never stated which API it was built for.
version = null;
label = getString(R.string.module_api_badge_unknown);
status = getString(R.string.module_api_status_unknown);
color = android.R.attr.textColorSecondary;
} else {
version = String.valueOf(item.targetVersion);
label = getString(R.string.module_api_badge, item.targetVersion);
if (item.minVersion > frameworkApi) {
// It declares it needs more than we have, whatever it targets.
status = getString(R.string.module_api_status_unsupported, item.minVersion, frameworkApi);
color = com.google.android.material.R.attr.colorError;
} else if (item.targetVersion == frameworkApi) {
status = getString(R.string.module_api_status_current);
color = androidx.appcompat.R.attr.colorPrimary;
} else {
status = getString(item.targetVersion > frameworkApi
? R.string.module_api_status_newer
: R.string.module_api_status_older,
item.targetVersion, frameworkApi);
color = android.R.attr.textColorSecondary;
}
}

final int resolved = ResourceUtils.resolveColor(requireActivity().getTheme(), color);
final SpannableStringBuilder text = new SpannableStringBuilder(label);
final int at = version == null ? -1 : label.lastIndexOf(version);
if (at > 0) {
// Small caps: the capitals of the word set smaller than the version beside them, so
// the word reads as a label and the version reads as the value.
text.setSpan(new RelativeSizeSpan(0.78f), 0, at, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
text.setSpan(new StyleSpan(Typeface.BOLD), at, label.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
text.setSpan(new ForegroundColorSpan(resolved), at, label.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
text.setSpan(new RelativeSizeSpan(0.78f), 0, label.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
text.setSpan(new ForegroundColorSpan(resolved), 0, label.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}

badge.setText(text);
badge.setContentDescription(status);
TooltipCompat.setTooltipText(badge, status);
badge.setVisibility(View.VISIBLE);
}

static class ViewHolder extends RecyclerView.ViewHolder {
ConstraintLayout root;
ImageView appIcon;
TextView appName;
TextView appDescription;
TextView appVersion;
TextView apiBadge;
TextView hint;
MaterialCheckBox checkBox;

Expand All @@ -764,6 +839,7 @@ static class ViewHolder extends RecyclerView.ViewHolder {
appName = binding.appName;
appDescription = binding.description;
appVersion = binding.versionName;
apiBadge = binding.apiBadge;
hint = binding.hint;
checkBox = binding.checkbox;
}
Expand Down
29 changes: 22 additions & 7 deletions app/src/main/java/org/lsposed/manager/util/ModuleUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public static synchronized ModuleUtil getInstance() {
}

public static int extractIntPart(String str) {
// minApiVersion and targetApiVersion are required of a module, but a third-party module
// omitting one must not take down the module list.
if (str == null) return 0;
int result = 0, length = str.length();
for (int offset = 0; offset < length; offset++) {
char c = str.charAt(offset);
Expand Down Expand Up @@ -295,11 +298,20 @@ private InstalledModule(PackageInfo pkg, ZipFile modernModuleApk) {
try (modernModuleApk) {
var propEntry = modernModuleApk.getEntry("META-INF/xposed/module.prop");
if (propEntry != null) {
var prop = new Properties();
prop.load(modernModuleApk.getInputStream(propEntry));
minVersion = extractIntPart(prop.getProperty("minApiVersion"));
targetVersion = extractIntPart(prop.getProperty("targetApiVersion"));
staticScope = TextUtils.equals(prop.getProperty("staticScope"), "true");
try {
var prop = new Properties();
prop.load(modernModuleApk.getInputStream(propEntry));
minVersion = extractIntPart(prop.getProperty("minApiVersion"));
targetVersion = extractIntPart(prop.getProperty("targetApiVersion"));
staticScope = TextUtils.equals(prop.getProperty("staticScope"), "true");
} catch (IllegalArgumentException e) {
// Properties.load rejects a malformed unicode escape, and nothing read
// out of the file before that point can be trusted either, so the
// module reports no version rather than the defaults above.
minVersion = 0;
targetVersion = 0;
Log.e(App.TAG, "Malformed module.prop in " + pkg.packageName, e);
}
}
var scopeEntry = modernModuleApk.getEntry("META-INF/xposed/scope.list");
if (scopeEntry != null) {
Expand All @@ -309,8 +321,11 @@ private InstalledModule(PackageInfo pkg, ZipFile modernModuleApk) {
} else {
scopeList = Collections.emptyList();
}
} catch (IOException | OutOfMemoryError e) {
Log.e(App.TAG, "Error while closing modern module APK", e);
} catch (IOException | OutOfMemoryError | IllegalArgumentException e) {
// Properties.load is documented to throw IllegalArgumentException for a
// malformed unicode escape. Uncaught, one such module.prop leaves the user
// with an empty module list instead of one unreadable entry.
Log.e(App.TAG, "Error while reading modern module APK", e);
}
this.minVersion = minVersion;
this.targetVersion = targetVersion;
Expand Down
22 changes: 21 additions & 1 deletion app/src/main/res/layout/item_module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,32 @@
android:id="@+id/app_icon"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/api_badge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription"
tools:srcCompat="@tools:sample/backgrounds/scenic" />

<!-- Under the icon, out of the way of the name, the version and the description. -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/api_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false"
android:textSize="11sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/app_icon"
app:layout_constraintStart_toStartOf="@id/app_icon"
app:layout_constraintTop_toBottomOf="@id/app_icon"
tools:text="api 101"
tools:visibility="visible" />

<com.google.android.material.textview.MaterialTextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
Expand Down
52 changes: 52 additions & 0 deletions app/src/main/res/layout/item_scope_footer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of LSPosed.
~
~ LSPosed is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ LSPosed is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2026 LSPosed Contributors
-->

<!-- Closes the list of a module that claims a fixed scope, since nothing else can follow it. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp"
android:paddingVertical="24dp">

<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="?attr/colorOutlineVariant" />

<com.google.android.material.textview.MaterialTextView
android:id="@+id/scope_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:text="@string/module_scope_static"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary" />

<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="?attr/colorOutlineVariant" />

</LinearLayout>
11 changes: 11 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@
<string name="warning_xposed_min_version">This module requires a newer Xposed version (%d) and thus cannot be activated</string>
<string name="warning_target_version_higher">This module is designed for a newer Xposed version (%d) and thus some functionalities may not work</string>
<string name="no_min_version_specified">This module does not specify the Xposed version it needs.</string>
<string name="module_api_badge">API %1$d</string>
<string name="module_api_badge_unknown">API ?</string>
<string name="module_api_badge_legacy">Xposed %1$d</string>
<string name="module_api_badge_legacy_unknown">Xposed ?</string>
<string name="module_api_status_current">Targets the API this framework implements</string>
<string name="module_api_status_older">Targets API %1$d, older than the API this framework implements (%2$d)</string>
<string name="module_api_status_newer">Targets API %1$d, newer than the API this framework implements (%2$d)</string>
<string name="module_api_status_unsupported">Needs API %1$d, more than this framework implements (%2$d)</string>
<string name="module_api_status_legacy">Uses the original Xposed API through the legacy bridge</string>
<string name="module_api_status_unknown">The targeted API version is unknown</string>
<string name="module_scope_static">Static Scope</string>
<string name="warning_min_version_too_low">This module was created for Xposed version %1$d, but due to incompatible changes in version %2$d, it has been disabled</string>
<string name="warning_installed_on_external_storage">This module cannot be loaded because it\'s installed on the SD card, please move it to internal storage</string>
<string name="module_uninstall">Uninstall</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ object VectorService : IDaemonService.Stub() {
}
}
}
.onFailure { runCatching { iCallback.onScopeRequestFailed(it.message) } }
// onScopeRequestFailed declares @NonNull, and Throwable.message is frequently null.
.onFailure { runCatching { iCallback.onScopeRequestFailed(it.message ?: it.toString()) } }

NotificationManager.cancelNotification(
NotificationManager.SCOPE_CHANNEL_ID, packageName, userId)
Expand Down
Loading
Loading