Finish lockscreen layout support
This commit is contained in:
+690
-48
@@ -10,11 +10,14 @@ import android.widget.TextView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
@@ -22,7 +25,10 @@ import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SystemNotificationDisplayStyleDetector;
|
||||
import se.ajpanton.statusbartweak.runtime.render.DebugBoundsOverlayController;
|
||||
import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationIconSourceStore;
|
||||
import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationStripRenderer;
|
||||
import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager;
|
||||
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController;
|
||||
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult;
|
||||
@@ -37,6 +43,8 @@ final class StockLayoutCanvasController {
|
||||
private static final String CLASS_AOD_ICONS_CONTROLLER = "aod.T4";
|
||||
private static final String CLASS_AOD_ICONS_ONLY_CONTAINER =
|
||||
"com.samsung.android.uniform.widget.notification.NotificationIconsOnlyContainer";
|
||||
private static final String CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER =
|
||||
"com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl";
|
||||
|
||||
private static final Set<String> TARGET_ID_NAMES = Set.of(
|
||||
"left_clock_container",
|
||||
@@ -60,6 +68,8 @@ final class StockLayoutCanvasController {
|
||||
private final Map<View, HiddenViewState> hiddenViewStates = new WeakHashMap<>();
|
||||
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, Long> clockTimeBucketByRoot = new WeakHashMap<>();
|
||||
private final Set<View> confirmedAodPluginViews =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> layoutOffCleanedRoots =
|
||||
@@ -70,12 +80,18 @@ final class StockLayoutCanvasController {
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> trackedLockedStatusBarIconSurfaces =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> pendingUnlockedAppearanceRefreshRoots =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> dirtyUnlockedLayoutRoots =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<ClassLoader> hookedPluginClassLoaders =
|
||||
Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
private final Map<View, UnlockedHosts> unlockedHostsByRoot = new WeakHashMap<>();
|
||||
private final OwnedIconHostManager ownedIconHostManager = new OwnedIconHostManager();
|
||||
private final DebugBoundsOverlayController debugBoundsOverlayController =
|
||||
new DebugBoundsOverlayController();
|
||||
private final LockscreenCardsNotificationStripRenderer lockscreenCardsNotificationStripRenderer =
|
||||
new LockscreenCardsNotificationStripRenderer();
|
||||
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
|
||||
@@ -94,10 +110,212 @@ final class StockLayoutCanvasController {
|
||||
return;
|
||||
}
|
||||
installPluginManagerHooks(classLoader);
|
||||
installCardsNotificationIconSourceHook(classLoader);
|
||||
installUnlockedAppearanceInvalidationHooks(classLoader);
|
||||
installViewRootHook(classLoader);
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void installUnlockedAppearanceInvalidationHooks(ClassLoader classLoader) {
|
||||
Class<?> darkIconDispatcherClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.DarkIconDispatcherImpl",
|
||||
classLoader);
|
||||
if (darkIconDispatcherClass != null) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
darkIconDispatcherClass,
|
||||
"applyIconTint",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
scheduleUnlockedAppearanceRefreshForAllRoots();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Class<?> statusBarIconViewClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.StatusBarIconView",
|
||||
classLoader);
|
||||
if (statusBarIconViewClass != null) {
|
||||
hookViewAppearanceInvalidation(statusBarIconViewClass, "onDarkChanged");
|
||||
hookViewAppearanceInvalidation(statusBarIconViewClass, "setStaticDrawableColor");
|
||||
hookViewAppearanceInvalidation(statusBarIconViewClass, "setDecorColor");
|
||||
hookViewAppearanceInvalidation(statusBarIconViewClass, "setIconColor");
|
||||
hookViewAppearanceInvalidation(statusBarIconViewClass, "updateIconColor");
|
||||
hookViewLayoutInvalidation(statusBarIconViewClass, "onLayout");
|
||||
hookViewLayoutInvalidation(statusBarIconViewClass, "setVisibleState");
|
||||
}
|
||||
|
||||
Class<?> notificationIconAreaClass = ReflectionSupport.findClassIfExists(
|
||||
CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER,
|
||||
classLoader);
|
||||
if (notificationIconAreaClass != null) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
notificationIconAreaClass,
|
||||
"onDarkChanged",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
scheduleUnlockedAppearanceRefreshForAllRoots();
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
notificationIconAreaClass,
|
||||
"updateTintForIcon",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object icon = chain.getArgs().isEmpty() ? null : chain.getArg(0);
|
||||
if (icon instanceof View view) {
|
||||
scheduleUnlockedAppearanceRefresh(view);
|
||||
} else {
|
||||
scheduleUnlockedAppearanceRefreshForAllRoots();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Class<?> clockClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView",
|
||||
classLoader);
|
||||
if (clockClass != null) {
|
||||
hookViewAppearanceInvalidation(clockClass, "onDarkChanged");
|
||||
hookViewAppearanceInvalidation(clockClass, "setTextColor");
|
||||
hookClockTextInvalidation(clockClass, "notifyTimeChanged");
|
||||
hookClockTextInvalidation(clockClass, "setText");
|
||||
hookViewLayoutInvalidation(clockClass, "onLayout");
|
||||
}
|
||||
|
||||
Class<?> notificationContainerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.NotificationIconContainer",
|
||||
classLoader);
|
||||
if (notificationContainerClass != null) {
|
||||
hookViewLayoutInvalidation(notificationContainerClass, "onLayout");
|
||||
hookViewLayoutInvalidation(notificationContainerClass, "calculateIconXTranslations");
|
||||
}
|
||||
|
||||
Class<?> statusIconContainerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.StatusIconContainer",
|
||||
classLoader);
|
||||
if (statusIconContainerClass != null) {
|
||||
hookViewLayoutInvalidation(statusIconContainerClass, "onLayout");
|
||||
}
|
||||
|
||||
Class<?> batteryClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.battery.BatteryMeterView",
|
||||
classLoader);
|
||||
if (batteryClass != null) {
|
||||
hookViewLayoutInvalidation(batteryClass, "onLayout");
|
||||
}
|
||||
}
|
||||
|
||||
private void hookViewAppearanceInvalidation(Class<?> type, String methodName) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
type,
|
||||
methodName,
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
scheduleUnlockedAppearanceRefresh(view);
|
||||
} else {
|
||||
scheduleUnlockedAppearanceRefreshForAllRoots();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void hookViewLayoutInvalidation(Class<?> type, String methodName) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
type,
|
||||
methodName,
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
markUnlockedLayoutDirty(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void hookClockTextInvalidation(Class<?> type, String methodName) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
type,
|
||||
methodName,
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
scheduleUnlockedClockTextRefresh(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installCardsNotificationIconSourceHook(ClassLoader classLoader) {
|
||||
Class<?> controllerClass = ReflectionSupport.findClassIfExists(
|
||||
CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER,
|
||||
classLoader);
|
||||
if (controllerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
controllerClass,
|
||||
"updateIconsForLayout",
|
||||
chain -> {
|
||||
List<Object> args = chain.getArgs();
|
||||
int functionIndex = -1;
|
||||
int containerIndex = -1;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
Object arg = args.get(i);
|
||||
if (functionIndex < 0 && arg instanceof Function<?, ?>) {
|
||||
functionIndex = i;
|
||||
}
|
||||
if (containerIndex < 0
|
||||
&& arg instanceof View view
|
||||
&& view.getClass().getName().contains("NotificationIconContainer")) {
|
||||
containerIndex = i;
|
||||
}
|
||||
}
|
||||
if (functionIndex < 0 || containerIndex < 0) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Object containerObj = args.get(containerIndex);
|
||||
if (!(containerObj instanceof ViewGroup container)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
if (!isCardsNotificationIconSourceContainer(container)) {
|
||||
Object result = chain.proceed();
|
||||
markUnlockedLayoutDirty(container);
|
||||
return result;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Function<Object, Object> original = (Function<Object, Object>) args.get(functionIndex);
|
||||
ArrayList<LockscreenCardsNotificationIconSourceStore.SourceEntry> captured =
|
||||
new ArrayList<>();
|
||||
Function<Object, Object> wrapped = entryObj -> {
|
||||
String key = LockscreenCardsNotificationIconSourceStore.keyForEntry(entryObj);
|
||||
Object result = original.apply(entryObj);
|
||||
if (result == null) {
|
||||
result = fallbackStatusBarIcon(entryObj);
|
||||
}
|
||||
if (result instanceof View view && !isStockOverflowDotView(view)) {
|
||||
if (key == null) {
|
||||
key = LockscreenCardsNotificationIconSourceStore.keyForIconView(view);
|
||||
}
|
||||
captured.add(LockscreenCardsNotificationIconSourceStore.source(view, key));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
Object[] newArgs = args.toArray(new Object[0]);
|
||||
newArgs[functionIndex] = wrapped;
|
||||
Object result = chain.proceed(newArgs);
|
||||
LockscreenCardsNotificationIconSourceStore.put(container, captured);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installPluginManagerHooks(ClassLoader classLoader) {
|
||||
Class<?> managerClass = ReflectionSupport.findClassIfExists(CLASS_PLUGIN_AOD_MANAGER, classLoader);
|
||||
if (managerClass == null) {
|
||||
@@ -176,6 +394,10 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
|
||||
private void applyBeforeDraw(View root) {
|
||||
applyBeforeDrawInternal(root);
|
||||
}
|
||||
|
||||
private void applyBeforeDrawInternal(View root) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
@@ -190,31 +412,182 @@ final class StockLayoutCanvasController {
|
||||
if (activeScene != null && !activeScene.isUnlocked()) {
|
||||
hideTrackedLockedStatusBarIconSurfaces(null);
|
||||
}
|
||||
if (activeScene != null
|
||||
&& activeScene.isLockscreen()
|
||||
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
|
||||
&& canContainLockedStatusBarIconSurfaces(root)) {
|
||||
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
|
||||
}
|
||||
if (!isUnlockedStatusBarRoot(root)) {
|
||||
return;
|
||||
}
|
||||
if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) {
|
||||
if (activeScene != null && activeScene.isUnlocked()) {
|
||||
if (isKnownRootGeometryChanged(root)) {
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, false);
|
||||
removeDebugOverlay(root);
|
||||
}
|
||||
hideTrackedUnlockedStockSurfaces(root);
|
||||
UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root);
|
||||
if (unlockedHosts != null) {
|
||||
unlockedIconRenderController.refreshUnlockedAppearance(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
unlockedHosts.carrierHost,
|
||||
unlockedHosts.chipHost,
|
||||
unlockedHosts.statusHost,
|
||||
unlockedHosts.notificationHost,
|
||||
activeScene);
|
||||
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleUnlockedAppearanceRefresh(View sourceView) {
|
||||
View root = unlockedStatusBarRootFor(sourceView);
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
scheduleUnlockedAppearanceRefreshForRoot(root);
|
||||
}
|
||||
|
||||
private void scheduleUnlockedClockTextRefresh(View sourceView) {
|
||||
View root = unlockedStatusBarRootFor(sourceView);
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
scheduleUnlockedClockTextRefreshForRoot(root);
|
||||
}
|
||||
|
||||
private void markUnlockedLayoutDirty(View sourceView) {
|
||||
View root = unlockedStatusBarRootFor(sourceView);
|
||||
if (root != null) {
|
||||
dirtyUnlockedLayoutRoots.add(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void markDirtyIfSettingsChanged(View root, SbtSettings settings, boolean statusBarRoot) {
|
||||
if (!statusBarRoot || root == null || settings == null) {
|
||||
return;
|
||||
}
|
||||
int identity = System.identityHashCode(settings);
|
||||
Integer previous = settingsIdentityByRoot.put(root, identity);
|
||||
if (previous != null && previous != identity) {
|
||||
dirtyUnlockedLayoutRoots.add(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void markDirtyIfClockTimeChanged(View root, SbtSettings settings, boolean statusBarRoot) {
|
||||
if (!statusBarRoot || root == null || settings == null || !settings.clockEnabledUnlocked) {
|
||||
return;
|
||||
}
|
||||
long bucket = clockTimeBucket(settings);
|
||||
Long previous = clockTimeBucketByRoot.put(root, bucket);
|
||||
if (previous != null && previous != bucket) {
|
||||
scheduleUnlockedClockTextRefreshForRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
private long clockTimeBucket(SbtSettings settings) {
|
||||
if (settings == null
|
||||
|| (!settings.clockShowSeconds
|
||||
&& !settings.clockShowDatePrefix
|
||||
&& !settings.clockCustomFormatEnabled)) {
|
||||
return 0L;
|
||||
}
|
||||
String pattern = settings.clockCustomFormat != null ? settings.clockCustomFormat : "";
|
||||
boolean seconds = settings.clockShowSeconds
|
||||
|| (settings.clockCustomFormatEnabled && pattern.contains("s"));
|
||||
long divisor = seconds ? 1_000L : 60_000L;
|
||||
return System.currentTimeMillis() / divisor;
|
||||
}
|
||||
|
||||
private void scheduleUnlockedAppearanceRefreshForAllRoots() {
|
||||
for (View root : new ArrayList<>(unlockedHostsByRoot.keySet())) {
|
||||
scheduleUnlockedAppearanceRefreshForRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleUnlockedAppearanceRefreshForRoot(View root) {
|
||||
if (root == null
|
||||
|| !root.isAttachedToWindow()
|
||||
|| !isUnlockedStatusBarRoot(root)
|
||||
|| !pendingUnlockedAppearanceRefreshRoots.add(root)) {
|
||||
return;
|
||||
}
|
||||
root.post(() -> {
|
||||
pendingUnlockedAppearanceRefreshRoots.remove(root);
|
||||
refreshUnlockedAppearanceNow(root);
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleUnlockedClockTextRefreshForRoot(View root) {
|
||||
if (root == null
|
||||
|| !root.isAttachedToWindow()
|
||||
|| !isUnlockedStatusBarRoot(root)
|
||||
|| !pendingUnlockedAppearanceRefreshRoots.add(root)) {
|
||||
return;
|
||||
}
|
||||
root.post(() -> {
|
||||
pendingUnlockedAppearanceRefreshRoots.remove(root);
|
||||
refreshUnlockedClockTextNow(root);
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshUnlockedAppearanceNow(View root) {
|
||||
if (root == null || !root.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
if (!settings.clockEnabled) {
|
||||
return;
|
||||
}
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
if (activeScene == null || !activeScene.isUnlocked()) {
|
||||
return;
|
||||
}
|
||||
UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root);
|
||||
if (unlockedHosts == null || !areUnlockedHostsAttached(root, unlockedHosts)) {
|
||||
return;
|
||||
}
|
||||
unlockedIconRenderController.refreshUnlockedAppearance(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
unlockedHosts.carrierHost,
|
||||
unlockedHosts.chipHost,
|
||||
unlockedHosts.statusHost,
|
||||
unlockedHosts.notificationHost,
|
||||
activeScene);
|
||||
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||
}
|
||||
|
||||
private View unlockedStatusBarRootFor(View view) {
|
||||
View current = view;
|
||||
while (current != null) {
|
||||
if (isUnlockedStatusBarRoot(current) && unlockedHostsByRoot.containsKey(current)) {
|
||||
return current;
|
||||
}
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyToRoot(View root) {
|
||||
applyToRootInternal(root);
|
||||
}
|
||||
|
||||
private void refreshUnlockedClockTextNow(View root) {
|
||||
if (root == null || !root.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
if (!settings.clockEnabled || !settings.clockEnabledUnlocked) {
|
||||
return;
|
||||
}
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
if (activeScene == null || !activeScene.isUnlocked()) {
|
||||
return;
|
||||
}
|
||||
UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root);
|
||||
if (unlockedHosts == null || !areUnlockedHostsAttached(root, unlockedHosts)) {
|
||||
return;
|
||||
}
|
||||
if (unlockedIconRenderController.refreshUnlockedClockText(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
activeScene)) {
|
||||
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyToRootInternal(View root) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
@@ -222,14 +595,12 @@ final class StockLayoutCanvasController {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
boolean layoutEnabled = settings.clockEnabled;
|
||||
boolean statusBarRoot = isUnlockedStatusBarRoot(root);
|
||||
markDirtyIfSettingsChanged(root, settings, statusBarRoot);
|
||||
markDirtyIfClockTimeChanged(root, settings, statusBarRoot);
|
||||
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
|
||||
if (!layoutEnabled) {
|
||||
if (statusBarRoot) {
|
||||
applyDebugOverlayIfNeeded(root, false, settings, false);
|
||||
} else {
|
||||
removeDebugOverlay(root);
|
||||
}
|
||||
cleanupLayoutOffRoot(root);
|
||||
updateActiveScene(root, context);
|
||||
applyLayoutOffRoot(root, settings, statusBarRoot, runtimeContext.getModeStateRepository().getActiveScene());
|
||||
return;
|
||||
}
|
||||
layoutOffCleanedRoots.remove(root);
|
||||
@@ -238,7 +609,10 @@ final class StockLayoutCanvasController {
|
||||
boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
|
||||
if (!statusBarRoot) {
|
||||
if (!unlockedScene && canContainLockedStatusBarIconSurfaces(root)) {
|
||||
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
|
||||
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
||||
} else {
|
||||
removeLockscreenCardsNotificationHost(root);
|
||||
}
|
||||
removeDebugOverlay(root);
|
||||
return;
|
||||
@@ -254,20 +628,25 @@ final class StockLayoutCanvasController {
|
||||
if (!unlockedScene) {
|
||||
restoreTrackedLockedStatusBarIconSurfaces(root);
|
||||
}
|
||||
markRootGeometryBeforeDraw(root);
|
||||
renderResult = unlockedIconRenderController.renderUnlocked(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
unlockedHosts.carrierHost,
|
||||
unlockedHosts.chipHost,
|
||||
unlockedHosts.statusHost,
|
||||
unlockedHosts.notificationHost,
|
||||
activeScene,
|
||||
lockedCarrierTextSource());
|
||||
boolean shouldShowUnlockedHosts = unlockedScene || renderResult.shouldOwnLockscreen();
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts);
|
||||
if (shouldShowUnlockedHosts) {
|
||||
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||
boolean shouldRender = !unlockedIconRenderController.hasLayoutPlan(root)
|
||||
|| dirtyUnlockedLayoutRoots.remove(root)
|
||||
|| !unlockedScene;
|
||||
if (shouldRender) {
|
||||
markRootGeometryBeforeDraw(root);
|
||||
renderResult = unlockedIconRenderController.renderUnlocked(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
unlockedHosts.carrierHost,
|
||||
unlockedHosts.chipHost,
|
||||
unlockedHosts.statusHost,
|
||||
unlockedHosts.notificationHost,
|
||||
activeScene,
|
||||
lockedCarrierTextSource());
|
||||
boolean shouldShowUnlockedHosts = unlockedScene || renderResult.shouldOwnLockscreen();
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts);
|
||||
if (shouldShowUnlockedHosts) {
|
||||
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!unlockedScene) {
|
||||
@@ -306,6 +685,40 @@ final class StockLayoutCanvasController {
|
||||
});
|
||||
}
|
||||
|
||||
private void applyLayoutOffRoot(
|
||||
View root,
|
||||
SbtSettings settings,
|
||||
boolean statusBarRoot,
|
||||
SceneKey activeScene
|
||||
) {
|
||||
if (statusBarRoot) {
|
||||
applyDebugOverlayIfNeeded(root, false, settings, false);
|
||||
} else {
|
||||
removeDebugOverlay(root);
|
||||
}
|
||||
cleanupLayoutOffRoot(root, activeScene);
|
||||
suppressLayoutOffShadeSurfaces(root, statusBarRoot, activeScene);
|
||||
}
|
||||
|
||||
private void suppressLayoutOffShadeSurfaces(
|
||||
View root,
|
||||
boolean statusBarRoot,
|
||||
SceneKey activeScene
|
||||
) {
|
||||
if (statusBarRoot
|
||||
|| root == null
|
||||
|| activeScene == null
|
||||
|| !canContainLockedStatusBarIconSurfaces(root)) {
|
||||
return;
|
||||
}
|
||||
boolean shadeSurfaceShouldStayHidden = activeScene.isUnlocked()
|
||||
|| activeScene.isLockscreen()
|
||||
&& runtimeContext.getModeStateRepository().isSystemUiShadeLocked();
|
||||
if (shadeSurfaceShouldStayHidden) {
|
||||
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDebugOverlay(
|
||||
View root,
|
||||
boolean layoutEnabled,
|
||||
@@ -442,7 +855,7 @@ final class StockLayoutCanvasController {
|
||||
if (isLockedStatusBarIconSurface(view)) {
|
||||
rememberLockedCarrierTextSource(view);
|
||||
trackedLockedStatusBarIconSurfaces.add(view);
|
||||
hideView(view, isCarrierView(view));
|
||||
hideView(view, keepLockedSurfaceLaidOut(view));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -467,7 +880,7 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
if (root == null || isDescendantOf(view, root)) {
|
||||
foundInRoot = true;
|
||||
hideView(view, isCarrierView(view));
|
||||
hideView(view, keepLockedSurfaceLaidOut(view));
|
||||
}
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
@@ -531,7 +944,7 @@ final class StockLayoutCanvasController {
|
||||
String viewClassName = view.getClass().getName();
|
||||
boolean carrierSurface = idName.toLowerCase(java.util.Locale.ROOT).contains("carrier")
|
||||
|| viewClassName.toLowerCase(java.util.Locale.ROOT).contains("carrier");
|
||||
boolean shelfSurface = viewClassName.contains("NotificationShelf");
|
||||
boolean shelfSurface = isCardsNotificationIconSurface(view);
|
||||
boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName)
|
||||
|| "notification_icononly_area".equals(idName)
|
||||
|| "keyguard_icononly_container_view".equals(idName)
|
||||
@@ -573,6 +986,54 @@ final class StockLayoutCanvasController {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean keepLockedSurfaceLaidOut(View view) {
|
||||
return isCarrierView(view) || isCardsNotificationIconSurface(view);
|
||||
}
|
||||
|
||||
private boolean isCardsNotificationIconSurface(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
String idName = resolveIdName(view);
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("SecShelfNotificationIconContainer")
|
||||
|| className.contains("NotificationShelfBackground")
|
||||
|| "notification_icononly_container".equals(idName)
|
||||
|| "notification_icononly_area".equals(idName)
|
||||
|| "keyguard_icononly_container_view".equals(idName)
|
||||
|| "common_notification_widget".equals(idName);
|
||||
}
|
||||
|
||||
private boolean isCardsNotificationShelfSurface(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("SecShelfNotificationIconContainer")
|
||||
|| className.contains("NotificationShelfBackground");
|
||||
}
|
||||
|
||||
private boolean isCardsNotificationIconSourceContainer(View view) {
|
||||
return view != null && view.getClass().getName().contains("SecShelfNotificationIconContainer");
|
||||
}
|
||||
|
||||
private Object fallbackStatusBarIcon(Object entryObj) {
|
||||
Object icons = ReflectionSupport.getFieldValue(entryObj, "mIcons");
|
||||
return icons != null ? ReflectionSupport.getFieldValue(icons, "mStatusBarIcon") : null;
|
||||
}
|
||||
|
||||
private boolean isStockOverflowDotView(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
return idName.contains("overflow")
|
||||
|| idName.contains("dot")
|
||||
|| className.contains("overflow")
|
||||
|| className.contains("dot");
|
||||
}
|
||||
|
||||
private boolean isCarrierView(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
@@ -703,6 +1164,10 @@ final class StockLayoutCanvasController {
|
||||
unlockedIconRenderController.clearAll();
|
||||
ownedIconHostManager.removeUnlockedHosts(root);
|
||||
unlockedHostsByRoot.remove(root);
|
||||
pendingUnlockedAppearanceRefreshRoots.remove(root);
|
||||
dirtyUnlockedLayoutRoots.remove(root);
|
||||
settingsIdentityByRoot.remove(root);
|
||||
clockTimeBucketByRoot.remove(root);
|
||||
removeDebugOverlay(root);
|
||||
root.requestLayout();
|
||||
root.invalidate();
|
||||
@@ -807,28 +1272,74 @@ final class StockLayoutCanvasController {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void releaseHiddenViews() {
|
||||
private boolean releaseHiddenViews(SceneKey activeScene) {
|
||||
boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
|
||||
boolean steadyLockscreenScene = activeScene != null
|
||||
&& activeScene.isLockscreen()
|
||||
&& !runtimeContext.getModeStateRepository().isSystemUiShadeLocked();
|
||||
boolean lockscreenShadeScene = activeScene != null
|
||||
&& activeScene.isLockscreen()
|
||||
&& runtimeContext.getModeStateRepository().isSystemUiShadeLocked();
|
||||
boolean deferredLockedShadeSurfaces = false;
|
||||
ArrayDeque<View> views = new ArrayDeque<>(hiddenViewStates.keySet());
|
||||
while (!views.isEmpty()) {
|
||||
View view = views.removeFirst();
|
||||
hiddenViewStates.remove(view);
|
||||
view.setAlpha(1f);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
boolean wasUnlockedStockSurface = trackedUnlockedStockSurfaces.contains(view);
|
||||
boolean wasLockedShadeSurface = trackedLockedStatusBarIconSurfaces.contains(view)
|
||||
&& isDescendantOfNotificationShadeWindow(view);
|
||||
if (unlockedScene && wasLockedShadeSurface) {
|
||||
deferredLockedShadeSurfaces = true;
|
||||
view.setAlpha(0f);
|
||||
view.setVisibility(View.GONE);
|
||||
requestViewAndParentLayout(view);
|
||||
continue;
|
||||
}
|
||||
if (lockscreenShadeScene && wasLockedShadeSurface) {
|
||||
deferredLockedShadeSurfaces = true;
|
||||
view.setAlpha(0f);
|
||||
view.setVisibility(View.GONE);
|
||||
requestViewAndParentLayout(view);
|
||||
continue;
|
||||
}
|
||||
if (steadyLockscreenScene
|
||||
&& wasLockedShadeSurface
|
||||
&& !isCardsNotificationShelfSurface(view)) {
|
||||
deferredLockedShadeSurfaces = true;
|
||||
view.setAlpha(0f);
|
||||
view.setVisibility(View.GONE);
|
||||
requestViewAndParentLayout(view);
|
||||
continue;
|
||||
}
|
||||
restoreView(view);
|
||||
if ((unlockedScene && wasUnlockedStockSurface)
|
||||
|| (steadyLockscreenScene && isCardsNotificationShelfSurface(view))) {
|
||||
forceViewVisible(view);
|
||||
}
|
||||
}
|
||||
hiddenViewStates.clear();
|
||||
trackedStatusChipSources.clear();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
if (!deferredLockedShadeSurfaces) {
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
}
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
return deferredLockedShadeSurfaces;
|
||||
}
|
||||
|
||||
private void cleanupLayoutOffRoot(View root) {
|
||||
private void cleanupLayoutOffRoot(View root, SceneKey activeScene) {
|
||||
boolean deferredLockedShadeSurfaces = false;
|
||||
if (!hiddenViewStates.isEmpty()) {
|
||||
releaseHiddenViews();
|
||||
deferredLockedShadeSurfaces = releaseHiddenViews(activeScene);
|
||||
}
|
||||
trackedStatusChipSources.clear();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
if (!deferredLockedShadeSurfaces) {
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
}
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
pendingUnlockedAppearanceRefreshRoots.remove(root);
|
||||
dirtyUnlockedLayoutRoots.remove(root);
|
||||
settingsIdentityByRoot.remove(root);
|
||||
clockTimeBucketByRoot.remove(root);
|
||||
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
|
||||
removeLockscreenCardsNotificationHost(root);
|
||||
return;
|
||||
}
|
||||
rootGeometries.remove(root);
|
||||
@@ -836,6 +1347,40 @@ final class StockLayoutCanvasController {
|
||||
ownedIconHostManager.removeUnlockedHosts(root);
|
||||
}
|
||||
|
||||
private void forceViewVisible(View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setAlpha(1f);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
requestViewAndParentLayout(view);
|
||||
}
|
||||
|
||||
private void requestViewAndParentLayout(View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.requestLayout();
|
||||
view.invalidate();
|
||||
Object parent = view.getParent();
|
||||
if (parent instanceof View parentView) {
|
||||
parentView.requestLayout();
|
||||
parentView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDescendantOfNotificationShadeWindow(View view) {
|
||||
View current = view;
|
||||
while (current != null) {
|
||||
if (current.getClass().getName().contains("NotificationShadeWindowView")) {
|
||||
return true;
|
||||
}
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDescendantOf(View child, View ancestor) {
|
||||
if (child == null || ancestor == null) {
|
||||
return false;
|
||||
@@ -871,6 +1416,97 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLockscreenCardsNotificationStripIfNeeded(
|
||||
View root,
|
||||
SbtSettings settings,
|
||||
SceneKey activeScene
|
||||
) {
|
||||
View visibleBouncerSurface = findVisibleBouncerSurface(root);
|
||||
boolean suppressForTransition = runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
|
||||
|| visibleBouncerSurface != null;
|
||||
if (root == null
|
||||
|| settings == null
|
||||
|| activeScene == null
|
||||
|| !activeScene.isLockscreen()
|
||||
|| activeScene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS
|
||||
|| suppressForTransition) {
|
||||
removeLockscreenCardsNotificationHost(root);
|
||||
return;
|
||||
}
|
||||
View host = ownedIconHostManager.ensureLockscreenCardsNotificationHost(root);
|
||||
if (host instanceof ViewGroup hostGroup) {
|
||||
lockscreenCardsNotificationStripRenderer.render(root, hostGroup, settings, activeScene);
|
||||
host.bringToFront();
|
||||
}
|
||||
}
|
||||
|
||||
private View findVisibleBouncerSurface(View root) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (view != root && isVisibleBouncerSurface(view)) {
|
||||
return view;
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isVisibleBouncerSurface(View view) {
|
||||
if (view == null || view.getVisibility() != View.VISIBLE || view.getAlpha() <= 0f) {
|
||||
return false;
|
||||
}
|
||||
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
|
||||
return false;
|
||||
}
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
return idName.contains("bouncer")
|
||||
|| idName.contains("keyguard_security")
|
||||
|| idName.contains("keyguard_pin")
|
||||
|| idName.contains("keyguard_password")
|
||||
|| idName.contains("keyguard_pattern")
|
||||
|| className.contains("bouncer")
|
||||
|| className.contains("keyguardsecurity")
|
||||
|| className.contains("keyguardpin")
|
||||
|| className.contains("keyguardpassword")
|
||||
|| className.contains("keyguardpattern");
|
||||
}
|
||||
|
||||
private void removeLockscreenCardsNotificationHost(View root) {
|
||||
View host = root instanceof ViewGroup group
|
||||
? findDirectOwnedHost(group, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST)
|
||||
: null;
|
||||
if (host instanceof ViewGroup hostGroup) {
|
||||
lockscreenCardsNotificationStripRenderer.clear(hostGroup);
|
||||
}
|
||||
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
|
||||
}
|
||||
|
||||
private View findDirectOwnedHost(ViewGroup root, String tag) {
|
||||
if (root == null || tag == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < root.getChildCount(); i++) {
|
||||
View child = root.getChildAt(i);
|
||||
if (tag.equals(child.getTag())) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private UnlockedHosts updateUnlockedOwnedHosts(
|
||||
View root,
|
||||
SceneKey activeScene
|
||||
@@ -903,6 +1539,10 @@ final class StockLayoutCanvasController {
|
||||
unlockedIconRenderController.clearAll();
|
||||
ownedIconHostManager.removeUnlockedHosts(root);
|
||||
unlockedHostsByRoot.remove(root);
|
||||
pendingUnlockedAppearanceRefreshRoots.remove(root);
|
||||
dirtyUnlockedLayoutRoots.remove(root);
|
||||
settingsIdentityByRoot.remove(root);
|
||||
clockTimeBucketByRoot.remove(root);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -932,12 +1572,14 @@ final class StockLayoutCanvasController {
|
||||
if (runtimeContext.getModeStateRepository().hasSystemUiStatusBarState()) {
|
||||
return;
|
||||
}
|
||||
NotificationDisplayStyle notificationStyle =
|
||||
SystemNotificationDisplayStyleDetector.read(context);
|
||||
if (isAodRoot(root) || (isKeyguardLocked(context) && !isInteractive(context))) {
|
||||
runtimeContext.getModeStateRepository().setAod(NotificationDisplayStyle.UNKNOWN);
|
||||
runtimeContext.getModeStateRepository().setAod(notificationStyle);
|
||||
return;
|
||||
}
|
||||
if (isKeyguardLocked(context)) {
|
||||
runtimeContext.getModeStateRepository().setLockscreen(NotificationDisplayStyle.UNKNOWN);
|
||||
runtimeContext.getModeStateRepository().setLockscreen(notificationStyle);
|
||||
return;
|
||||
}
|
||||
runtimeContext.getModeStateRepository().setUnlocked();
|
||||
|
||||
@@ -16,6 +16,7 @@ public final class ModeStateRepository {
|
||||
|
||||
private SceneKey activeScene = SceneKey.unlocked();
|
||||
private boolean systemUiStatusBarStateKnown;
|
||||
private int systemUiStatusBarState;
|
||||
private boolean layoutEnabled;
|
||||
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
|
||||
|
||||
@@ -27,6 +28,10 @@ public final class ModeStateRepository {
|
||||
return systemUiStatusBarStateKnown;
|
||||
}
|
||||
|
||||
public synchronized boolean isSystemUiShadeLocked() {
|
||||
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
|
||||
}
|
||||
|
||||
public synchronized boolean isLayoutEnabled() {
|
||||
return layoutEnabled;
|
||||
}
|
||||
@@ -67,14 +72,23 @@ public final class ModeStateRepository {
|
||||
activeScene = Objects.requireNonNull(sceneKey, "sceneKey");
|
||||
}
|
||||
|
||||
public synchronized void setSystemUiStatusBarState(int state, boolean dozing) {
|
||||
public synchronized void setSystemUiStatusBarState(
|
||||
int state,
|
||||
boolean dozing,
|
||||
NotificationDisplayStyle notificationDisplayStyle
|
||||
) {
|
||||
systemUiStatusBarStateKnown = true;
|
||||
systemUiStatusBarState = state;
|
||||
NotificationDisplayStyle style = notificationDisplayStyle;
|
||||
if (style == null || style == NotificationDisplayStyle.NONE) {
|
||||
style = NotificationDisplayStyle.UNKNOWN;
|
||||
}
|
||||
if (dozing) {
|
||||
activeScene = SceneKey.aod(NotificationDisplayStyle.UNKNOWN);
|
||||
activeScene = SceneKey.aod(style);
|
||||
} else if (state == 0) {
|
||||
activeScene = SceneKey.unlocked();
|
||||
} else {
|
||||
activeScene = SceneKey.lockscreen(NotificationDisplayStyle.UNKNOWN);
|
||||
activeScene = SceneKey.lockscreen(style);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
|
||||
/**
|
||||
* Reads Samsung's lockscreen/AOD notification display style from the system
|
||||
* setting SystemUI itself uses for Cards / Icons / Dot.
|
||||
*/
|
||||
public final class SystemNotificationDisplayStyleDetector {
|
||||
private static final String KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION =
|
||||
"lockscreen_minimizing_notification";
|
||||
|
||||
private SystemNotificationDisplayStyleDetector() {
|
||||
}
|
||||
|
||||
public static NotificationDisplayStyle read(Context context) {
|
||||
ContentResolver resolver = context != null ? context.getContentResolver() : null;
|
||||
if (resolver == null) {
|
||||
return NotificationDisplayStyle.UNKNOWN;
|
||||
}
|
||||
try {
|
||||
return styleForMinimizingValue(
|
||||
Settings.System.getInt(resolver, KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION));
|
||||
} catch (Throwable ignored) {
|
||||
return NotificationDisplayStyle.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static NotificationDisplayStyle styleForMinimizingValue(int value) {
|
||||
return switch (value) {
|
||||
case 0 -> NotificationDisplayStyle.CARDS;
|
||||
case 1 -> NotificationDisplayStyle.ICONS;
|
||||
case 2 -> NotificationDisplayStyle.DOT;
|
||||
default -> NotificationDisplayStyle.UNKNOWN;
|
||||
};
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -1,5 +1,7 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
@@ -65,6 +67,26 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
||||
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
|
||||
int state = rawState instanceof Integer integer ? integer : 0;
|
||||
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
|
||||
context.getModeStateRepository().setSystemUiStatusBarState(state, dozing);
|
||||
context.getModeStateRepository().setSystemUiStatusBarState(
|
||||
state,
|
||||
dozing,
|
||||
SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
|
||||
}
|
||||
|
||||
private Context systemContext(Object controller) {
|
||||
Object context = ReflectionSupport.getFieldValue(controller, "mContext");
|
||||
if (context instanceof Context androidContext) {
|
||||
return androidContext;
|
||||
}
|
||||
try {
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Object app = ReflectionSupport.invokeStaticMethod(
|
||||
activityThreadClass,
|
||||
"currentApplication",
|
||||
new Class<?>[0]);
|
||||
return app instanceof Context androidContext ? androidContext : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
|
||||
public final class LockscreenCardsNotificationIconSourceStore {
|
||||
private static final WeakHashMap<ViewGroup, ArrayList<SourceEntry>> SOURCES_BY_CONTAINER =
|
||||
new WeakHashMap<>();
|
||||
|
||||
private LockscreenCardsNotificationIconSourceStore() {
|
||||
}
|
||||
|
||||
public static void put(ViewGroup container, ArrayList<SourceEntry> sources) {
|
||||
if (container == null || sources == null || sources.isEmpty()) {
|
||||
if (container != null) {
|
||||
SOURCES_BY_CONTAINER.remove(container);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ArrayList<SourceEntry> attached = new ArrayList<>();
|
||||
Set<View> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (SourceEntry source : sources) {
|
||||
if (source == null || source.view == null || !seen.add(source.view)) {
|
||||
continue;
|
||||
}
|
||||
attached.add(source);
|
||||
}
|
||||
if (attached.isEmpty()) {
|
||||
SOURCES_BY_CONTAINER.remove(container);
|
||||
} else {
|
||||
SOURCES_BY_CONTAINER.put(container, attached);
|
||||
}
|
||||
}
|
||||
|
||||
static ArrayList<SnapshotSource> snapshotSources(ViewGroup container, View root) {
|
||||
ArrayList<SourceEntry> views = SOURCES_BY_CONTAINER.get(container);
|
||||
ArrayList<SnapshotSource> result = new ArrayList<>();
|
||||
if (views == null || views.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
int skipCards = visibleShelfStartIndex(container, views);
|
||||
for (int i = 0; i < views.size(); i++) {
|
||||
if (i < skipCards) {
|
||||
continue;
|
||||
}
|
||||
SourceEntry entry = views.get(i);
|
||||
View view = entry.view;
|
||||
if (view == null || !view.isAttachedToWindow()) {
|
||||
continue;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
continue;
|
||||
}
|
||||
result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int visibleShelfStartIndex(ViewGroup container, ArrayList<SourceEntry> entries) {
|
||||
if (container == null || entries == null || entries.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
ArrayList<View> visibleShelfIcons = visibleShelfIcons(container);
|
||||
for (View icon : visibleShelfIcons) {
|
||||
String key = keyForIconView(icon);
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
SourceEntry entry = entries.get(i);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
if (entry.view == icon) {
|
||||
return i;
|
||||
}
|
||||
if (key != null && entry.key != null && key.equals(entry.key)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ArrayList<View> visibleShelfIcons(ViewGroup container) {
|
||||
ArrayList<View> result = new ArrayList<>();
|
||||
ArrayList<View> stack = new ArrayList<>();
|
||||
stack.add(container);
|
||||
for (int i = 0; i < stack.size(); i++) {
|
||||
View view = stack.get(i);
|
||||
if (view != container && isVisibleShelfIcon(view)) {
|
||||
result.add(view);
|
||||
continue;
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int child = 0; child < group.getChildCount(); child++) {
|
||||
View childView = group.getChildAt(child);
|
||||
if (childView != null) {
|
||||
stack.add(childView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isVisibleShelfIcon(View view) {
|
||||
if (view == null || view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
if (isOverflowDotCandidate(view)) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("StatusBarIconView")
|
||||
|| className.contains("IconView")
|
||||
|| view instanceof android.widget.ImageView;
|
||||
}
|
||||
|
||||
private static boolean isOverflowDotCandidate(View view) {
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
return idName.contains("overflow")
|
||||
|| idName.contains("dot")
|
||||
|| className.contains("overflow")
|
||||
|| className.contains("dot");
|
||||
}
|
||||
|
||||
public static SourceEntry source(View view, String key) {
|
||||
return new SourceEntry(view, key);
|
||||
}
|
||||
|
||||
public static String keyForEntry(Object entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
Object methodValue = ReflectionSupport.invokeMethod(entry, "getKey", new Class<?>[0]);
|
||||
if (methodValue instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
for (String fieldName : new String[]{"mKey", "key", "notificationKey", "mNotificationKey"}) {
|
||||
Object value = ReflectionSupport.getFieldValue(entry, fieldName);
|
||||
if (value instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String keyForIconView(View view) {
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
Object methodValue = ReflectionSupport.invokeMethod(view, "getNotificationKey", new Class<?>[0]);
|
||||
if (methodValue instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
for (String fieldName : new String[]{"mNotificationKey", "notificationKey", "mKey", "key"}) {
|
||||
Object value = ReflectionSupport.getFieldValue(view, fieldName);
|
||||
if (value instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int visibleCardCountBeforeShelf(View root, View shelf) {
|
||||
if (!(root instanceof ViewGroup group)) {
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private static boolean isNotificationRow(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("ExpandableNotificationRow");
|
||||
}
|
||||
|
||||
private static String resolveIdName(View view) {
|
||||
if (view == null || view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Throwable ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isVisibleCardRow(View view) {
|
||||
if (view == null || view.getVisibility() == View.GONE) {
|
||||
return false;
|
||||
}
|
||||
if (!view.isShown() || effectiveAlpha(view) <= 0.05f) {
|
||||
return false;
|
||||
}
|
||||
if (ReflectionSupport.getBooleanField(view, "mInShelf", false)
|
||||
|| ReflectionSupport.getBooleanField(view, "mTransformingInShelf", false)) {
|
||||
return false;
|
||||
}
|
||||
Object viewState = ReflectionSupport.getFieldValue(view, "mViewState");
|
||||
if (viewState != null
|
||||
&& (ReflectionSupport.getBooleanField(viewState, "hidden", false)
|
||||
|| ReflectionSupport.getBooleanField(viewState, "gone", false))) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
return width > 0 && height > 0 && actualHeight(view, height) > 0;
|
||||
}
|
||||
|
||||
private static int actualHeight(View view, int fallback) {
|
||||
Object methodValue = ReflectionSupport.invokeMethod(view, "getActualHeight", new Class<?>[0]);
|
||||
if (methodValue instanceof Integer integer && integer > 0) {
|
||||
return integer;
|
||||
}
|
||||
Object fieldValue = ReflectionSupport.getFieldValue(view, "mActualHeight");
|
||||
if (fieldValue instanceof Integer integer && integer > 0) {
|
||||
return integer;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static float effectiveAlpha(View view) {
|
||||
float alpha = 1f;
|
||||
Object current = view;
|
||||
while (current instanceof View currentView) {
|
||||
alpha *= currentView.getAlpha();
|
||||
current = currentView.getParent();
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
public static final class SourceEntry {
|
||||
final View view;
|
||||
final String key;
|
||||
|
||||
private SourceEntry(View view, String key) {
|
||||
this.view = view;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
/**
|
||||
* Mirrors Samsung's cards-mode shelf/icon-only notification strip into an owned
|
||||
* host. The stock shelf already represents notifications not currently shown as
|
||||
* cards, so this renderer treats it as the source list and only owns placement.
|
||||
*/
|
||||
public final class LockscreenCardsNotificationStripRenderer {
|
||||
private static final int DEFAULT_PILL_COLOR = 0xAA202124;
|
||||
private static final String PILL_BACKGROUND_TAG = "statusbartweak.cards.notification.pill";
|
||||
|
||||
private final SnapshotRenderer renderer = new SnapshotRenderer(false, false, false);
|
||||
private final WeakHashMap<ViewGroup, ArrayList<View>> pillViewsByHost = new WeakHashMap<>();
|
||||
|
||||
public RenderResult render(
|
||||
View root,
|
||||
ViewGroup host,
|
||||
SbtSettings settings,
|
||||
SceneKey scene
|
||||
) {
|
||||
if (root == null
|
||||
|| host == null
|
||||
|| settings == null
|
||||
|| scene == null
|
||||
|| !scene.isLockscreen()
|
||||
|| scene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS
|
||||
|| settings.cardsLockMaxRows <= 0) {
|
||||
clear(host);
|
||||
return RenderResult.empty();
|
||||
}
|
||||
ViewGroup sourceRoot = findSourceRoot(root);
|
||||
if (sourceRoot == null) {
|
||||
clear(host);
|
||||
return RenderResult.empty();
|
||||
}
|
||||
if (hasHiddenShelfAncestor(sourceRoot, root)) {
|
||||
clear(host);
|
||||
return RenderResult.empty();
|
||||
}
|
||||
syncHostAnimation(host, sourceRoot);
|
||||
ArrayList<SnapshotSource> sources = LockscreenCardsNotificationIconSourceStore.snapshotSources(sourceRoot, root);
|
||||
if (sources.isEmpty()) {
|
||||
sources = collectSources(sourceRoot);
|
||||
}
|
||||
if (sources.isEmpty()) {
|
||||
clear(host);
|
||||
return RenderResult.empty();
|
||||
}
|
||||
ArrayList<SnapshotPlacement> placements = SnapshotIconFilter.notificationPlacements(
|
||||
renderer.rawPlacements(root, sources),
|
||||
settings,
|
||||
scene);
|
||||
StripLayout layout = layoutPlacements(root, sourceRoot, placements, settings);
|
||||
renderPills(host, layout.rowBounds, resolvePillColor(sourceRoot));
|
||||
Bounds renderedBounds = renderer.renderPlacements(host, layout.placements);
|
||||
return new RenderResult(sourceRoot, renderedBounds);
|
||||
}
|
||||
|
||||
public void clear(ViewGroup host) {
|
||||
renderer.clear(host);
|
||||
clearPills(host);
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
renderer.clearAll();
|
||||
}
|
||||
|
||||
private ViewGroup findSourceRoot(View root) {
|
||||
if (!(root instanceof ViewGroup group)) {
|
||||
return null;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(group);
|
||||
ViewGroup shelf = null;
|
||||
ViewGroup iconOnly = null;
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (view instanceof ViewGroup candidate) {
|
||||
if (isShelfIconContainer(candidate)) {
|
||||
shelf = candidate;
|
||||
} else if (isIconOnlyContainer(candidate) && iconOnly == null) {
|
||||
iconOnly = candidate;
|
||||
}
|
||||
for (int i = 0; i < candidate.getChildCount(); i++) {
|
||||
View child = candidate.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return shelf != null ? shelf : iconOnly;
|
||||
}
|
||||
|
||||
private boolean hasHiddenShelfAncestor(View sourceRoot, View root) {
|
||||
Object current = sourceRoot != null ? sourceRoot.getParent() : null;
|
||||
while (current instanceof View currentView) {
|
||||
if (currentView == root) {
|
||||
return false;
|
||||
}
|
||||
if (currentView.getClass().getName().contains("NotificationShelf")) {
|
||||
return currentView.getVisibility() != View.VISIBLE || currentView.getAlpha() <= 0f;
|
||||
}
|
||||
current = currentView.getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void syncHostAnimation(ViewGroup host, View sourceRoot) {
|
||||
if (host == null || sourceRoot == null) {
|
||||
return;
|
||||
}
|
||||
float alpha = effectiveAncestorAlpha(sourceRoot);
|
||||
if (host.getAlpha() != alpha) {
|
||||
host.setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private float effectiveAncestorAlpha(View view) {
|
||||
float alpha = 1f;
|
||||
Object current = view.getParent();
|
||||
while (current instanceof View currentView) {
|
||||
alpha *= currentView.getAlpha();
|
||||
current = currentView.getParent();
|
||||
}
|
||||
return Math.max(0f, Math.min(1f, alpha));
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotSource> collectSources(ViewGroup sourceRoot) {
|
||||
ArrayList<SnapshotSource> result = new ArrayList<>();
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(sourceRoot);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (view != sourceRoot && isNotificationIconCandidate(view)) {
|
||||
result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE));
|
||||
continue;
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private StripLayout layoutPlacements(
|
||||
View root,
|
||||
View sourceRoot,
|
||||
ArrayList<SnapshotPlacement> placements,
|
||||
SbtSettings settings
|
||||
) {
|
||||
ArrayList<SnapshotPlacement> result = new ArrayList<>();
|
||||
ArrayList<Bounds> rowBounds = new ArrayList<>();
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
return new StripLayout(result, rowBounds);
|
||||
}
|
||||
int maxPerRow = Math.max(1, settings.cardsLockMaxIconsPerRow);
|
||||
int maxRows = Math.max(0, settings.cardsLockMaxRows);
|
||||
int capacity = maxPerRow * maxRows;
|
||||
if (capacity <= 0) {
|
||||
return new StripLayout(result, rowBounds);
|
||||
}
|
||||
boolean showDot = placements.size() > capacity;
|
||||
int iconSlots = showDot ? Math.max(0, capacity - 1) : Math.min(capacity, placements.size());
|
||||
int visible = iconSlots + (showDot ? 1 : 0);
|
||||
int iconHeight = representativeHeight(placements);
|
||||
int iconWidth = representativeWidth(placements, iconHeight);
|
||||
Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, sourceRoot);
|
||||
int top = sourceBounds != null ? sourceBounds.top : placements.get(0).bounds.top;
|
||||
int centerX = sourceBounds != null
|
||||
? sourceBounds.left + sourceBounds.width / 2
|
||||
: root.getWidth() / 2;
|
||||
int availableWidth = settings.cardsLockLimitToWidth
|
||||
? Math.max(0, root.getWidth())
|
||||
: Integer.MAX_VALUE;
|
||||
int index = 0;
|
||||
int[] rowCounts = rowCounts(visible, maxPerRow, maxRows, settings.cardsLockEvenDistribution);
|
||||
int dotColor = dotColor(placements);
|
||||
int rowGap = dp(root, 8);
|
||||
for (int row = 0; row < rowCounts.length && index < visible; row++) {
|
||||
int count = rowCounts[row];
|
||||
if (count <= 0) {
|
||||
continue;
|
||||
}
|
||||
int rowWidth = count * iconWidth;
|
||||
int left = centerX - rowWidth / 2;
|
||||
if (availableWidth != Integer.MAX_VALUE) {
|
||||
left = Math.max(0, Math.min(left, Math.max(0, availableWidth - rowWidth)));
|
||||
}
|
||||
int y = top + row * (iconHeight + rowGap);
|
||||
rowBounds.add(new Bounds(left, y, rowWidth, iconHeight));
|
||||
for (int col = 0; col < count && index < visible; col++, index++) {
|
||||
boolean dotSlot = showDot && index == visible - 1;
|
||||
SnapshotPlacement placement = dotSlot
|
||||
? dotPlacement(iconWidth, iconHeight, dotColor)
|
||||
: placements.get(index);
|
||||
result.add(new SnapshotPlacement(
|
||||
placement.source,
|
||||
new Bounds(left + col * iconWidth, y, iconWidth, iconHeight),
|
||||
placement.key,
|
||||
placement.signal,
|
||||
placement.overflowDot,
|
||||
placement.overflowDotColor,
|
||||
placement.heldSignal,
|
||||
new Bounds(0, 0, iconWidth, iconHeight),
|
||||
0));
|
||||
}
|
||||
}
|
||||
return new StripLayout(result, rowBounds);
|
||||
}
|
||||
|
||||
private int[] rowCounts(
|
||||
int slots,
|
||||
int maxPerRow,
|
||||
int maxRows,
|
||||
boolean evenDistribution
|
||||
) {
|
||||
if (slots <= 0 || maxPerRow <= 0 || maxRows <= 0) {
|
||||
return new int[0];
|
||||
}
|
||||
int rowCount = Math.min(maxRows, (slots + maxPerRow - 1) / maxPerRow);
|
||||
int[] result = new int[rowCount];
|
||||
if (evenDistribution && rowCount > 1) {
|
||||
int base = slots / rowCount;
|
||||
int remainder = slots % rowCount;
|
||||
for (int row = 0; row < rowCount; row++) {
|
||||
result[row] = base + (row < remainder ? 1 : 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
int remaining = slots;
|
||||
for (int row = 0; row < rowCount; row++) {
|
||||
result[row] = Math.min(maxPerRow, remaining);
|
||||
remaining -= result[row];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private SnapshotPlacement dotPlacement(int width, int height, int color) {
|
||||
return new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(0, 0, Math.max(1, width), Math.max(1, height)),
|
||||
SnapshotKeys.OVERFLOW_DOT + ":cards_notifications",
|
||||
false,
|
||||
true,
|
||||
color,
|
||||
null,
|
||||
new Bounds(0, 0, Math.max(1, width), Math.max(1, height)),
|
||||
0);
|
||||
}
|
||||
|
||||
private int dotColor(ArrayList<SnapshotPlacement> placements) {
|
||||
if (placements != null) {
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (placement == null || placement.source == null) {
|
||||
continue;
|
||||
}
|
||||
int color = SnapshotAppearanceSignature.preferredColor(placement.source.view(), Color.TRANSPARENT);
|
||||
if (Color.alpha(color) != 0) {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
private void renderPills(ViewGroup host, ArrayList<Bounds> rowBounds, int color) {
|
||||
if (host == null || rowBounds == null || rowBounds.isEmpty()) {
|
||||
clearPills(host);
|
||||
return;
|
||||
}
|
||||
ArrayList<View> pills = pillViewsByHost.get(host);
|
||||
if (pills == null) {
|
||||
pills = new ArrayList<>();
|
||||
pillViewsByHost.put(host, pills);
|
||||
}
|
||||
while (pills.size() < rowBounds.size()) {
|
||||
View pill = new View(host.getContext());
|
||||
pill.setTag(PILL_BACKGROUND_TAG);
|
||||
pill.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
pills.add(pill);
|
||||
}
|
||||
int horizontalPadding = dp(host, 8);
|
||||
int verticalPadding = dp(host, 3);
|
||||
for (int i = 0; i < pills.size(); i++) {
|
||||
View pill = pills.get(i);
|
||||
if (i >= rowBounds.size()) {
|
||||
if (pill.getParent() == host) {
|
||||
host.removeView(pill);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Bounds row = rowBounds.get(i);
|
||||
int left = row.left - horizontalPadding;
|
||||
int top = row.top - verticalPadding;
|
||||
int width = Math.max(1, row.width + horizontalPadding * 2);
|
||||
int height = Math.max(1, row.height + verticalPadding * 2);
|
||||
GradientDrawable background = new GradientDrawable();
|
||||
background.setColor(color);
|
||||
background.setCornerRadius(height / 2f);
|
||||
pill.setBackground(background);
|
||||
if (pill.getParent() != host) {
|
||||
host.addView(pill, Math.min(i, host.getChildCount()));
|
||||
} else {
|
||||
host.removeView(pill);
|
||||
host.addView(pill, Math.min(i, host.getChildCount()));
|
||||
}
|
||||
if (applyPillLayoutParamsIfChanged(host, pill, left, top, width, height)) {
|
||||
host.requestLayout();
|
||||
}
|
||||
if (pill.getVisibility() != View.VISIBLE) {
|
||||
pill.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (pill.getAlpha() != 1f) {
|
||||
pill.setAlpha(1f);
|
||||
}
|
||||
if (pill.getLeft() != left
|
||||
|| pill.getTop() != top
|
||||
|| pill.getRight() != left + width
|
||||
|| pill.getBottom() != top + height) {
|
||||
pill.layout(left, top, left + width, top + height);
|
||||
}
|
||||
}
|
||||
host.invalidate();
|
||||
}
|
||||
|
||||
private void clearPills(ViewGroup host) {
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<View> pills = pillViewsByHost.remove(host);
|
||||
if (pills == null) {
|
||||
return;
|
||||
}
|
||||
for (View pill : pills) {
|
||||
if (pill != null && pill.getParent() == host) {
|
||||
host.removeView(pill);
|
||||
}
|
||||
}
|
||||
host.invalidate();
|
||||
}
|
||||
|
||||
private int resolvePillColor(View sourceRoot) {
|
||||
View backgroundSource = findShelfBackground(sourceRoot);
|
||||
int color = backgroundColor(backgroundSource);
|
||||
if (Color.alpha(color) != 0) {
|
||||
return color;
|
||||
}
|
||||
color = backgroundColor(sourceRoot);
|
||||
return Color.alpha(color) != 0 ? color : DEFAULT_PILL_COLOR;
|
||||
}
|
||||
|
||||
private View findShelfBackground(View root) {
|
||||
if (!(root instanceof ViewGroup group)) {
|
||||
return null;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(group);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
String className = view.getClass().getName();
|
||||
String idName = resolveIdName(view);
|
||||
if (className.contains("NotificationShelfBackground")
|
||||
|| idName.toLowerCase(java.util.Locale.ROOT).contains("background")) {
|
||||
return view;
|
||||
}
|
||||
if (view instanceof ViewGroup childGroup) {
|
||||
for (int i = 0; i < childGroup.getChildCount(); i++) {
|
||||
View child = childGroup.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int backgroundColor(View view) {
|
||||
if (view == null) {
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
Drawable background = view.getBackground();
|
||||
if (background instanceof ColorDrawable colorDrawable) {
|
||||
return colorDrawable.getColor();
|
||||
}
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
|
||||
private int dp(View view, int value) {
|
||||
float density = view.getResources().getDisplayMetrics().density;
|
||||
return Math.max(0, Math.round(value * density));
|
||||
}
|
||||
|
||||
private boolean applyPillLayoutParamsIfChanged(
|
||||
ViewGroup host,
|
||||
View pill,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
ViewGroup.LayoutParams current = pill.getLayoutParams();
|
||||
if (current != null
|
||||
&& current.width == width
|
||||
&& current.height == height
|
||||
&& current instanceof ViewGroup.MarginLayoutParams marginParams
|
||||
&& marginParams.leftMargin == left
|
||||
&& marginParams.topMargin == top) {
|
||||
return false;
|
||||
}
|
||||
pill.setLayoutParams(layoutParamsFor(host, left, top, width, height));
|
||||
return true;
|
||||
}
|
||||
|
||||
private ViewGroup.LayoutParams layoutParamsFor(
|
||||
ViewGroup host,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
if (host instanceof FrameLayout) {
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height);
|
||||
params.leftMargin = left;
|
||||
params.topMargin = top;
|
||||
return params;
|
||||
}
|
||||
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(width, height);
|
||||
params.leftMargin = left;
|
||||
params.topMargin = top;
|
||||
return params;
|
||||
}
|
||||
|
||||
private int representativeWidth(ArrayList<SnapshotPlacement> placements, int fallback) {
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (placement != null && placement.bounds != null && placement.bounds.width > 0) {
|
||||
return placement.bounds.width;
|
||||
}
|
||||
}
|
||||
return Math.max(1, fallback);
|
||||
}
|
||||
|
||||
private int representativeHeight(ArrayList<SnapshotPlacement> placements) {
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (placement != null && placement.bounds != null && placement.bounds.height > 0) {
|
||||
return placement.bounds.height;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private boolean isShelfIconContainer(View view) {
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("SecShelfNotificationIconContainer");
|
||||
}
|
||||
|
||||
private boolean isIconOnlyContainer(View view) {
|
||||
String idName = resolveIdName(view);
|
||||
return "keyguard_icononly_container_view".equals(idName)
|
||||
|| "notification_icononly_container".equals(idName)
|
||||
|| "notification_icononly_area".equals(idName)
|
||||
|| "common_notification_widget".equals(idName);
|
||||
}
|
||||
|
||||
private boolean isNotificationIconCandidate(View view) {
|
||||
if (view == null || view instanceof TextView || view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0 || isOverflowDotCandidate(view)) {
|
||||
return false;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("StatusBarIconView")
|
||||
|| className.contains("IconView")
|
||||
|| view instanceof ImageView;
|
||||
}
|
||||
|
||||
private boolean isOverflowDotCandidate(View view) {
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
return idName.contains("overflow")
|
||||
|| idName.contains("dot")
|
||||
|| className.contains("overflow")
|
||||
|| className.contains("dot");
|
||||
}
|
||||
|
||||
private String resolveIdName(View view) {
|
||||
if (view == null || view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Throwable ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public record RenderResult(View sourceRoot, Bounds renderedBounds) {
|
||||
static RenderResult empty() {
|
||||
return new RenderResult(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private record StripLayout(
|
||||
ArrayList<SnapshotPlacement> placements,
|
||||
ArrayList<Bounds> rowBounds
|
||||
) {
|
||||
}
|
||||
}
|
||||
+9
-13
@@ -24,6 +24,8 @@ public final class OwnedIconHostManager {
|
||||
"statusbartweak.unlocked.carrier.host";
|
||||
public static final String TAG_UNLOCKED_CHIP_HOST =
|
||||
"statusbartweak.unlocked.chip.host";
|
||||
public static final String TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST =
|
||||
"statusbartweak.lockscreen.cards.notification.host";
|
||||
|
||||
public FrameLayout ensureUnlockedStatusHost(View root) {
|
||||
return ensureHost(root, TAG_UNLOCKED_STATUS_HOST);
|
||||
@@ -45,6 +47,10 @@ public final class OwnedIconHostManager {
|
||||
return ensureHost(root, TAG_UNLOCKED_CARRIER_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureLockscreenCardsNotificationHost(View root) {
|
||||
return ensureHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureHost(View root, String tag) {
|
||||
Objects.requireNonNull(tag, "tag");
|
||||
if (!(root instanceof ViewGroup parent)) {
|
||||
@@ -64,7 +70,6 @@ public final class OwnedIconHostManager {
|
||||
parent.addView(host, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
disableAncestorClipping(parent);
|
||||
host.bringToFront();
|
||||
return host;
|
||||
}
|
||||
@@ -85,6 +90,7 @@ public final class OwnedIconHostManager {
|
||||
removeHost(root, TAG_UNLOCKED_CLOCK_HOST);
|
||||
removeHost(root, TAG_UNLOCKED_CARRIER_HOST);
|
||||
removeHost(root, TAG_UNLOCKED_CHIP_HOST);
|
||||
removeHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
|
||||
}
|
||||
|
||||
public void setUnlockedHostsVisible(View root, boolean visible) {
|
||||
@@ -112,7 +118,8 @@ public final class OwnedIconHostManager {
|
||||
|| TAG_UNLOCKED_NOTIFICATION_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_CLOCK_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_CARRIER_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_CHIP_HOST.equals(tag);
|
||||
|| TAG_UNLOCKED_CHIP_HOST.equals(tag)
|
||||
|| TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST.equals(tag);
|
||||
}
|
||||
|
||||
private FrameLayout findDirectHost(ViewGroup parent, String tag) {
|
||||
@@ -146,15 +153,4 @@ public final class OwnedIconHostManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void disableAncestorClipping(View view) {
|
||||
Object current = view;
|
||||
int depth = 0;
|
||||
while (current instanceof ViewGroup group && depth < 8) {
|
||||
group.setClipChildren(false);
|
||||
group.setClipToPadding(false);
|
||||
group.setClipToOutline(false);
|
||||
current = group.getParent();
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SystemIconRules;
|
||||
|
||||
final class SnapshotIconFilter {
|
||||
private SnapshotIconFilter() {
|
||||
}
|
||||
|
||||
static ArrayList<SnapshotPlacement> statusPlacements(
|
||||
ArrayList<SnapshotPlacement> placements,
|
||||
SbtSettings settings,
|
||||
SceneKey scene
|
||||
) {
|
||||
String mode = modeName(scene);
|
||||
if (placements == null
|
||||
|| placements.isEmpty()
|
||||
|| settings == null
|
||||
|| settings.systemIconBlockedModes.isEmpty()
|
||||
|| mode == null) {
|
||||
return placements != null ? placements : new ArrayList<>();
|
||||
}
|
||||
ArrayList<SnapshotPlacement> filtered = new ArrayList<>(placements.size());
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (!isStatusBlocked(placement, settings.systemIconBlockedModes, mode)) {
|
||||
filtered.add(placement);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
static ArrayList<SnapshotPlacement> notificationPlacements(
|
||||
ArrayList<SnapshotPlacement> placements,
|
||||
SbtSettings settings,
|
||||
SceneKey scene
|
||||
) {
|
||||
String mode = modeName(scene);
|
||||
if (placements == null
|
||||
|| placements.isEmpty()
|
||||
|| settings == null
|
||||
|| mode == null) {
|
||||
return placements != null ? placements : new ArrayList<>();
|
||||
}
|
||||
ArrayList<SnapshotPlacement> filtered = new ArrayList<>(placements.size());
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
String pkg = notificationPackage(placement != null ? placement.key : null);
|
||||
if (pkg == null
|
||||
|| !isNotificationBlocked(
|
||||
placement,
|
||||
pkg,
|
||||
settings.appIconBlockedModes,
|
||||
settings.appIconExceptionRules,
|
||||
mode)) {
|
||||
filtered.add(placement);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
static String notificationPackage(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;
|
||||
}
|
||||
|
||||
static boolean isNotificationBlocked(
|
||||
SnapshotPlacement placement,
|
||||
String pkg,
|
||||
Map<String, Integer> blockedModes,
|
||||
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptionRules,
|
||||
String mode
|
||||
) {
|
||||
if (!AppIconRules.isValidPackageName(pkg) || mode == null) {
|
||||
return false;
|
||||
}
|
||||
AppIconRules.ExceptionRule matchingRule = matchingExceptionRule(
|
||||
placement,
|
||||
exceptionRules != null ? exceptionRules.get(pkg) : null);
|
||||
if (matchingRule != null) {
|
||||
return matchingRule.blocksMode(mode);
|
||||
}
|
||||
return AppIconRules.isBlocked(blockedModes, pkg, mode);
|
||||
}
|
||||
|
||||
static boolean isStatusBlocked(
|
||||
SnapshotPlacement placement,
|
||||
Map<String, Integer> blockedModes,
|
||||
String mode
|
||||
) {
|
||||
if (placement == null || blockedModes == null || blockedModes.isEmpty() || mode == null) {
|
||||
return false;
|
||||
}
|
||||
String text = statusMatchText(placement);
|
||||
for (String slot : statusSlots(text)) {
|
||||
if (SystemIconRules.isBlocked(blockedModes, slot, mode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AppIconRules.ExceptionRule matchingExceptionRule(
|
||||
SnapshotPlacement placement,
|
||||
ArrayList<AppIconRules.ExceptionRule> rules
|
||||
) {
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String text = notificationMatchText(placement);
|
||||
for (AppIconRules.ExceptionRule rule : rules) {
|
||||
if (rule == null || rule.regex == null || rule.regex.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (Pattern.compile(rule.regex).matcher(text).find()) {
|
||||
return rule;
|
||||
}
|
||||
} catch (PatternSyntaxException ignored) {
|
||||
// Invalid user regexes are ignored instead of blocking all icons.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String notificationMatchText(SnapshotPlacement placement) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
append(sb, placement != null ? placement.key : null);
|
||||
View view = placement != null && placement.source != null ? placement.source.view() : null;
|
||||
appendViewText(sb, view);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String statusMatchText(SnapshotPlacement placement) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
append(sb, placement.key);
|
||||
View view = placement.source != null ? placement.source.view() : null;
|
||||
appendViewText(sb, view);
|
||||
return sb.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static void appendViewText(StringBuilder sb, View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
append(sb, view.getClass().getName());
|
||||
append(sb, resolveIdName(view));
|
||||
CharSequence contentDescription = view.getContentDescription();
|
||||
append(sb, contentDescription != null ? contentDescription.toString() : null);
|
||||
}
|
||||
|
||||
private static ArrayList<String> statusSlots(String matchText) {
|
||||
ArrayList<String> slots = new ArrayList<>();
|
||||
add(slots, normalizedSlot(matchText));
|
||||
if (matchText.contains("signal:wifi") || matchText.contains("wifi")) {
|
||||
add(slots, "wifi");
|
||||
}
|
||||
if (matchText.contains("signal:mobile") || matchText.contains("mobile")) {
|
||||
add(slots, "mobile");
|
||||
}
|
||||
if (matchText.contains("battery")) {
|
||||
add(slots, "battery");
|
||||
add(slots, "battery_icon");
|
||||
}
|
||||
if (matchText.contains("zen") || matchText.contains("dnd") || matchText.contains("disturb")) {
|
||||
add(slots, "zen");
|
||||
add(slots, "dnd");
|
||||
add(slots, "donotdisturb");
|
||||
}
|
||||
if (matchText.contains("volume") || matchText.contains("mute")) {
|
||||
add(slots, "volume");
|
||||
add(slots, "mute");
|
||||
}
|
||||
if (matchText.contains("bluetooth")) {
|
||||
add(slots, "bluetooth");
|
||||
add(slots, "bluetooth_connected");
|
||||
}
|
||||
if (matchText.contains("hotspot")) {
|
||||
add(slots, "hotspot");
|
||||
}
|
||||
if (matchText.contains("alarm")) {
|
||||
add(slots, "alarm_clock");
|
||||
}
|
||||
if (matchText.contains("airplane")) {
|
||||
add(slots, "airplane");
|
||||
add(slots, "stat_sys_airplane_mode");
|
||||
}
|
||||
if (matchText.contains("vpn")) {
|
||||
add(slots, "vpn");
|
||||
}
|
||||
if (matchText.contains("nfc")) {
|
||||
add(slots, "nfc");
|
||||
add(slots, "nfc_on");
|
||||
}
|
||||
if (matchText.contains("data_saver")) {
|
||||
add(slots, "data_saver");
|
||||
}
|
||||
if (matchText.contains("location")) {
|
||||
add(slots, "location");
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static String normalizedSlot(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalized.startsWith("signal:")) {
|
||||
normalized = normalized.substring("signal:".length());
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String modeName(SceneKey scene) {
|
||||
if (scene == null) {
|
||||
return null;
|
||||
}
|
||||
if (scene.isAod()) {
|
||||
return "AOD";
|
||||
}
|
||||
if (scene.isLockscreen()) {
|
||||
return "LOCK";
|
||||
}
|
||||
if (scene.isUnlocked()) {
|
||||
return "UNLOCK";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void add(ArrayList<String> values, String value) {
|
||||
if (value == null || value.isEmpty() || values.contains(value)) {
|
||||
return;
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
private static void append(StringBuilder sb, String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(value);
|
||||
}
|
||||
|
||||
private static String resolveIdName(View view) {
|
||||
if (view == null || view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -265,6 +265,47 @@ public final class UnlockedIconSnapshotRenderController {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean refreshUnlockedClockText(
|
||||
View root,
|
||||
ViewGroup clockHost,
|
||||
SceneKey scene
|
||||
) {
|
||||
if (root == null
|
||||
|| clockHost == null
|
||||
|| scene == null
|
||||
|| !scene.isUnlocked()
|
||||
|| !root.isAttachedToWindow()) {
|
||||
return false;
|
||||
}
|
||||
LayoutPlan layoutPlan = lastLayoutPlanByRoot.get(root);
|
||||
if (layoutPlan == null || layoutPlan.clockPlacement == null) {
|
||||
return false;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
if (settings == null || !settings.clockEnabledUnlocked) {
|
||||
return false;
|
||||
}
|
||||
CollectedIcons collectedIcons = lastCollectedIconsByRoot.get(root);
|
||||
int referenceColor = clockReferenceColor(layoutPlan.clockPlacement, collectedIcons);
|
||||
ClockPlacement updatedClock = clockPlacementWithTextFromModel(
|
||||
layoutPlan.clockPlacement,
|
||||
layoutPlan.clockPlacement.source,
|
||||
settings,
|
||||
referenceColor);
|
||||
if (updatedClock == null) {
|
||||
return false;
|
||||
}
|
||||
LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock);
|
||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||
lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings));
|
||||
clockRenderer.render(clockHost, updatedClock);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasLayoutPlan(View root) {
|
||||
return root != null && lastLayoutPlanByRoot.containsKey(root);
|
||||
}
|
||||
|
||||
private void clearLockscreenHosts(
|
||||
ViewGroup clockHost,
|
||||
ViewGroup carrierHost,
|
||||
|
||||
+8
-2
@@ -96,7 +96,10 @@ final class UnlockedLayoutPlanner {
|
||||
int statusCount = statusCountForScene(settings, scene);
|
||||
if (statusCount > 0) {
|
||||
ArrayList<SnapshotPlacement> rawPlacements =
|
||||
statusRenderer.rawPlacements(root, collectedIcons.statusIcons);
|
||||
SnapshotIconFilter.statusPlacements(
|
||||
statusRenderer.rawPlacements(root, collectedIcons.statusIcons),
|
||||
settings,
|
||||
scene);
|
||||
int representativeHeight = representativeHeight(rawPlacements);
|
||||
String[] positions = statusPositionsForScene(settings, scene);
|
||||
String[] middleSides = statusMiddleSidesForScene(settings, scene);
|
||||
@@ -128,7 +131,10 @@ final class UnlockedLayoutPlanner {
|
||||
int notificationCount = notificationCountForScene(settings, scene);
|
||||
if (notificationCount > 0) {
|
||||
ArrayList<SnapshotPlacement> rawPlacements =
|
||||
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons);
|
||||
SnapshotIconFilter.notificationPlacements(
|
||||
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons),
|
||||
settings,
|
||||
scene);
|
||||
if (scene != null
|
||||
&& scene.isLockscreen()
|
||||
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
|
||||
|
||||
+37
-20
@@ -120,8 +120,8 @@ final class UnlockedStockSourceCollector {
|
||||
anchors != null ? anchors.clockContainer : null),
|
||||
notificationRootSignature(anchors != null ? anchors.notificationRoot : null),
|
||||
viewSignatureOrZero(anchors != null ? anchors.carrierView : null),
|
||||
sourceListSignature(previousIcons != null ? previousIcons.statusIcons : null),
|
||||
sourceListSignature(previousIcons != null ? previousIcons.notificationIcons : null),
|
||||
sourceLayoutListSignature(previousIcons != null ? previousIcons.statusIcons : null),
|
||||
sourceLayoutListSignature(previousIcons != null ? previousIcons.notificationIcons : null),
|
||||
chipSourceSignature);
|
||||
}
|
||||
|
||||
@@ -466,20 +466,6 @@ final class UnlockedStockSourceCollector {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int viewTreeSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = viewSignature(view);
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
result = 31 * result + viewTreeSignature(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int viewSignatureOrZero(View view) {
|
||||
return view != null ? viewSignature(view) : 0;
|
||||
}
|
||||
@@ -520,7 +506,6 @@ final class UnlockedStockSourceCollector {
|
||||
result = 31 * result + view.getClass().getName().hashCode();
|
||||
result = 31 * result + view.getId();
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(view.getAlpha());
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
@@ -529,13 +514,12 @@ final class UnlockedStockSourceCollector {
|
||||
result = 31 * result + child.getClass().getName().hashCode();
|
||||
result = 31 * result + child.getId();
|
||||
result = 31 * result + child.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(child.getAlpha());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int sourceListSignature(ArrayList<SnapshotSource> sources) {
|
||||
private static int sourceLayoutListSignature(ArrayList<SnapshotSource> sources) {
|
||||
if (sources == null || sources.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -543,7 +527,34 @@ final class UnlockedStockSourceCollector {
|
||||
result = 31 * result + sources.size();
|
||||
for (SnapshotSource source : sources) {
|
||||
result = 31 * result + source.mode().ordinal();
|
||||
result = 31 * result + viewTreeSignature(source.view());
|
||||
result = 31 * result + sourceLayoutSignature(source.view());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int sourceLayoutSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(view);
|
||||
result = 31 * result + view.getClass().getName().hashCode();
|
||||
result = 31 * result + view.getId();
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + width;
|
||||
result = 31 * result + height;
|
||||
CharSequence contentDescription = view.getContentDescription();
|
||||
result = 31 * result + (contentDescription != null ? contentDescription.toString().hashCode() : 0);
|
||||
if (view instanceof ImageView imageView) {
|
||||
result = 31 * result + drawableLayoutSignature(imageView.getDrawable());
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
result = 31 * result + sourceLayoutSignature(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -560,6 +571,9 @@ final class UnlockedStockSourceCollector {
|
||||
hasKnownChipSource = true;
|
||||
result = 31 * result + chipSourceListSignature(previousIcons.statusChipMetrics);
|
||||
}
|
||||
if (!hasKnownChipSource) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (!hasKnownChipSource) {
|
||||
result = 31 * result + statusChipCandidateSignature(root);
|
||||
@@ -780,6 +794,9 @@ final class UnlockedStockSourceCollector {
|
||||
settings.layoutStatusMiddleSide,
|
||||
settings.layoutStatusMiddleAutoNearestSide,
|
||||
settings.layoutStatusVerticalOffsetPx,
|
||||
settings.systemIconBlockedModes,
|
||||
settings.appIconBlockedModes,
|
||||
settings.appIconExceptionRules,
|
||||
settings.statusChipsHideAll,
|
||||
settings.statusChipsHideMediaUnlocked,
|
||||
settings.statusChipsHideNavUnlocked,
|
||||
|
||||
@@ -1417,8 +1417,10 @@ public final class SbtSettings {
|
||||
String fallback
|
||||
) {
|
||||
String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
String previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = prefs.getString(key.key(i), fallback);
|
||||
previous = prefs.contains(key.key(i)) ? prefs.getString(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1429,40 +1431,50 @@ public final class SbtSettings {
|
||||
boolean fallback
|
||||
) {
|
||||
boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
boolean previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = prefs.getBoolean(key.key(i), fallback);
|
||||
previous = prefs.contains(key.key(i)) ? prefs.getBoolean(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int[] readIntArrayFromPrefs(SharedPreferences prefs, IndexedKey key, int fallback) {
|
||||
int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
int previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = prefs.getInt(key.key(i), fallback);
|
||||
previous = prefs.contains(key.key(i)) ? prefs.getInt(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String[] readStringArrayFromBundle(Bundle bundle, IndexedKey key, String fallback) {
|
||||
String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
String previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = bundle.getString(key.key(i), fallback);
|
||||
previous = bundle.containsKey(key.key(i)) ? bundle.getString(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean[] readBooleanArrayFromBundle(Bundle bundle, IndexedKey key, boolean fallback) {
|
||||
boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
boolean previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = bundle.getBoolean(key.key(i), fallback);
|
||||
previous = bundle.containsKey(key.key(i)) ? bundle.getBoolean(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int[] readIntArrayFromBundle(Bundle bundle, IndexedKey key, int fallback) {
|
||||
int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
|
||||
int previous = fallback;
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = bundle.getInt(key.key(i), fallback);
|
||||
previous = bundle.containsKey(key.key(i)) ? bundle.getInt(key.key(i), previous) : previous;
|
||||
result[i] = previous;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+20
-4
@@ -411,11 +411,27 @@ public class SbtSettingsProvider extends ContentProvider {
|
||||
IndexedKey verticalOffsetKey,
|
||||
int verticalOffsetDefault
|
||||
) {
|
||||
String position = positionDefault;
|
||||
String middleSide = middleSideDefault;
|
||||
boolean autoNearest = autoNearestDefault;
|
||||
int verticalOffset = verticalOffsetDefault;
|
||||
for (int i = 0; i < SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX; i++) {
|
||||
out.putString(positionKey.key(i), prefs.getString(positionKey.key(i), positionDefault));
|
||||
out.putString(middleSideKey.key(i), prefs.getString(middleSideKey.key(i), middleSideDefault));
|
||||
out.putBoolean(autoNearestKey.key(i), prefs.getBoolean(autoNearestKey.key(i), autoNearestDefault));
|
||||
out.putInt(verticalOffsetKey.key(i), prefs.getInt(verticalOffsetKey.key(i), verticalOffsetDefault));
|
||||
position = prefs.contains(positionKey.key(i))
|
||||
? prefs.getString(positionKey.key(i), position)
|
||||
: position;
|
||||
middleSide = prefs.contains(middleSideKey.key(i))
|
||||
? prefs.getString(middleSideKey.key(i), middleSide)
|
||||
: middleSide;
|
||||
autoNearest = prefs.contains(autoNearestKey.key(i))
|
||||
? prefs.getBoolean(autoNearestKey.key(i), autoNearest)
|
||||
: autoNearest;
|
||||
verticalOffset = prefs.contains(verticalOffsetKey.key(i))
|
||||
? prefs.getInt(verticalOffsetKey.key(i), verticalOffset)
|
||||
: verticalOffset;
|
||||
out.putString(positionKey.key(i), position);
|
||||
out.putString(middleSideKey.key(i), middleSide);
|
||||
out.putBoolean(autoNearestKey.key(i), autoNearest);
|
||||
out.putInt(verticalOffsetKey.key(i), verticalOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,14 @@ public final class LayoutFragment extends Fragment {
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.clock_position_mode_unlocked),
|
||||
SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
|
||||
SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT);
|
||||
LayoutSectionCopySupport.addDropdown(
|
||||
requireContext(),
|
||||
prefs,
|
||||
root.findViewById(R.id.clock_position_mode_unlocked),
|
||||
LayoutSectionCopySupport.Mode.UNLOCKED,
|
||||
LayoutSectionCopySupport.Section.CLOCK,
|
||||
false,
|
||||
this::refreshSelf);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_position_mode_aod),
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT);
|
||||
@@ -221,7 +229,8 @@ public final class LayoutFragment extends Fragment {
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS);
|
||||
setupUnlockedIconContainerCount(
|
||||
root,
|
||||
prefs,
|
||||
@@ -238,7 +247,8 @@ public final class LayoutFragment extends Fragment {
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
LayoutSectionCopySupport.Section.STATUS);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -262,7 +272,8 @@ public final class LayoutFragment extends Fragment {
|
||||
String positionDefault,
|
||||
boolean autoNearestDefault,
|
||||
String middleSideDefault,
|
||||
int verticalOffsetDefault
|
||||
int verticalOffsetDefault,
|
||||
LayoutSectionCopySupport.Section copySection
|
||||
) {
|
||||
CheckBox checkbox = root.findViewById(checkboxId);
|
||||
if (checkbox == null || !(checkbox.getParent() instanceof ViewGroup modeRow)
|
||||
@@ -287,6 +298,17 @@ public final class LayoutFragment extends Fragment {
|
||||
removeGeneratedCopyCards(cardsParent, copyTag);
|
||||
int count = iconContainerCount(prefs, countKey, countDefault, legacyEnabledKey, legacyEnabledDefault);
|
||||
updateIconCardTitle(sourceCard, sectionTitleId, 0, count);
|
||||
boolean cardsMode = LockedNotificationModeUi.MODE_CARDS.equals(
|
||||
LockedNotificationModeUi.currentMode(requireContext()));
|
||||
LayoutSectionCopySupport.addDropdownRow(
|
||||
requireContext(),
|
||||
prefs,
|
||||
countContainer,
|
||||
LayoutSectionCopySupport.Mode.UNLOCKED,
|
||||
copySection,
|
||||
cardsMode,
|
||||
0,
|
||||
this::refreshSelf);
|
||||
countContainer.addView(iconContainerCountControl(
|
||||
prefs,
|
||||
countKey,
|
||||
@@ -322,6 +344,22 @@ public final class LayoutFragment extends Fragment {
|
||||
rebuild[0].run();
|
||||
}
|
||||
|
||||
private void refreshSelf() {
|
||||
View view = getView();
|
||||
if (view == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
view.post(() -> {
|
||||
if (isAdded()) {
|
||||
getParentFragmentManager()
|
||||
.beginTransaction()
|
||||
.detach(this)
|
||||
.attach(this)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private View iconContainerCountControl(
|
||||
SharedPreferences prefs,
|
||||
String countKey,
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class LayoutSectionCopySupport {
|
||||
private static final String COPY_ROW_TAG = "layout_section_copy_row";
|
||||
|
||||
enum Mode {
|
||||
UNLOCKED,
|
||||
LOCKSCREEN,
|
||||
AOD
|
||||
}
|
||||
|
||||
enum Section {
|
||||
CLOCK,
|
||||
STATUS,
|
||||
NOTIFICATIONS_ICONS,
|
||||
NOTIFICATIONS_CARDS
|
||||
}
|
||||
|
||||
private static final int MAX_COPIES = SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX;
|
||||
|
||||
private LayoutSectionCopySupport() {
|
||||
}
|
||||
|
||||
static void addDropdown(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
CheckBox enabled,
|
||||
Mode destinationMode,
|
||||
Section section,
|
||||
boolean cardsMode,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
if (context == null || prefs == null || enabled == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<Option> options = optionsFor(context, destinationMode, section, cardsMode);
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Spinner spinner = new Spinner(context);
|
||||
CopyAdapter adapter = new CopyAdapter(context, options);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setSelection(0, false);
|
||||
spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position <= 0 || position >= options.size()) {
|
||||
return;
|
||||
}
|
||||
Option option = options.get(position);
|
||||
spinner.setSelection(0, false);
|
||||
if (!option.enabled) {
|
||||
return;
|
||||
}
|
||||
copy(prefs, option.mode, destinationMode, section);
|
||||
SbtSettings.ensureReadable(context);
|
||||
if (refresh != null) {
|
||||
refresh.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(android.widget.AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
insertBesideEnabled(context, enabled, spinner);
|
||||
}
|
||||
|
||||
static void addCardsDropdown(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
LinearLayout content,
|
||||
Mode destinationMode,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
addDropdownRow(context, prefs, content, destinationMode, Section.NOTIFICATIONS_CARDS, true, 0, refresh);
|
||||
}
|
||||
|
||||
static void addDropdownRow(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
LinearLayout content,
|
||||
Mode destinationMode,
|
||||
Section section,
|
||||
boolean cardsMode,
|
||||
int insertIndex,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
if (context == null || prefs == null || content == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<Option> options = optionsFor(context, destinationMode, section, cardsMode);
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
removeExistingRows(content);
|
||||
LinearLayout row = buildDropdownRow(context, prefs, options, destinationMode, section, refresh);
|
||||
int index = Math.max(0, Math.min(insertIndex, content.getChildCount()));
|
||||
content.addView(row, index);
|
||||
}
|
||||
|
||||
private static ArrayList<Option> optionsFor(
|
||||
Context context,
|
||||
Mode destinationMode,
|
||||
Section section,
|
||||
boolean cardsMode
|
||||
) {
|
||||
ArrayList<Option> result = new ArrayList<>();
|
||||
result.add(new Option(null, context.getString(R.string.layout_copy_from), true));
|
||||
for (Mode mode : Mode.values()) {
|
||||
if (mode == destinationMode || !hasSection(mode, section)) {
|
||||
continue;
|
||||
}
|
||||
result.add(new Option(mode, label(context, mode), canCopy(mode, destinationMode, section, cardsMode)));
|
||||
}
|
||||
if (result.size() == 1) {
|
||||
result.clear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean hasSection(Mode mode, Section section) {
|
||||
if (section == Section.CLOCK) {
|
||||
return mode != Mode.AOD;
|
||||
}
|
||||
if (section == Section.NOTIFICATIONS_CARDS) {
|
||||
return true;
|
||||
}
|
||||
return section == Section.STATUS || section == Section.NOTIFICATIONS_ICONS;
|
||||
}
|
||||
|
||||
private static boolean canCopy(Mode source, Mode destination, Section section, boolean cardsMode) {
|
||||
if (section == Section.NOTIFICATIONS_ICONS) {
|
||||
boolean sourceCards = cardsMode && source != Mode.UNLOCKED;
|
||||
boolean destinationCards = cardsMode && destination != Mode.UNLOCKED;
|
||||
return sourceCards == destinationCards;
|
||||
}
|
||||
if (section == Section.NOTIFICATIONS_CARDS) {
|
||||
return cardsMode && source != Mode.UNLOCKED && destination != Mode.UNLOCKED;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void copy(SharedPreferences prefs, Mode source, Mode destination, Section section) {
|
||||
if (source == null || source == destination) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
if (section == Section.CLOCK) {
|
||||
copySingle(editor, prefs, clockKeys(source), clockKeys(destination));
|
||||
} else if (section == Section.STATUS) {
|
||||
copyIcons(editor, prefs, statusKeys(source), statusKeys(destination));
|
||||
} else if (section == Section.NOTIFICATIONS_ICONS) {
|
||||
copyIcons(editor, prefs, notificationKeys(source), notificationKeys(destination));
|
||||
} else if (section == Section.NOTIFICATIONS_CARDS) {
|
||||
copySingle(editor, prefs, cardKeys(source), cardKeys(destination));
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private static void copySingle(SharedPreferences.Editor editor, SharedPreferences prefs, KeySet src, KeySet dst) {
|
||||
copyBoolean(editor, prefs, src.enabled, dst.enabled, src.enabledDefault);
|
||||
copyString(editor, prefs, src.position, dst.position, src.positionDefault);
|
||||
copyBoolean(editor, prefs, src.autoNearest, dst.autoNearest, src.autoNearestDefault);
|
||||
copyString(editor, prefs, src.middleSide, dst.middleSide, src.middleSideDefault);
|
||||
copyInt(editor, prefs, src.verticalOffset, dst.verticalOffset, src.verticalOffsetDefault);
|
||||
copyInt(editor, prefs, src.maxRows, dst.maxRows, src.maxRowsDefault);
|
||||
copyInt(editor, prefs, src.maxIcons, dst.maxIcons, src.maxIconsDefault);
|
||||
copyBoolean(editor, prefs, src.evenDistribution, dst.evenDistribution, src.evenDistributionDefault);
|
||||
copyBoolean(editor, prefs, src.limitWidth, dst.limitWidth, src.limitWidthDefault);
|
||||
}
|
||||
|
||||
private static void copyIcons(SharedPreferences.Editor editor, SharedPreferences prefs, KeySet src, KeySet dst) {
|
||||
copyBoolean(editor, prefs, src.enabled, dst.enabled, src.enabledDefault);
|
||||
int count = prefs.getInt(src.count, src.countDefault);
|
||||
editor.putInt(dst.count, count);
|
||||
for (int i = 0; i < MAX_COPIES; i++) {
|
||||
copyString(editor, prefs, src.positionAt(i), dst.positionAt(i), src.positionDefault);
|
||||
copyBoolean(editor, prefs, src.autoNearestAt(i), dst.autoNearestAt(i), src.autoNearestDefault);
|
||||
copyString(editor, prefs, src.middleSideAt(i), dst.middleSideAt(i), src.middleSideDefault);
|
||||
copyInt(editor, prefs, src.verticalOffsetAt(i), dst.verticalOffsetAt(i), src.verticalOffsetDefault);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyString(
|
||||
SharedPreferences.Editor editor,
|
||||
SharedPreferences prefs,
|
||||
@Nullable String src,
|
||||
@Nullable String dst,
|
||||
String fallback
|
||||
) {
|
||||
if (src != null && dst != null) {
|
||||
editor.putString(dst, prefs.getString(src, fallback));
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyBoolean(
|
||||
SharedPreferences.Editor editor,
|
||||
SharedPreferences prefs,
|
||||
@Nullable String src,
|
||||
@Nullable String dst,
|
||||
boolean fallback
|
||||
) {
|
||||
if (src != null && dst != null) {
|
||||
editor.putBoolean(dst, prefs.getBoolean(src, fallback));
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyInt(
|
||||
SharedPreferences.Editor editor,
|
||||
SharedPreferences prefs,
|
||||
@Nullable String src,
|
||||
@Nullable String dst,
|
||||
int fallback
|
||||
) {
|
||||
if (src != null && dst != null) {
|
||||
editor.putInt(dst, prefs.getInt(src, fallback));
|
||||
}
|
||||
}
|
||||
|
||||
private static KeySet clockKeys(Mode mode) {
|
||||
if (mode == Mode.LOCKSCREEN) {
|
||||
return new KeySet(
|
||||
SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN,
|
||||
"layout_lock_clock_position",
|
||||
"layout_lock_clock_middle_auto_nearest_side",
|
||||
"layout_lock_clock_middle_side",
|
||||
"layout_lock_clock_vertical_offset_px",
|
||||
null,
|
||||
SbtDefaults.CLOCK_ENABLED_LOCKSCREEN_DEFAULT,
|
||||
SbtDefaults.CLOCK_POSITION_DEFAULT,
|
||||
SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
|
||||
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
1);
|
||||
}
|
||||
return new KeySet(
|
||||
SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
|
||||
SbtSettings.KEY_CLOCK_POSITION,
|
||||
SbtSettings.KEY_CLOCK_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_SIDE,
|
||||
SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX,
|
||||
null,
|
||||
SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT,
|
||||
SbtDefaults.CLOCK_POSITION_DEFAULT,
|
||||
SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
|
||||
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
1);
|
||||
}
|
||||
|
||||
private static KeySet notificationKeys(Mode mode) {
|
||||
if (mode == Mode.LOCKSCREEN) {
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
|
||||
"layout_lock_notifications_count",
|
||||
"layout_lock_notifications_position",
|
||||
"layout_lock_notifications_middle_auto_nearest_side",
|
||||
"layout_lock_notifications_middle_side",
|
||||
"layout_lock_notifications_vertical_offset_px",
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
if (mode == Mode.AOD) {
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
|
||||
"layout_aod_notifications_count",
|
||||
"layout_aod_notifications_position",
|
||||
"layout_aod_notifications_middle_auto_nearest_side",
|
||||
"layout_aod_notifications_middle_side",
|
||||
"layout_aod_notifications_vertical_offset_px",
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_POSITION,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
|
||||
private static KeySet statusKeys(Mode mode) {
|
||||
if (mode == Mode.LOCKSCREEN) {
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
|
||||
"layout_lock_status_count",
|
||||
"layout_lock_status_position",
|
||||
"layout_lock_status_middle_auto_nearest_side",
|
||||
"layout_lock_status_middle_side",
|
||||
"layout_lock_status_vertical_offset_px",
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
if (mode == Mode.AOD) {
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
|
||||
"layout_aod_status_count",
|
||||
"layout_aod_status_position",
|
||||
"layout_aod_status_middle_auto_nearest_side",
|
||||
"layout_aod_status_middle_side",
|
||||
"layout_aod_status_vertical_offset_px",
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
return iconKeys(
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_POSITION,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
}
|
||||
|
||||
private static KeySet cardKeys(Mode mode) {
|
||||
if (mode == Mode.AOD) {
|
||||
return KeySet.cards(
|
||||
SbtSettings.KEY_CARDS_AOD_MAX_ROWS,
|
||||
SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW,
|
||||
SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION,
|
||||
SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH,
|
||||
SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT,
|
||||
SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT);
|
||||
}
|
||||
return KeySet.cards(
|
||||
SbtSettings.KEY_CARDS_LOCK_MAX_ROWS,
|
||||
SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW,
|
||||
SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION,
|
||||
SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT,
|
||||
SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT);
|
||||
}
|
||||
|
||||
private static KeySet iconKeys(
|
||||
String enabled,
|
||||
String count,
|
||||
String position,
|
||||
String autoNearest,
|
||||
String middleSide,
|
||||
String verticalOffset,
|
||||
boolean enabledDefault,
|
||||
int countDefault,
|
||||
String positionDefault,
|
||||
boolean autoNearestDefault,
|
||||
String middleSideDefault,
|
||||
int verticalOffsetDefault
|
||||
) {
|
||||
return new KeySet(
|
||||
enabled,
|
||||
position,
|
||||
autoNearest,
|
||||
middleSide,
|
||||
verticalOffset,
|
||||
count,
|
||||
enabledDefault,
|
||||
positionDefault,
|
||||
autoNearestDefault,
|
||||
middleSideDefault,
|
||||
verticalOffsetDefault,
|
||||
countDefault);
|
||||
}
|
||||
|
||||
private static String label(Context context, Mode mode) {
|
||||
return switch (mode) {
|
||||
case UNLOCKED -> context.getString(R.string.section_unlocked);
|
||||
case LOCKSCREEN -> context.getString(R.string.section_lockscreen);
|
||||
case AOD -> context.getString(R.string.section_aod);
|
||||
};
|
||||
}
|
||||
|
||||
private static void insertBesideEnabled(Context context, CheckBox enabled, Spinner spinner) {
|
||||
if (!(enabled.getParent() instanceof LinearLayout parent)) {
|
||||
return;
|
||||
}
|
||||
removeExistingRows(parent);
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
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,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
rowParams.leftMargin = dp(context, 12);
|
||||
if (parent.getOrientation() == LinearLayout.HORIZONTAL) {
|
||||
parent.addView(row, rowParams);
|
||||
} else {
|
||||
parent.addView(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static LinearLayout buildDropdownRow(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
ArrayList<Option> options,
|
||||
Mode destinationMode,
|
||||
Section section,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
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);
|
||||
spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position <= 0 || position >= options.size()) {
|
||||
return;
|
||||
}
|
||||
Option option = options.get(position);
|
||||
spinner.setSelection(0, false);
|
||||
if (!option.enabled) {
|
||||
return;
|
||||
}
|
||||
copy(prefs, option.mode, destinationMode, section);
|
||||
SbtSettings.ensureReadable(context);
|
||||
if (refresh != null) {
|
||||
refresh.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(android.widget.AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
private static void removeExistingRows(LinearLayout parent) {
|
||||
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
|
||||
if (COPY_ROW_TAG.equals(parent.getChildAt(i).getTag())) {
|
||||
parent.removeViewAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int dp(Context context, int value) {
|
||||
return Math.round(value * context.getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private record Option(@Nullable Mode mode, String label, boolean enabled) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CopyAdapter extends ArrayAdapter<Option> {
|
||||
CopyAdapter(Context context, List<Option> options) {
|
||||
super(context, android.R.layout.simple_spinner_item, options);
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
Option option = getItem(position);
|
||||
return option != null && option.enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, ViewGroup parent) {
|
||||
View view = super.getDropDownView(position, convertView, parent);
|
||||
Option option = getItem(position);
|
||||
view.setEnabled(option != null && option.enabled);
|
||||
if (view instanceof TextView textView) {
|
||||
textView.setAlpha(option != null && option.enabled ? 1f : 0.45f);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class KeySet {
|
||||
final String enabled;
|
||||
final String position;
|
||||
final String autoNearest;
|
||||
final String middleSide;
|
||||
final String verticalOffset;
|
||||
final String count;
|
||||
final boolean enabledDefault;
|
||||
final String positionDefault;
|
||||
final boolean autoNearestDefault;
|
||||
final String middleSideDefault;
|
||||
final int verticalOffsetDefault;
|
||||
final int countDefault;
|
||||
final String maxRows;
|
||||
final String maxIcons;
|
||||
final String evenDistribution;
|
||||
final String limitWidth;
|
||||
final int maxRowsDefault;
|
||||
final int maxIconsDefault;
|
||||
final boolean evenDistributionDefault;
|
||||
final boolean limitWidthDefault;
|
||||
|
||||
KeySet(
|
||||
String enabled,
|
||||
String position,
|
||||
String autoNearest,
|
||||
String middleSide,
|
||||
String verticalOffset,
|
||||
String count,
|
||||
boolean enabledDefault,
|
||||
String positionDefault,
|
||||
boolean autoNearestDefault,
|
||||
String middleSideDefault,
|
||||
int verticalOffsetDefault,
|
||||
int countDefault
|
||||
) {
|
||||
this.enabled = enabled;
|
||||
this.position = position;
|
||||
this.autoNearest = autoNearest;
|
||||
this.middleSide = middleSide;
|
||||
this.verticalOffset = verticalOffset;
|
||||
this.count = count;
|
||||
this.enabledDefault = enabledDefault;
|
||||
this.positionDefault = positionDefault;
|
||||
this.autoNearestDefault = autoNearestDefault;
|
||||
this.middleSideDefault = middleSideDefault;
|
||||
this.verticalOffsetDefault = verticalOffsetDefault;
|
||||
this.countDefault = countDefault;
|
||||
this.maxRows = null;
|
||||
this.maxIcons = null;
|
||||
this.evenDistribution = null;
|
||||
this.limitWidth = null;
|
||||
this.maxRowsDefault = 0;
|
||||
this.maxIconsDefault = 0;
|
||||
this.evenDistributionDefault = false;
|
||||
this.limitWidthDefault = false;
|
||||
}
|
||||
|
||||
static KeySet cards(
|
||||
String maxRows,
|
||||
String maxIcons,
|
||||
String evenDistribution,
|
||||
String limitWidth,
|
||||
int maxRowsDefault,
|
||||
int maxIconsDefault,
|
||||
boolean evenDistributionDefault,
|
||||
boolean limitWidthDefault
|
||||
) {
|
||||
return new KeySet(maxRows, maxIcons, evenDistribution, limitWidth,
|
||||
maxRowsDefault, maxIconsDefault, evenDistributionDefault, limitWidthDefault);
|
||||
}
|
||||
|
||||
private KeySet(
|
||||
String maxRows,
|
||||
String maxIcons,
|
||||
String evenDistribution,
|
||||
String limitWidth,
|
||||
int maxRowsDefault,
|
||||
int maxIconsDefault,
|
||||
boolean evenDistributionDefault,
|
||||
boolean limitWidthDefault
|
||||
) {
|
||||
this.enabled = null;
|
||||
this.position = null;
|
||||
this.autoNearest = null;
|
||||
this.middleSide = null;
|
||||
this.verticalOffset = null;
|
||||
this.count = null;
|
||||
this.enabledDefault = false;
|
||||
this.positionDefault = null;
|
||||
this.autoNearestDefault = false;
|
||||
this.middleSideDefault = null;
|
||||
this.verticalOffsetDefault = 0;
|
||||
this.countDefault = 0;
|
||||
this.maxRows = maxRows;
|
||||
this.maxIcons = maxIcons;
|
||||
this.evenDistribution = evenDistribution;
|
||||
this.limitWidth = limitWidth;
|
||||
this.maxRowsDefault = maxRowsDefault;
|
||||
this.maxIconsDefault = maxIconsDefault;
|
||||
this.evenDistributionDefault = evenDistributionDefault;
|
||||
this.limitWidthDefault = limitWidthDefault;
|
||||
}
|
||||
|
||||
String positionAt(int index) {
|
||||
return suffixed(position, index);
|
||||
}
|
||||
|
||||
String autoNearestAt(int index) {
|
||||
return suffixed(autoNearest, index);
|
||||
}
|
||||
|
||||
String middleSideAt(int index) {
|
||||
return suffixed(middleSide, index);
|
||||
}
|
||||
|
||||
String verticalOffsetAt(int index) {
|
||||
return suffixed(verticalOffset, index);
|
||||
}
|
||||
|
||||
private String suffixed(String base, int index) {
|
||||
if (base == null || index <= 0) {
|
||||
return base;
|
||||
}
|
||||
return base + "_" + (index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-7
@@ -80,9 +80,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
|
||||
private void rebuildDynamic(Context context, SharedPreferences prefs, String mode) {
|
||||
dynamicContent.removeAllViews();
|
||||
Runnable refresh = () -> rebuildDynamic(context, prefs, LockedNotificationModeUi.currentMode(context));
|
||||
LinkedHashMap<String, View> cardsByOrderKey = new LinkedHashMap<>();
|
||||
if (!aod) {
|
||||
cardsByOrderKey.put("clock", configuredPositionCardFromLayout(
|
||||
View clockCard = configuredPositionCardFromLayout(
|
||||
context,
|
||||
prefs,
|
||||
R.id.layout_clock_card,
|
||||
@@ -108,7 +109,16 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
sceneKey("clock_middle_side"),
|
||||
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
|
||||
sceneKey("clock_vertical_offset_px"),
|
||||
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
LayoutSectionCopySupport.addDropdown(
|
||||
context,
|
||||
prefs,
|
||||
clockCard.findViewById(R.id.clock_position_mode_lockscreen),
|
||||
copyMode(),
|
||||
LayoutSectionCopySupport.Section.CLOCK,
|
||||
false,
|
||||
refresh);
|
||||
cardsByOrderKey.put("clock", clockCard);
|
||||
View carrierCard = configuredPositionCardFromLayout(
|
||||
context,
|
||||
prefs,
|
||||
@@ -163,9 +173,12 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
LayoutSectionCopySupport.Section.STATUS,
|
||||
false,
|
||||
refresh));
|
||||
if (LockedNotificationModeUi.MODE_CARDS.equals(mode)) {
|
||||
cardsByOrderKey.put("notifications", cardsNotificationCard(context, prefs));
|
||||
cardsByOrderKey.put("notifications", cardsNotificationCard(context, prefs, refresh));
|
||||
} else {
|
||||
cardsByOrderKey.put("notifications", configuredIconContainersCardFromLayout(
|
||||
context,
|
||||
@@ -191,7 +204,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT,
|
||||
LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS,
|
||||
false,
|
||||
refresh));
|
||||
}
|
||||
for (String key : LayoutOrderingUi.orderedKeys(prefs)) {
|
||||
View card = cardsByOrderKey.remove(key);
|
||||
@@ -292,7 +308,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
String positionDefault,
|
||||
boolean autoNearestDefault,
|
||||
String middleSideDefault,
|
||||
int verticalOffsetDefault
|
||||
int verticalOffsetDefault,
|
||||
LayoutSectionCopySupport.Section copySection,
|
||||
boolean cardsMode,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
LinearLayout wrapper = vertical(context);
|
||||
String countKey = sceneKey(base + "_count");
|
||||
@@ -331,6 +350,7 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
countKey,
|
||||
count,
|
||||
rebuild[0]);
|
||||
addCopyDropdownToCard(context, prefs, primary, copySection, cardsMode, refresh);
|
||||
wrapper.addView(primary);
|
||||
for (int i = 1; i < count; i++) {
|
||||
wrapper.addView(configuredIconContainerCard(
|
||||
@@ -370,6 +390,29 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void addCopyDropdownToCard(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
View card,
|
||||
LayoutSectionCopySupport.Section section,
|
||||
boolean cardsMode,
|
||||
@Nullable Runnable refresh
|
||||
) {
|
||||
LinearLayout content = cardContent(card);
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
LayoutSectionCopySupport.addDropdownRow(
|
||||
context,
|
||||
prefs,
|
||||
content,
|
||||
copyMode(),
|
||||
section,
|
||||
cardsMode,
|
||||
1,
|
||||
refresh);
|
||||
}
|
||||
|
||||
private View configuredIconContainerCard(
|
||||
Context context,
|
||||
SharedPreferences prefs,
|
||||
@@ -549,8 +592,9 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
return null;
|
||||
}
|
||||
|
||||
private View cardsNotificationCard(Context context, SharedPreferences prefs) {
|
||||
private View cardsNotificationCard(Context context, SharedPreferences prefs, @Nullable Runnable refresh) {
|
||||
LinearLayout content = vertical(context);
|
||||
LayoutSectionCopySupport.addCardsDropdown(context, prefs, content, copyMode(), refresh);
|
||||
content.addView(stepper(
|
||||
context,
|
||||
prefs,
|
||||
@@ -908,6 +952,23 @@ abstract class LockedSceneLayoutFragment extends Fragment {
|
||||
return index <= 0 ? sceneKey(suffix) : sceneKey(suffix) + "_" + (index + 1);
|
||||
}
|
||||
|
||||
private LayoutSectionCopySupport.Mode copyMode() {
|
||||
return aod ? LayoutSectionCopySupport.Mode.AOD : LayoutSectionCopySupport.Mode.LOCKSCREEN;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LinearLayout cardContent(View card) {
|
||||
if (!(card instanceof ViewGroup cardGroup) || cardGroup.getChildCount() <= 0) {
|
||||
return null;
|
||||
}
|
||||
View inner = cardGroup.getChildAt(0);
|
||||
if (!(inner instanceof ViewGroup innerGroup) || innerGroup.getChildCount() <= 1) {
|
||||
return null;
|
||||
}
|
||||
View content = innerGroup.getChildAt(1);
|
||||
return content instanceof LinearLayout layout ? layout : null;
|
||||
}
|
||||
|
||||
private MaterialButton button(Context context, String text) {
|
||||
MaterialButton button = new MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle);
|
||||
button.setText(text);
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
<string name="debug_camera_unknown">unknown</string>
|
||||
<string name="notification_icons_title">Notification icons</string>
|
||||
<string name="system_icons_title">Hide status icons</string>
|
||||
<string name="system_icons_hint">Saved status-icon rules for the upcoming per-icon layout rewrite.</string>
|
||||
<string name="system_icons_hide_hint">Currently unwired: changes are stored, but do not affect SystemUI yet. Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
|
||||
<string name="system_icons_hint">Status-icon rules for the custom layout engine.</string>
|
||||
<string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
|
||||
<string name="system_icons_group_connectivity">Connectivity</string>
|
||||
<string name="system_icons_group_notifications">Notifications</string>
|
||||
<string name="system_icons_group_system">System</string>
|
||||
@@ -94,7 +94,7 @@
|
||||
<string name="hidden_apps_dialog_title">Notification icon app blocks</string>
|
||||
<string name="hidden_apps_page_title">Hide notification icons</string>
|
||||
<string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string>
|
||||
<string name="hidden_apps_page_hint">Currently unwired: changes are stored for the upcoming per-icon rewrite and do not affect icon visibility yet. Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
|
||||
<string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
|
||||
<string name="hidden_apps_clear_session">Clear session list</string>
|
||||
<string name="hidden_apps_empty">No apps yet. Generate a notification first.</string>
|
||||
<string name="hidden_apps_close">Close</string>
|
||||
@@ -169,6 +169,7 @@
|
||||
<string name="layout_notification_position_title">Notification icons</string>
|
||||
<string name="layout_status_position_title">Status icons</string>
|
||||
<string name="layout_item_enabled">Enabled</string>
|
||||
<string name="layout_copy_from">Copy from</string>
|
||||
<string name="layout_icon_container_count">Number of containers</string>
|
||||
<string name="layout_locked_notifications_title">Locked notifications mode</string>
|
||||
<string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string>
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class SystemNotificationDisplayStyleDetectorTest {
|
||||
@Test
|
||||
public void styleForMinimizingValueMapsSamsungModes() {
|
||||
assertEquals(
|
||||
NotificationDisplayStyle.CARDS,
|
||||
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(0));
|
||||
assertEquals(
|
||||
NotificationDisplayStyle.ICONS,
|
||||
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(1));
|
||||
assertEquals(
|
||||
NotificationDisplayStyle.DOT,
|
||||
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void styleForMinimizingValueRejectsUnknownValues() {
|
||||
assertEquals(
|
||||
NotificationDisplayStyle.UNKNOWN,
|
||||
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(-1));
|
||||
assertEquals(
|
||||
NotificationDisplayStyle.UNKNOWN,
|
||||
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(99));
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SystemIconRules;
|
||||
|
||||
public final class SnapshotIconFilterTest {
|
||||
@Test
|
||||
public void parsesPackageFromNormalizedNotificationKey() {
|
||||
assertTrue("com.discord".equals(
|
||||
SnapshotIconFilter.notificationPackage("com.discord/0x0@0x1234")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockedAppHidesNotificationWhenNoExceptionMatches() {
|
||||
Map<String, Integer> blocked = new HashMap<>();
|
||||
AppIconRules.setModeBlocked(blocked, "com.discord", false, true, false);
|
||||
|
||||
assertTrue(SnapshotIconFilter.isNotificationBlocked(
|
||||
placement("com.discord/0x0@0x1234"),
|
||||
"com.discord",
|
||||
blocked,
|
||||
new HashMap<>(),
|
||||
"LOCK"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingExceptionOverridesBlockedAppDefault() {
|
||||
Map<String, Integer> blocked = new HashMap<>();
|
||||
AppIconRules.setModeBlocked(blocked, "com.discord", false, true, false);
|
||||
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptions = new HashMap<>();
|
||||
ArrayList<AppIconRules.ExceptionRule> rules = new ArrayList<>();
|
||||
rules.add(new AppIconRules.ExceptionRule("0x0", 0));
|
||||
exceptions.put("com.discord", rules);
|
||||
|
||||
assertFalse(SnapshotIconFilter.isNotificationBlocked(
|
||||
placement("com.discord/0x0@0x1234"),
|
||||
"com.discord",
|
||||
blocked,
|
||||
exceptions,
|
||||
"LOCK"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signalStatusKeyMatchesStoredSystemIconRule() {
|
||||
Map<String, Integer> blocked = new HashMap<>();
|
||||
SystemIconRules.setModeBlocked(blocked, "wifi", SystemIconRules.MODE_UNLOCK, true);
|
||||
|
||||
assertTrue(SnapshotIconFilter.isStatusBlocked(
|
||||
placement("signal:wifi"),
|
||||
blocked,
|
||||
"UNLOCK"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidExceptionRegexDoesNotBlockByItself() {
|
||||
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptions = new HashMap<>();
|
||||
ArrayList<AppIconRules.ExceptionRule> rules = new ArrayList<>();
|
||||
rules.add(new AppIconRules.ExceptionRule("[", AppIconRules.MODE_LOCK));
|
||||
exceptions.put("com.discord", rules);
|
||||
|
||||
assertFalse(SnapshotIconFilter.isNotificationBlocked(
|
||||
placement("com.discord/0x0@0x1234"),
|
||||
"com.discord",
|
||||
new HashMap<>(),
|
||||
exceptions,
|
||||
"LOCK"));
|
||||
}
|
||||
|
||||
private static SnapshotPlacement placement(String key) {
|
||||
return new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(0, 0, 10, 10),
|
||||
key,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
new Bounds(0, 0, 10, 10),
|
||||
0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user