Finish lockscreen cards follow-ups

This commit is contained in:
ajp_anton
2026-05-30 17:51:11 +00:00
parent 86888774cc
commit 7d158cb0b1
11 changed files with 863 additions and 69 deletions
@@ -0,0 +1,416 @@
package se.ajpanton.statusbartweak.runtime.layout;
import android.app.KeyguardManager;
import android.app.Notification;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
import se.ajpanton.statusbartweak.runtime.mode.SystemNotificationDisplayStyleDetector;
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
final class LockscreenCardsNotificationRowFilter {
private static final String MODE_LOCK = "LOCK";
private final Map<View, HiddenRowState> hiddenRows = new WeakHashMap<>();
boolean apply(ViewGroup stack) {
return apply(stack, false);
}
boolean apply(ViewGroup stack, boolean allowAfterKeyguardUnlock) {
if (stack == null) {
return false;
}
Context context = stack.getContext();
boolean active = isActive(context, allowAfterKeyguardUnlock);
boolean changed = false;
int childCount = stack.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = stack.getChildAt(i);
if (!isNotificationRow(child)) {
continue;
}
if (active && shouldHideRow(context, child, allowAfterKeyguardUnlock)) {
changed |= hideRow(child);
} else {
changed |= restoreRow(child);
}
}
if (changed) {
requestLayout(stack);
}
return changed;
}
void restoreAll() {
if (hiddenRows.isEmpty()) {
return;
}
ArrayList<View> rows = new ArrayList<>(hiddenRows.keySet());
for (View row : rows) {
restoreRow(row);
}
}
void suppressHiddenRows() {
if (hiddenRows.isEmpty()) {
return;
}
ArrayList<View> rows = new ArrayList<>(hiddenRows.keySet());
for (View row : rows) {
if (row == null || !row.isAttachedToWindow() || !isNotificationRow(row)) {
hiddenRows.remove(row);
continue;
}
hideRow(row);
}
}
boolean isActive(Context context) {
return isActive(context, false);
}
private boolean isActive(Context context, boolean allowAfterKeyguardUnlock) {
if (context == null) {
return false;
}
SbtSettings settings = RuntimeSettingsCache.get(context);
if (settings == null || !settings.clockEnabled) {
return false;
}
if (settings.appIconBlockedModes.isEmpty() && settings.appIconExceptionRules.isEmpty()) {
return false;
}
if (SystemNotificationDisplayStyleDetector.read(context) != NotificationDisplayStyle.CARDS) {
return false;
}
KeyguardManager keyguard = context.getSystemService(KeyguardManager.class);
if (!allowAfterKeyguardUnlock && (keyguard == null || !keyguard.isKeyguardLocked())) {
return false;
}
return true;
}
boolean shouldHideEntry(Context context, Object entry) {
return shouldHideEntry(context, entry, false);
}
private boolean shouldHideEntry(Context context, Object entry, boolean allowAfterKeyguardUnlock) {
if (!isActive(context, allowAfterKeyguardUnlock)) {
return false;
}
SbtSettings settings = RuntimeSettingsCache.get(context);
String packageName = packageFor(entry);
if (!AppIconRules.isValidPackageName(packageName)) {
return false;
}
String text = matchText(null, entry);
AppIconRules.ExceptionRule exceptionRule =
matchingExceptionRule(text, settings.appIconExceptionRules.get(packageName));
return exceptionRule != null
? exceptionRule.blocksMode(MODE_LOCK)
: AppIconRules.isBlocked(settings.appIconBlockedModes, packageName, MODE_LOCK);
}
private boolean shouldHideRow(Context context, View row, boolean allowAfterKeyguardUnlock) {
Object entry = entryForRow(row);
if (shouldHideEntry(context, entry, allowAfterKeyguardUnlock)) {
return true;
}
SbtSettings settings = RuntimeSettingsCache.get(context);
String packageName = packageFor(row);
if (!AppIconRules.isValidPackageName(packageName)) {
return false;
}
String text = matchText(row, entry);
AppIconRules.ExceptionRule exceptionRule =
matchingExceptionRule(text, settings.appIconExceptionRules.get(packageName));
if (exceptionRule != null) {
return exceptionRule.blocksMode(MODE_LOCK);
}
return AppIconRules.isBlocked(settings.appIconBlockedModes, packageName, MODE_LOCK);
}
private boolean hideRow(View row) {
if (row == null) {
return false;
}
if (row.getVisibility() == View.GONE && row.getAlpha() == 0f) {
return false;
}
hiddenRows.computeIfAbsent(row, ignored -> new HiddenRowState(View.VISIBLE, 1f));
boolean changed = false;
if (row.getVisibility() != View.GONE) {
row.setVisibility(View.GONE);
changed = true;
}
if (row.getAlpha() != 0f) {
row.setAlpha(0f);
changed = true;
}
return changed;
}
private boolean restoreRow(View row) {
if (row == null) {
return false;
}
HiddenRowState state = hiddenRows.remove(row);
if (state == null) {
return false;
}
boolean changed = false;
if (row.getVisibility() != state.visibility) {
row.setVisibility(state.visibility);
changed = true;
}
if (row.getAlpha() != state.alpha) {
row.setAlpha(state.alpha);
changed = true;
}
changed |= normalizeRowState(row);
requestLayout(row);
return changed;
}
private boolean normalizeRowState(View row) {
boolean changed = false;
changed |= setBooleanField(row, "mWillBeGone", false);
changed |= setBooleanField(row, "mInShelf", false);
changed |= setBooleanField(row, "mTransformingInShelf", false);
Object viewState = ReflectionSupport.getFieldValue(row, "mViewState");
if (viewState != null) {
changed |= setBooleanField(viewState, "hidden", false);
changed |= setBooleanField(viewState, "gone", false);
}
return changed;
}
private boolean setBooleanField(Object target, String fieldName, boolean value) {
Object current = ReflectionSupport.getFieldValue(target, fieldName);
if (!(current instanceof Boolean) || (Boolean) current == value) {
return false;
}
ReflectionSupport.setFieldValue(target, fieldName, value);
return true;
}
private void requestLayout(View view) {
if (view == null) {
return;
}
view.requestLayout();
view.invalidate();
Object parent = view.getParent();
if (parent instanceof View parentView) {
parentView.requestLayout();
parentView.invalidate();
}
}
private static boolean isNotificationRow(View view) {
return view != null && view.getClass().getName().contains("ExpandableNotificationRow");
}
private Object entryForRow(View row) {
Object value = ReflectionSupport.invokeMethod(row, "getEntry", new Class<?>[0]);
if (value != null) {
return value;
}
value = ReflectionSupport.getFieldValue(row, "mEntry");
return value != null ? value : ReflectionSupport.getFieldValue(row, "entry");
}
private String packageFor(Object target) {
if (target == null) {
return null;
}
StatusBarNotification sbn = statusBarNotification(target);
if (sbn != null && AppIconRules.isValidPackageName(sbn.getPackageName())) {
return sbn.getPackageName();
}
String key = notificationKey(target);
String fromKey = packageFromNotificationKey(key);
if (AppIconRules.isValidPackageName(fromKey)) {
return fromKey;
}
Object packageName = ReflectionSupport.invokeMethod(target, "getPackageName", new Class<?>[0]);
if (packageName instanceof String string && AppIconRules.isValidPackageName(string)) {
return string;
}
packageName = ReflectionSupport.getFieldValue(target, "packageName");
if (packageName instanceof String string && AppIconRules.isValidPackageName(string)) {
return string;
}
packageName = ReflectionSupport.getFieldValue(target, "mPackageName");
if (packageName instanceof String string && AppIconRules.isValidPackageName(string)) {
return string;
}
if (target instanceof View view) {
Object entry = entryForRow(view);
String fromEntry = packageFor(entry);
if (AppIconRules.isValidPackageName(fromEntry)) {
return fromEntry;
}
CharSequence description = view.getContentDescription();
if (description != null) {
String fromDescription = packageFromNotificationKey(description.toString());
if (AppIconRules.isValidPackageName(fromDescription)) {
return fromDescription;
}
}
}
return null;
}
private String notificationKey(Object target) {
Object key = ReflectionSupport.invokeMethod(target, "getKey", new Class<?>[0]);
if (key instanceof String string && !string.isEmpty()) {
return string;
}
key = ReflectionSupport.invokeMethod(target, "getNotificationKey", new Class<?>[0]);
if (key instanceof String string && !string.isEmpty()) {
return string;
}
key = ReflectionSupport.getFieldValue(target, "mKey");
if (key instanceof String string && !string.isEmpty()) {
return string;
}
key = ReflectionSupport.getFieldValue(target, "key");
return key instanceof String string && !string.isEmpty() ? string : null;
}
private StatusBarNotification statusBarNotification(Object target) {
if (target instanceof StatusBarNotification sbn) {
return sbn;
}
Object value = ReflectionSupport.invokeMethod(target, "getSbn", new Class<?>[0]);
if (value instanceof StatusBarNotification sbn) {
return sbn;
}
for (String field : new String[]{"mSbn", "sbn", "mStatusBarNotification", "statusBarNotification"}) {
value = ReflectionSupport.getFieldValue(target, field);
if (value instanceof StatusBarNotification sbn) {
return sbn;
}
}
return null;
}
private String packageFromNotificationKey(String key) {
if (key == null) {
return null;
}
String normalized = key.trim();
int suffix = normalized.indexOf('@');
if (suffix >= 0) {
normalized = normalized.substring(0, suffix);
}
int slash = normalized.indexOf('/');
if (slash >= 0) {
normalized = normalized.substring(0, slash);
}
return AppIconRules.isValidPackageName(normalized) ? normalized : null;
}
private String matchText(View row, Object entry) {
StringBuilder out = new StringBuilder();
append(out, notificationKey(entry));
append(out, notificationKey(row));
appendNotification(out, statusBarNotification(entry));
appendNotification(out, statusBarNotification(row));
if (row != null) {
append(out, row.getContentDescription());
}
return out.toString();
}
private void appendNotification(StringBuilder out, StatusBarNotification sbn) {
if (sbn == null) {
return;
}
append(out, sbn.getKey());
append(out, sbn.getPackageName());
Notification notification = sbn.getNotification();
Bundle extras = notification != null ? notification.extras : null;
if (extras == null) {
return;
}
append(out, extras.getCharSequence(Notification.EXTRA_TITLE));
append(out, extras.getCharSequence(Notification.EXTRA_TITLE_BIG));
append(out, extras.getCharSequence(Notification.EXTRA_TEXT));
append(out, extras.getCharSequence(Notification.EXTRA_BIG_TEXT));
append(out, extras.getCharSequence(Notification.EXTRA_SUB_TEXT));
append(out, extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT));
append(out, extras.getCharSequence(Notification.EXTRA_INFO_TEXT));
CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
if (lines != null) {
for (CharSequence line : lines) {
append(out, line);
}
}
Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
if (messages != null) {
for (Parcelable message : messages) {
append(out, message != null ? message.toString() : null);
}
}
}
private void append(StringBuilder out, CharSequence text) {
if (out == null || text == null || text.length() == 0) {
return;
}
if (out.length() > 0) {
out.append('\n');
}
out.append(text);
}
private AppIconRules.ExceptionRule matchingExceptionRule(
String text,
ArrayList<AppIconRules.ExceptionRule> rules
) {
if (rules == null || rules.isEmpty()) {
return null;
}
String candidateText = text != null ? text : "";
for (AppIconRules.ExceptionRule rule : rules) {
if (rule == null || rule.regex == null || rule.regex.trim().isEmpty()) {
continue;
}
try {
if (Pattern.compile(rule.regex).matcher(candidateText).find()) {
return rule;
}
} catch (PatternSyntaxException ignored) {
// Invalid user regexes are ignored instead of blocking every card.
}
}
return null;
}
private static final class HiddenRowState {
final int visibility;
final float alpha;
HiddenRowState(int visibility, float alpha) {
this.visibility = visibility;
this.alpha = alpha;
}
}
}
@@ -3,6 +3,7 @@ package se.ajpanton.statusbartweak.runtime.layout;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.res.Resources;
import android.os.Looper;
import android.os.PowerManager;
import android.view.View;
import android.view.ViewGroup;
@@ -69,6 +70,7 @@ final class StockLayoutCanvasController {
private final Map<View, RootGeometry> rootGeometries = new WeakHashMap<>();
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
private final Map<View, Integer> settingsIdentityByRoot = new WeakHashMap<>();
private final Map<View, Integer> lockscreenCardsRowFilterSignatures = new WeakHashMap<>();
private final Map<View, Long> clockTimeBucketByRoot = new WeakHashMap<>();
private final Set<View> confirmedAodPluginViews =
Collections.newSetFromMap(new WeakHashMap<>());
@@ -92,6 +94,8 @@ final class StockLayoutCanvasController {
new DebugBoundsOverlayController();
private final LockscreenCardsNotificationStripRenderer lockscreenCardsNotificationStripRenderer =
new LockscreenCardsNotificationStripRenderer();
private final LockscreenCardsNotificationRowFilter lockscreenCardsNotificationRowFilter =
new LockscreenCardsNotificationRowFilter();
private final UnlockedIconSnapshotRenderController unlockedIconRenderController;
// The keyguard carrier text lives outside the statusbar root on this device.
// Keep a weak source reference so the lockscreen statusbar can render it when
@@ -111,11 +115,68 @@ final class StockLayoutCanvasController {
}
installPluginManagerHooks(classLoader);
installCardsNotificationIconSourceHook(classLoader);
installLockscreenCardsNotificationRowFilterHook(classLoader);
installUnlockedAppearanceInvalidationHooks(classLoader);
installShadeHeaderAnimationGuardHook(classLoader);
installViewRootHook(classLoader);
hooksInstalled = true;
}
private void installShadeHeaderAnimationGuardHook(ClassLoader classLoader) {
Class<?> viewClass = ReflectionSupport.requireClass("android.view.View", classLoader);
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setAlpha", chain -> {
Object target = chain.getThisObject();
Object alphaArg = firstArg(chain.getArgs());
if (target instanceof View view && alphaArg instanceof Float alpha) {
if (shouldSuppressConflictingShadeHeaderAlpha(view, alpha)) {
return null;
}
}
return chain.proceed();
});
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setTranslationY", chain -> {
Object target = chain.getThisObject();
Object translationArg = firstArg(chain.getArgs());
if (target instanceof View view && translationArg instanceof Float translationY) {
if (shouldSuppressConflictingShadeHeaderTranslationY(view, translationY)) {
return null;
}
}
return chain.proceed();
});
}
private void installLockscreenCardsNotificationRowFilterHook(ClassLoader classLoader) {
Class<?> stackClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout",
classLoader);
if (stackClass != null) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
stackClass,
"onViewAdded",
chain -> {
Object result = chain.proceed();
if (chain.getThisObject() instanceof ViewGroup stack) {
applyLockscreenCardsNotificationRowFilter(stack);
}
return result;
});
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
stackClass,
"onViewRemoved",
chain -> {
Object result = chain.proceed();
if (chain.getThisObject() instanceof ViewGroup stack) {
applyLockscreenCardsNotificationRowFilter(stack);
}
return result;
});
}
}
private void installUnlockedAppearanceInvalidationHooks(ClassLoader classLoader) {
Class<?> darkIconDispatcherClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.DarkIconDispatcherImpl",
@@ -296,6 +357,12 @@ final class StockLayoutCanvasController {
new ArrayList<>();
Function<Object, Object> wrapped = entryObj -> {
String key = LockscreenCardsNotificationIconSourceStore.keyForEntry(entryObj);
if (!runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
&& lockscreenCardsNotificationRowFilter.shouldHideEntry(
container.getContext(),
entryObj)) {
return null;
}
Object result = original.apply(entryObj);
if (result == null) {
result = fallbackStatusBarIcon(entryObj);
@@ -384,6 +451,10 @@ final class StockLayoutCanvasController {
return chain.proceed();
});
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), viewRootClass, "performTraversals", chain -> {
Object rootObjBefore = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
if (rootObjBefore instanceof View root) {
applyBeforeTraversal(root);
}
Object result = chain.proceed();
Object rootObj = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
if (rootObj instanceof View root) {
@@ -394,9 +465,44 @@ final class StockLayoutCanvasController {
}
private void applyBeforeDraw(View root) {
if (!isViewThread(root)) {
return;
}
applyBeforeDrawInternal(root);
}
private void applyBeforeTraversal(View root) {
if (!isViewThread(root)) {
return;
}
if (root == null) {
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.clockEnabled) {
return;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene == null) {
return;
}
if (activeScene.isLockscreen()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& isNotificationShadeWindowRoot(root)) {
applyLockscreenCardsNotificationRowFilterInTreeIfNeeded(
root,
settings,
activeScene,
true);
lockscreenCardsNotificationRowFilter.suppressHiddenRows();
} else if (activeScene.isUnlocked() && isNotificationShadeWindowRoot(root)) {
lockscreenCardsNotificationRowFilter.restoreAll();
} else if (activeScene.isAod()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) {
lockscreenCardsNotificationRowFilter.suppressHiddenRows();
}
}
private void applyBeforeDrawInternal(View root) {
if (root == null) {
return;
@@ -416,7 +522,22 @@ final class StockLayoutCanvasController {
&& activeScene.isLockscreen()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& canContainLockedStatusBarIconSurfaces(root)) {
applyLockscreenCardsNotificationRowFilterInTreeIfNeeded(
root,
settings,
activeScene,
isNotificationShadeWindowRoot(root));
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
} else if (activeScene != null
&& activeScene.isAod()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) {
lockscreenCardsRowFilterSignatures.remove(root);
lockscreenCardsNotificationRowFilter.suppressHiddenRows();
} else {
lockscreenCardsRowFilterSignatures.remove(root);
if (activeScene != null && activeScene.isUnlocked()) {
lockscreenCardsNotificationRowFilter.restoreAll();
}
}
if (!isUnlockedStatusBarRoot(root)) {
return;
@@ -460,6 +581,7 @@ final class StockLayoutCanvasController {
Integer previous = settingsIdentityByRoot.put(root, identity);
if (previous != null && previous != identity) {
dirtyUnlockedLayoutRoots.add(root);
lockscreenCardsRowFilterSignatures.remove(root);
}
}
@@ -560,6 +682,9 @@ final class StockLayoutCanvasController {
}
private void applyToRoot(View root) {
if (!isViewThread(root)) {
return;
}
applyToRootInternal(root);
}
@@ -889,6 +1014,53 @@ final class StockLayoutCanvasController {
return foundInRoot;
}
private boolean shouldSuppressConflictingShadeHeaderAlpha(View view, float alpha) {
return view != null
&& alpha != view.getAlpha()
&& isConflictingShadeHeaderAnimatorWrite(view);
}
private boolean shouldSuppressConflictingShadeHeaderTranslationY(View view, float translationY) {
return view != null
&& translationY != view.getTranslationY()
&& isConflictingShadeHeaderAnimatorWrite(view);
}
private boolean isConflictingShadeHeaderAnimatorWrite(View view) {
if (view == null
|| !"split_shade_status_bar".equals(resolveIdName(view))
|| !isLayoutEnabled(view.getContext())) {
return false;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
// Samsung runs two QS header animators during lockscreen cards shade pulls.
// LegacyQsOpenAnimator provides the visible fade. LegacyQsExpandAnimator
// writes conflicting values both while tracking expansion and when it clears
// its state after collapse.
return activeScene != null
&& activeScene.isLockscreen()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& !runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
&& (stackContains("LegacyQsExpandAnimator", "setQsExpansionPosition")
|| stackContains("LegacyQsExpandAnimator", "clearAnimationState"));
}
private boolean stackContains(String classNamePart, String methodName) {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stack) {
if (element == null) {
continue;
}
String className = element.getClassName();
if (className != null
&& className.contains(classNamePart)
&& methodName.equals(element.getMethodName())) {
return true;
}
}
return false;
}
private void restoreTrackedLockedStatusBarIconSurfaces(View root) {
if (trackedLockedStatusBarIconSurfaces.isEmpty()) {
return;
@@ -967,6 +1139,9 @@ final class StockLayoutCanvasController {
Object current = view;
while (current instanceof View currentView) {
String className = currentView.getClass().getName();
if ("split_shade_status_bar".equals(resolveIdName(currentView))) {
return false;
}
if (className.contains("KeyguardStatusBarView")
|| className.contains("PhoneStatusBarView")
|| className.contains("StatusBarWindowView")) {
@@ -1054,6 +1229,10 @@ final class StockLayoutCanvasController {
|| className.contains("StatusBarWindowView");
}
private boolean isNotificationShadeWindowRoot(View root) {
return root != null && root.getClass().getName().contains("NotificationShadeWindowView");
}
private void rememberLockedCarrierTextSource(View root) {
if (lockedCarrierTextSource() != null || root == null || !isCarrierView(root)) {
return;
@@ -1167,6 +1346,7 @@ final class StockLayoutCanvasController {
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
lockscreenCardsRowFilterSignatures.remove(root);
clockTimeBucketByRoot.remove(root);
removeDebugOverlay(root);
root.requestLayout();
@@ -1242,7 +1422,7 @@ final class StockLayoutCanvasController {
}
private boolean hideView(View view, boolean keepLaidOut) {
if (view == null) {
if (view == null || !isViewThread(view)) {
return false;
}
int targetVisibility = keepLaidOut ? View.VISIBLE : View.GONE;
@@ -1260,7 +1440,7 @@ final class StockLayoutCanvasController {
}
private boolean restoreView(View view) {
if (view == null) {
if (view == null || !isViewThread(view)) {
return false;
}
HiddenViewState state = hiddenViewStates.remove(view);
@@ -1337,6 +1517,7 @@ final class StockLayoutCanvasController {
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
lockscreenCardsRowFilterSignatures.remove(root);
clockTimeBucketByRoot.remove(root);
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
removeLockscreenCardsNotificationHost(root);
@@ -1348,7 +1529,7 @@ final class StockLayoutCanvasController {
}
private void forceViewVisible(View view) {
if (view == null) {
if (view == null || !isViewThread(view)) {
return;
}
view.setAlpha(1f);
@@ -1357,7 +1538,7 @@ final class StockLayoutCanvasController {
}
private void requestViewAndParentLayout(View view) {
if (view == null) {
if (view == null || !isViewThread(view)) {
return;
}
view.requestLayout();
@@ -1440,6 +1621,72 @@ final class StockLayoutCanvasController {
}
}
private void applyLockscreenCardsNotificationRowFilter(ViewGroup stack) {
applyLockscreenCardsNotificationRowFilter(stack, false);
}
private void applyLockscreenCardsNotificationRowFilter(ViewGroup stack, boolean allowAfterKeyguardUnlock) {
if (stack == null) {
return;
}
if (runtimeContext.getModeStateRepository().isSystemUiShadeLocked()) {
lockscreenCardsNotificationRowFilter.restoreAll();
return;
}
lockscreenCardsNotificationRowFilter.apply(stack, allowAfterKeyguardUnlock);
}
private void applyLockscreenCardsNotificationRowFilterInTreeIfNeeded(
View root,
SbtSettings settings,
SceneKey activeScene,
boolean allowAfterKeyguardUnlock) {
int signature = lockscreenCardsRowFilterSignature(root, settings, activeScene);
Integer previous = lockscreenCardsRowFilterSignatures.put(root, signature);
if (previous != null && previous == signature) {
return;
}
applyLockscreenCardsNotificationRowFilterInTree(root, allowAfterKeyguardUnlock);
}
private int lockscreenCardsRowFilterSignature(
View root,
SbtSettings settings,
SceneKey activeScene) {
int result = 17;
result = 31 * result + System.identityHashCode(settings);
result = 31 * result + (activeScene != null ? activeScene.hashCode() : 0);
result = 31 * result
+ (runtimeContext.getModeStateRepository().isSystemUiShadeLocked() ? 1 : 0);
result = 31 * result + root.getWidth();
result = 31 * result + root.getHeight();
result = 31 * result + (root instanceof ViewGroup group ? group.getChildCount() : 0);
return result;
}
private void applyLockscreenCardsNotificationRowFilterInTree(View root, boolean allowAfterKeyguardUnlock) {
if (!(root instanceof ViewGroup group)) {
return;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(group);
while (!queue.isEmpty()) {
View view = queue.removeFirst();
if (view instanceof ViewGroup viewGroup) {
if (viewGroup.getClass().getName().contains("NotificationStackScrollLayout")) {
applyLockscreenCardsNotificationRowFilter(viewGroup, allowAfterKeyguardUnlock);
return;
}
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
if (child != null) {
queue.addLast(child);
}
}
}
}
}
private View findVisibleBouncerSurface(View root) {
if (root == null) {
return null;
@@ -1542,6 +1789,7 @@ final class StockLayoutCanvasController {
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
lockscreenCardsRowFilterSignatures.remove(root);
clockTimeBucketByRoot.remove(root);
return null;
}
@@ -1628,6 +1876,18 @@ final class StockLayoutCanvasController {
return powerManager == null || powerManager.isInteractive();
}
private boolean isViewThread(View view) {
Looper current = Looper.myLooper();
if (current == null || current != Looper.getMainLooper()) {
return false;
}
if (view == null || view.getHandler() == null) {
return true;
}
Looper looper = view.getHandler().getLooper();
return looper == null || looper == current;
}
private boolean isLayoutEnabled(Context context) {
SbtSettings settings = RuntimeSettingsCache.get(context);
return settings.clockEnabled;
@@ -5,6 +5,7 @@ import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.WeakHashMap;
@@ -47,11 +48,15 @@ public final class LockscreenCardsNotificationIconSourceStore {
return result;
}
int skipCards = visibleShelfStartIndex(container, views);
HashSet<String> visibleCardKeys = visibleCardKeys(root, container);
for (int i = 0; i < views.size(); i++) {
if (i < skipCards) {
continue;
}
SourceEntry entry = views.get(i);
if (entry.key != null && visibleCardKeys.contains(entry.key)) {
continue;
}
View view = entry.view;
if (view == null || !view.isAttachedToWindow()) {
continue;
@@ -61,7 +66,46 @@ public final class LockscreenCardsNotificationIconSourceStore {
if (width <= 0 || height <= 0) {
continue;
}
result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE));
result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE, entry.key));
}
return result;
}
private static HashSet<String> visibleCardKeys(View root, View shelf) {
HashSet<String> result = new HashSet<>();
if (!(root instanceof ViewGroup group)) {
return result;
}
int shelfTop = Integer.MAX_VALUE;
if (shelf != null) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds shelfBounds =
ViewGeometry.boundsRelativeTo(root, shelf);
if (shelfBounds != null) {
shelfTop = shelfBounds.top;
}
}
ArrayList<View> stack = new ArrayList<>();
stack.add(group);
for (int i = 0; i < stack.size(); i++) {
View view = stack.get(i);
if (isNotificationRow(view) && isVisibleCardRow(view)) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds rowBounds =
ViewGeometry.boundsRelativeTo(root, view);
if (rowBounds != null && rowBounds.top < shelfTop) {
String key = keyForRow(view);
if (key != null && !key.isEmpty()) {
result.add(key);
}
}
}
if (view instanceof ViewGroup viewGroup) {
for (int child = 0; child < viewGroup.getChildCount(); child++) {
View childView = viewGroup.getChildAt(child);
if (childView != null) {
stack.add(childView);
}
}
}
}
return result;
}
@@ -176,40 +220,18 @@ public final class LockscreenCardsNotificationIconSourceStore {
return null;
}
private static int visibleCardCountBeforeShelf(View root, View shelf) {
if (!(root instanceof ViewGroup group)) {
return 0;
private static String keyForRow(View view) {
Object entry = ReflectionSupport.invokeMethod(view, "getEntry", new Class<?>[0]);
String key = keyForEntry(entry);
if (key != null && !key.isEmpty()) {
return key;
}
int shelfTop = Integer.MAX_VALUE;
if (shelf != null) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds shelfBounds =
ViewGeometry.boundsRelativeTo(root, shelf);
if (shelfBounds != null) {
shelfTop = shelfBounds.top;
entry = ReflectionSupport.getFieldValue(view, "mEntry");
key = keyForEntry(entry);
if (key != null && !key.isEmpty()) {
return key;
}
}
int count = 0;
ArrayList<View> stack = new ArrayList<>();
stack.add(group);
for (int i = 0; i < stack.size(); i++) {
View view = stack.get(i);
if (isNotificationRow(view) && isVisibleCardRow(view)) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds rowBounds =
ViewGeometry.boundsRelativeTo(root, view);
if (rowBounds != null && rowBounds.top < shelfTop) {
count++;
}
}
if (view instanceof ViewGroup viewGroup) {
for (int child = 0; child < viewGroup.getChildCount(); child++) {
View childView = viewGroup.getChildAt(child);
if (childView != null) {
stack.add(childView);
}
}
}
}
return count;
return keyForIconView(view);
}
private static boolean isNotificationRow(View view) {
@@ -30,6 +30,7 @@ public final class LockscreenCardsNotificationStripRenderer {
private final SnapshotRenderer renderer = new SnapshotRenderer(false, false, false);
private final WeakHashMap<ViewGroup, ArrayList<View>> pillViewsByHost = new WeakHashMap<>();
private final NotificationSeenAppReporter seenAppReporter = new NotificationSeenAppReporter();
public RenderResult render(
View root,
@@ -65,8 +66,10 @@ public final class LockscreenCardsNotificationStripRenderer {
clear(host);
return RenderResult.empty();
}
ArrayList<SnapshotPlacement> unfilteredPlacements = renderer.rawPlacements(root, sources);
seenAppReporter.report(root.getContext(), unfilteredPlacements);
ArrayList<SnapshotPlacement> placements = SnapshotIconFilter.notificationPlacements(
renderer.rawPlacements(root, sources),
unfilteredPlacements,
settings,
scene);
StripLayout layout = layoutPlacements(root, sourceRoot, placements, settings);
@@ -0,0 +1,37 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
final class NotificationSeenAppReporter {
private static final long MIN_REPORT_INTERVAL_MS = 500L;
private final Map<String, Long> lastReportedMs = new HashMap<>();
void report(Context context, ArrayList<SnapshotPlacement> placements) {
if (context == null || placements == null || placements.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
HashSet<String> reportedThisPass = new HashSet<>();
for (SnapshotPlacement placement : placements) {
String packageName = SnapshotIconFilter.notificationPackage(
placement != null ? placement.key : null);
if (packageName == null || !reportedThisPass.add(packageName)) {
continue;
}
Long lastReported = lastReportedMs.get(packageName);
if (lastReported != null && now - lastReported < MIN_REPORT_INTERVAL_MS) {
continue;
}
lastReportedMs.put(packageName, now);
SbtSettings.noteSeenIconApp(context, packageName, now);
}
}
}
@@ -4,6 +4,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
@@ -26,6 +28,13 @@ public final class OwnedIconHostManager {
"statusbartweak.unlocked.chip.host";
public static final String TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST =
"statusbartweak.lockscreen.cards.notification.host";
private static final String[] UNLOCKED_HOST_ORDER = {
TAG_UNLOCKED_STATUS_HOST,
TAG_UNLOCKED_NOTIFICATION_HOST,
TAG_UNLOCKED_CLOCK_HOST,
TAG_UNLOCKED_CARRIER_HOST,
TAG_UNLOCKED_CHIP_HOST
};
public FrameLayout ensureUnlockedStatusHost(View root) {
return ensureHost(root, TAG_UNLOCKED_STATUS_HOST);
@@ -58,7 +67,6 @@ public final class OwnedIconHostManager {
}
FrameLayout existing = findDirectHost(parent, tag);
if (existing != null) {
existing.bringToFront();
return existing;
}
FrameLayout host = new FrameLayout(parent.getContext());
@@ -94,19 +102,22 @@ public final class OwnedIconHostManager {
}
public void setUnlockedHostsVisible(View root, boolean visible) {
setHostVisible(root, TAG_UNLOCKED_STATUS_HOST, visible);
setHostVisible(root, TAG_UNLOCKED_NOTIFICATION_HOST, visible);
setHostVisible(root, TAG_UNLOCKED_CLOCK_HOST, visible);
setHostVisible(root, TAG_UNLOCKED_CARRIER_HOST, visible);
setHostVisible(root, TAG_UNLOCKED_CHIP_HOST, visible);
for (String tag : UNLOCKED_HOST_ORDER) {
setHostVisible(root, tag, visible);
}
}
public void bringUnlockedHostsToFront(View root) {
bringHostToFront(root, TAG_UNLOCKED_STATUS_HOST);
bringHostToFront(root, TAG_UNLOCKED_NOTIFICATION_HOST);
bringHostToFront(root, TAG_UNLOCKED_CLOCK_HOST);
bringHostToFront(root, TAG_UNLOCKED_CARRIER_HOST);
bringHostToFront(root, TAG_UNLOCKED_CHIP_HOST);
if (!(root instanceof ViewGroup parent)) {
return;
}
ArrayList<View> hosts = unlockedHosts(parent);
if (hosts.isEmpty() || unlockedHostsAreAlreadyAtFront(parent, hosts)) {
return;
}
for (View host : hosts) {
host.bringToFront();
}
}
public boolean isOwnedHost(View view) {
@@ -140,17 +151,35 @@ public final class OwnedIconHostManager {
if (host == null) {
return;
}
host.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
int visibility = visible ? View.VISIBLE : View.INVISIBLE;
if (host.getVisibility() != visibility) {
host.setVisibility(visibility);
}
}
private void bringHostToFront(View root, String tag) {
if (!(root instanceof ViewGroup parent) || tag == null) {
return;
}
FrameLayout host = findDirectHost(parent, tag);
private ArrayList<View> unlockedHosts(ViewGroup parent) {
ArrayList<View> hosts = new ArrayList<>();
for (String tag : UNLOCKED_HOST_ORDER) {
View host = findDirectHost(parent, tag);
if (host != null) {
host.bringToFront();
hosts.add(host);
}
}
return hosts;
}
private boolean unlockedHostsAreAlreadyAtFront(ViewGroup parent, List<View> hosts) {
if (parent == null || hosts == null || hosts.isEmpty()
|| parent.getChildCount() < hosts.size()) {
return false;
}
int start = parent.getChildCount() - hosts.size();
for (int i = 0; i < hosts.size(); i++) {
if (parent.getChildAt(start + i) != hosts.get(i)) {
return false;
}
}
return true;
}
}
@@ -11,8 +11,12 @@ import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bou
record SnapshotSource(
View view,
SnapshotMode mode
SnapshotMode mode,
String keyHint
) {
SnapshotSource(View view, SnapshotMode mode) {
this(view, mode, null);
}
}
enum SnapshotMode {
@@ -347,6 +347,10 @@ final class SnapshotRenderer {
private String keyFor(SnapshotSource source, int order) {
View view = source.view();
String keyHint = source.keyHint();
if (keyHint != null && !keyHint.isEmpty()) {
return keyHint;
}
String slot = reflectString(view, "getSlot", "mSlot", "slot");
if (!slot.isEmpty()) {
return slot;
@@ -27,6 +27,8 @@ final class UnlockedLayoutPlanner {
private final SnapshotRenderer chipRenderer;
private final UnlockedChipPlacementController chipPlacementController;
private final UnlockedVerticalOffsetScaler offsetScaler;
private final NotificationSeenAppReporter notificationSeenAppReporter =
new NotificationSeenAppReporter();
UnlockedLayoutPlanner(
SnapshotRenderer statusRenderer,
@@ -130,11 +132,13 @@ final class UnlockedLayoutPlanner {
}
int notificationCount = notificationCountForScene(settings, scene);
if (notificationCount > 0) {
ArrayList<SnapshotPlacement> unfilteredPlacements =
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons);
notificationSeenAppReporter.report(
root != null ? root.getContext() : null,
unfilteredPlacements);
ArrayList<SnapshotPlacement> rawPlacements =
SnapshotIconFilter.notificationPlacements(
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons),
settings,
scene);
SnapshotIconFilter.notificationPlacements(unfilteredPlacements, settings, scene);
if (scene != null
&& scene.isLockscreen()
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
@@ -868,6 +868,29 @@ public final class SbtSettings {
}
}
public static void noteSeenIconApp(Context context, String packageName, long firstSeenMs) {
if (context == null || !AppIconRules.isValidPackageName(packageName)) {
return;
}
try {
if (!MODULE_PACKAGE.equals(context.getPackageName()) && hasSettingsProvider(context)) {
Uri uri = Uri.parse("content://" + SbtSettingsProvider.AUTHORITY);
Bundle extras = new Bundle();
extras.putString(AppIconRules.EXTRA_PACKAGE, packageName);
extras.putLong(AppIconRules.EXTRA_FIRST_SEEN_MS, firstSeenMs);
context.getContentResolver()
.call(uri, SbtSettingsProvider.METHOD_NOTE_SEEN_ICON_APP, null, extras);
return;
}
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
Map<String, Long> seen = AppIconRules.loadSeenSession(prefs);
AppIconRules.noteSeen(seen, packageName, firstSeenMs);
AppIconRules.saveSeenSession(prefs, seen);
} catch (Throwable ignored) {
// This list is advisory UI state; rendering should never fail because it cannot be updated.
}
}
private static boolean hasSettingsProvider(Context context) {
if (context == null) {
return false;
@@ -429,13 +429,9 @@ final class LayoutSectionCopySupport {
row.setTag(COPY_ROW_TAG);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setOrientation(LinearLayout.HORIZONTAL);
TextView label = new TextView(context);
label.setText(R.string.layout_copy_from);
row.addView(label);
LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
spinnerParams.leftMargin = dp(context, 8);
row.addView(spinner, spinnerParams);
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
@@ -460,9 +456,6 @@ final class LayoutSectionCopySupport {
row.setTag(COPY_ROW_TAG);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
TextView label = new TextView(context);
label.setText(R.string.layout_copy_from);
row.addView(label);
Spinner spinner = new Spinner(context);
spinner.setAdapter(new CopyAdapter(context, options));
spinner.setSelection(0, false);
@@ -491,7 +484,6 @@ final class LayoutSectionCopySupport {
LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
spinnerParams.leftMargin = dp(context, 8);
row.addView(spinner, spinnerParams);
return row;
}