Advance lockscreen layout and rotation handling
This commit is contained in:
+93
-20
@@ -6,7 +6,9 @@ import android.content.res.Resources;
|
|||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
@@ -56,7 +58,7 @@ final class StockLayoutCanvasController {
|
|||||||
|
|
||||||
private final RuntimeContext runtimeContext;
|
private final RuntimeContext runtimeContext;
|
||||||
private final Map<View, HiddenViewState> hiddenViewStates = new WeakHashMap<>();
|
private final Map<View, HiddenViewState> hiddenViewStates = new WeakHashMap<>();
|
||||||
private final Map<View, RootSize> rootSizes = new WeakHashMap<>();
|
private final Map<View, RootGeometry> rootGeometries = new WeakHashMap<>();
|
||||||
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
|
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
|
||||||
private final Set<View> confirmedAodPluginViews =
|
private final Set<View> confirmedAodPluginViews =
|
||||||
Collections.newSetFromMap(new WeakHashMap<>());
|
Collections.newSetFromMap(new WeakHashMap<>());
|
||||||
@@ -75,6 +77,10 @@ final class StockLayoutCanvasController {
|
|||||||
private final DebugBoundsOverlayController debugBoundsOverlayController =
|
private final DebugBoundsOverlayController debugBoundsOverlayController =
|
||||||
new DebugBoundsOverlayController();
|
new DebugBoundsOverlayController();
|
||||||
private final UnlockedIconSnapshotRenderController unlockedIconRenderController;
|
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
|
||||||
|
// normal current-root anchor discovery cannot find it.
|
||||||
|
private WeakReference<TextView> lockedCarrierTextSource = new WeakReference<>(null);
|
||||||
|
|
||||||
private boolean hooksInstalled;
|
private boolean hooksInstalled;
|
||||||
|
|
||||||
@@ -188,7 +194,7 @@ final class StockLayoutCanvasController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) {
|
if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) {
|
||||||
if (isKnownRootSizeChanged(root)) {
|
if (isKnownRootGeometryChanged(root)) {
|
||||||
ownedIconHostManager.setUnlockedHostsVisible(root, false);
|
ownedIconHostManager.setUnlockedHostsVisible(root, false);
|
||||||
removeDebugOverlay(root);
|
removeDebugOverlay(root);
|
||||||
}
|
}
|
||||||
@@ -203,6 +209,7 @@ final class StockLayoutCanvasController {
|
|||||||
unlockedHosts.statusHost,
|
unlockedHosts.statusHost,
|
||||||
unlockedHosts.notificationHost,
|
unlockedHosts.notificationHost,
|
||||||
activeScene);
|
activeScene);
|
||||||
|
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,13 +243,18 @@ final class StockLayoutCanvasController {
|
|||||||
removeDebugOverlay(root);
|
removeDebugOverlay(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (activeScene != null
|
||||||
|
&& (activeScene.isUnlocked() || activeScene.isLockscreen())
|
||||||
|
&& resetUnlockedHostsForRootGeometryChange(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
UnlockedHosts unlockedHosts = updateUnlockedOwnedHosts(root, activeScene);
|
UnlockedHosts unlockedHosts = updateUnlockedOwnedHosts(root, activeScene);
|
||||||
RenderResult renderResult = new RenderResult(false, false);
|
RenderResult renderResult = new RenderResult(false, false);
|
||||||
if (unlockedHosts != null && (unlockedScene || activeScene != null && activeScene.isLockscreen())) {
|
if (unlockedHosts != null && (unlockedScene || activeScene != null && activeScene.isLockscreen())) {
|
||||||
if (!unlockedScene) {
|
if (!unlockedScene) {
|
||||||
restoreTrackedLockedStatusBarIconSurfaces(root);
|
restoreTrackedLockedStatusBarIconSurfaces(root);
|
||||||
}
|
}
|
||||||
markRootSizeBeforeDraw(root);
|
markRootGeometryBeforeDraw(root);
|
||||||
renderResult = unlockedIconRenderController.renderUnlocked(
|
renderResult = unlockedIconRenderController.renderUnlocked(
|
||||||
root,
|
root,
|
||||||
unlockedHosts.clockHost,
|
unlockedHosts.clockHost,
|
||||||
@@ -250,13 +262,20 @@ final class StockLayoutCanvasController {
|
|||||||
unlockedHosts.chipHost,
|
unlockedHosts.chipHost,
|
||||||
unlockedHosts.statusHost,
|
unlockedHosts.statusHost,
|
||||||
unlockedHosts.notificationHost,
|
unlockedHosts.notificationHost,
|
||||||
activeScene);
|
activeScene,
|
||||||
ownedIconHostManager.setUnlockedHostsVisible(root, true);
|
lockedCarrierTextSource());
|
||||||
|
boolean shouldShowUnlockedHosts = unlockedScene || renderResult.shouldOwnLockscreen();
|
||||||
|
ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts);
|
||||||
|
if (shouldShowUnlockedHosts) {
|
||||||
|
ownedIconHostManager.bringUnlockedHostsToFront(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!unlockedScene) {
|
if (!unlockedScene) {
|
||||||
trackedUnlockedStockSurfaces.clear();
|
trackedUnlockedStockSurfaces.clear();
|
||||||
restoreTransientUnlockedSources();
|
restoreTransientUnlockedSources();
|
||||||
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
if (renderResult.shouldOwnLockscreen()) {
|
||||||
|
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
||||||
|
}
|
||||||
removeDebugOverlay(root);
|
removeDebugOverlay(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -421,6 +440,7 @@ final class StockLayoutCanvasController {
|
|||||||
private void trackAndHideLockedStatusBarIconSurfaces(View root) {
|
private void trackAndHideLockedStatusBarIconSurfaces(View root) {
|
||||||
walkTree(root, view -> {
|
walkTree(root, view -> {
|
||||||
if (isLockedStatusBarIconSurface(view)) {
|
if (isLockedStatusBarIconSurface(view)) {
|
||||||
|
rememberLockedCarrierTextSource(view);
|
||||||
trackedLockedStatusBarIconSurfaces.add(view);
|
trackedLockedStatusBarIconSurfaces.add(view);
|
||||||
hideView(view, isCarrierView(view));
|
hideView(view, isCarrierView(view));
|
||||||
}
|
}
|
||||||
@@ -509,13 +529,15 @@ final class StockLayoutCanvasController {
|
|||||||
}
|
}
|
||||||
String idName = resolveIdName(view);
|
String idName = resolveIdName(view);
|
||||||
String viewClassName = view.getClass().getName();
|
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 = viewClassName.contains("NotificationShelf");
|
||||||
boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName)
|
boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName)
|
||||||
|| "notification_icononly_area".equals(idName)
|
|| "notification_icononly_area".equals(idName)
|
||||||
|| "keyguard_icononly_container_view".equals(idName)
|
|| "keyguard_icononly_container_view".equals(idName)
|
||||||
|| "common_notification_widget".equals(idName);
|
|| "common_notification_widget".equals(idName);
|
||||||
if (!("system_icons".equals(idName)
|
if (!("system_icons".equals(idName)
|
||||||
|| idName.toLowerCase(java.util.Locale.ROOT).contains("carrier")
|
|| carrierSurface
|
||||||
|| "system_icon_area".equals(idName)
|
|| "system_icon_area".equals(idName)
|
||||||
|| "statusIcons".equals(idName)
|
|| "statusIcons".equals(idName)
|
||||||
|| "battery".equals(idName)
|
|| "battery".equals(idName)
|
||||||
@@ -537,6 +559,9 @@ final class StockLayoutCanvasController {
|
|||||||
|| className.contains("StatusBarWindowView")) {
|
|| className.contains("StatusBarWindowView")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (carrierSurface && className.contains("NotificationShadeWindowView")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (shelfSurface && className.contains("NotificationShadeWindowView")) {
|
if (shelfSurface && className.contains("NotificationShadeWindowView")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -568,6 +593,32 @@ final class StockLayoutCanvasController {
|
|||||||
|| className.contains("StatusBarWindowView");
|
|| className.contains("StatusBarWindowView");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void rememberLockedCarrierTextSource(View root) {
|
||||||
|
if (lockedCarrierTextSource() != null || root == null || !isCarrierView(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
walkTree(root, view -> {
|
||||||
|
if (lockedCarrierTextSource() != null || !(view instanceof TextView textView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String idName = resolveIdName(textView);
|
||||||
|
String className = textView.getClass().getName();
|
||||||
|
if ("keyguard_carrier_text".equals(idName) || className.contains("CarrierText")) {
|
||||||
|
lockedCarrierTextSource = new WeakReference<>(textView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView lockedCarrierTextSource() {
|
||||||
|
TextView source = lockedCarrierTextSource.get();
|
||||||
|
if (source == null || !source.isAttachedToWindow()) {
|
||||||
|
lockedCarrierTextSource.clear();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CharSequence text = source.getText();
|
||||||
|
return text != null && text.length() > 0 ? source : null;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldHideNonChipView(View view) {
|
private boolean shouldHideNonChipView(View view) {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -637,32 +688,54 @@ final class StockLayoutCanvasController {
|
|||||||
|| "ongoing_activity_capsule".equals(idName);
|
|| "ongoing_activity_capsule".equals(idName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markRootSizeBeforeDraw(View root) {
|
private void markRootGeometryBeforeDraw(View root) {
|
||||||
if (!isUnlockedStatusBarRoot(root)) {
|
if (!isUnlockedStatusBarRoot(root)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markRootSize(root, new RootSize(root.getWidth(), root.getHeight()));
|
markRootGeometry(root, rootGeometry(root));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isKnownRootSizeChanged(View root) {
|
private boolean resetUnlockedHostsForRootGeometryChange(View root) {
|
||||||
|
if (!isKnownRootGeometryChanged(root)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
markRootGeometryBeforeDraw(root);
|
||||||
|
unlockedIconRenderController.clearAll();
|
||||||
|
ownedIconHostManager.removeUnlockedHosts(root);
|
||||||
|
unlockedHostsByRoot.remove(root);
|
||||||
|
removeDebugOverlay(root);
|
||||||
|
root.requestLayout();
|
||||||
|
root.invalidate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isKnownRootGeometryChanged(View root) {
|
||||||
if (!isUnlockedStatusBarRoot(root)) {
|
if (!isUnlockedStatusBarRoot(root)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
RootSize previous = rootSizes.get(root);
|
RootGeometry previous = rootGeometries.get(root);
|
||||||
return previous != null
|
RootGeometry current = rootGeometry(root);
|
||||||
&& (previous.width != root.getWidth() || previous.height != root.getHeight());
|
return previous != null && !previous.equals(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markRootSize(View root, RootSize size) {
|
private void markRootGeometry(View root, RootGeometry geometry) {
|
||||||
if (size.width <= 0 || size.height <= 0) {
|
if (geometry.width <= 0 || geometry.height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RootSize previous = rootSizes.put(root, size);
|
RootGeometry previous = rootGeometries.put(root, geometry);
|
||||||
if (previous != null && !previous.equals(size)) {
|
if (previous != null && !previous.equals(geometry)) {
|
||||||
unlockedIconRenderController.beginRootTransition();
|
unlockedIconRenderController.beginRootTransition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RootGeometry rootGeometry(View root) {
|
||||||
|
int rotation = root != null && root.getDisplay() != null ? root.getDisplay().getRotation() : -1;
|
||||||
|
return new RootGeometry(
|
||||||
|
root != null ? root.getWidth() : 0,
|
||||||
|
root != null ? root.getHeight() : 0,
|
||||||
|
rotation);
|
||||||
|
}
|
||||||
|
|
||||||
private void enforceDirectAodControllerPolicy(Object controller) {
|
private void enforceDirectAodControllerPolicy(Object controller) {
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
return;
|
return;
|
||||||
@@ -758,7 +831,7 @@ final class StockLayoutCanvasController {
|
|||||||
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
|
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rootSizes.remove(root);
|
rootGeometries.remove(root);
|
||||||
unlockedIconRenderController.clearAll();
|
unlockedIconRenderController.clearAll();
|
||||||
ownedIconHostManager.removeUnlockedHosts(root);
|
ownedIconHostManager.removeUnlockedHosts(root);
|
||||||
}
|
}
|
||||||
@@ -805,7 +878,7 @@ final class StockLayoutCanvasController {
|
|||||||
if (!isUnlockedStatusBarRoot(root)) {
|
if (!isUnlockedStatusBarRoot(root)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (activeScene != null && activeScene.isUnlocked()) {
|
if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) {
|
||||||
UnlockedHosts cached = unlockedHostsByRoot.get(root);
|
UnlockedHosts cached = unlockedHostsByRoot.get(root);
|
||||||
if (areUnlockedHostsAttached(root, cached)) {
|
if (areUnlockedHostsAttached(root, cached)) {
|
||||||
return cached;
|
return cached;
|
||||||
@@ -942,7 +1015,7 @@ final class StockLayoutCanvasController {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RootSize(int width, int height) {
|
private record RootGeometry(int width, int height, int rotation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ public final class OwnedIconHostManager {
|
|||||||
setHostVisible(root, TAG_UNLOCKED_CHIP_HOST, visible);
|
setHostVisible(root, TAG_UNLOCKED_CHIP_HOST, visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void bringUnlockedHostsToFront(View root) {
|
||||||
|
bringHostToFront(root, TAG_UNLOCKED_STATUS_HOST);
|
||||||
|
bringHostToFront(root, TAG_UNLOCKED_NOTIFICATION_HOST);
|
||||||
|
bringHostToFront(root, TAG_UNLOCKED_CLOCK_HOST);
|
||||||
|
bringHostToFront(root, TAG_UNLOCKED_CARRIER_HOST);
|
||||||
|
bringHostToFront(root, TAG_UNLOCKED_CHIP_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isOwnedHost(View view) {
|
public boolean isOwnedHost(View view) {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -128,6 +136,16 @@ public final class OwnedIconHostManager {
|
|||||||
host.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
|
host.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bringHostToFront(View root, String tag) {
|
||||||
|
if (!(root instanceof ViewGroup parent) || tag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FrameLayout host = findDirectHost(parent, tag);
|
||||||
|
if (host != null) {
|
||||||
|
host.bringToFront();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void disableAncestorClipping(View view) {
|
private void disableAncestorClipping(View view) {
|
||||||
Object current = view;
|
Object current = view;
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
|
|||||||
@@ -196,6 +196,18 @@ final class RootAnchors {
|
|||||||
&& (clockContainer == null || isAttached(clockContainer));
|
&& (clockContainer == null || isAttached(clockContainer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RootAnchors withCarrierView(TextView carrierView) {
|
||||||
|
return new RootAnchors(
|
||||||
|
clockContainer,
|
||||||
|
clockView,
|
||||||
|
statusRoot,
|
||||||
|
statusEndContent,
|
||||||
|
statusIcons,
|
||||||
|
battery,
|
||||||
|
notificationRoot,
|
||||||
|
carrierView);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isAttached(View view) {
|
private static boolean isAttached(View view) {
|
||||||
return view != null && view.isAttachedToWindow();
|
return view != null && view.isAttachedToWindow();
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-4
@@ -57,7 +57,8 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
ViewGroup chipHost,
|
ViewGroup chipHost,
|
||||||
ViewGroup statusHost,
|
ViewGroup statusHost,
|
||||||
ViewGroup notificationHost,
|
ViewGroup notificationHost,
|
||||||
SceneKey scene
|
SceneKey scene,
|
||||||
|
TextView externalCarrierView
|
||||||
) {
|
) {
|
||||||
if (scene == null || (!scene.isUnlocked() && !scene.isLockscreen()) || !root.isAttachedToWindow()) {
|
if (scene == null || (!scene.isUnlocked() && !scene.isLockscreen()) || !root.isAttachedToWindow()) {
|
||||||
clearAll();
|
clearAll();
|
||||||
@@ -70,6 +71,18 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
layoutHostToRoot(root, statusHost);
|
layoutHostToRoot(root, statusHost);
|
||||||
layoutHostToRoot(root, notificationHost);
|
layoutHostToRoot(root, notificationHost);
|
||||||
RootAnchors anchors = sourceCollector.anchorsFor(root);
|
RootAnchors anchors = sourceCollector.anchorsFor(root);
|
||||||
|
if (scene.isLockscreen()
|
||||||
|
&& settings.layoutCarrierEnabledLockscreen
|
||||||
|
&& (anchors == null || anchors.carrierView == null)) {
|
||||||
|
anchors = sourceCollector.refreshAnchors(root);
|
||||||
|
}
|
||||||
|
if (scene.isLockscreen()
|
||||||
|
&& settings.layoutCarrierEnabledLockscreen
|
||||||
|
&& anchors != null
|
||||||
|
&& anchors.carrierView == null
|
||||||
|
&& externalCarrierView != null) {
|
||||||
|
anchors = anchors.withCarrierView(externalCarrierView);
|
||||||
|
}
|
||||||
CollectedIcons previousIcons = lastCollectedIconsByRoot.get(root);
|
CollectedIcons previousIcons = lastCollectedIconsByRoot.get(root);
|
||||||
LayoutInputSignature signature = sourceCollector.layoutInputSignature(
|
LayoutInputSignature signature = sourceCollector.layoutInputSignature(
|
||||||
root,
|
root,
|
||||||
@@ -84,7 +97,11 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
boolean chipSourcesChanged = previousSignature == null
|
boolean chipSourcesChanged = previousSignature == null
|
||||||
|| signature.chipSources != previousSignature.chipSources;
|
|| signature.chipSources != previousSignature.chipSources;
|
||||||
if (signature.equals(previousSignature)) {
|
if (signature.equals(previousSignature)) {
|
||||||
return new RenderResult(false, false);
|
return new RenderResult(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
!scene.isLockscreen() || lastLayoutPlanByRoot.get(root) != null);
|
||||||
}
|
}
|
||||||
lastLayoutSignatureByRoot.put(root, signature);
|
lastLayoutSignatureByRoot.put(root, signature);
|
||||||
Integer currentClockSourceGeometry = clockSourceGeometrySignature(
|
Integer currentClockSourceGeometry = clockSourceGeometrySignature(
|
||||||
@@ -156,6 +173,11 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
settings,
|
settings,
|
||||||
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings.clockMiddleCutoutGapPx));
|
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings.clockMiddleCutoutGapPx));
|
||||||
}
|
}
|
||||||
|
if (scene.isLockscreen() && clockEnabledForScene(settings, scene) && currentClockLayout == null) {
|
||||||
|
clearLockscreenHosts(clockHost, carrierHost, chipHost, statusHost, notificationHost);
|
||||||
|
lastLayoutPlanByRoot.remove(root);
|
||||||
|
return new RenderResult(false, stockSourcesChanged, chipSourcesChanged, false);
|
||||||
|
}
|
||||||
LayoutPlan layoutPlan = layoutPlanner.solve(
|
LayoutPlan layoutPlan = layoutPlanner.solve(
|
||||||
root,
|
root,
|
||||||
clockHost,
|
clockHost,
|
||||||
@@ -243,6 +265,20 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearLockscreenHosts(
|
||||||
|
ViewGroup clockHost,
|
||||||
|
ViewGroup carrierHost,
|
||||||
|
ViewGroup chipHost,
|
||||||
|
ViewGroup statusHost,
|
||||||
|
ViewGroup notificationHost
|
||||||
|
) {
|
||||||
|
clockRenderer.clear(clockHost);
|
||||||
|
carrierRenderer.clear(carrierHost);
|
||||||
|
chipRenderer.clear(chipHost);
|
||||||
|
statusRenderer.clear(statusHost);
|
||||||
|
notificationRenderer.clear(notificationHost);
|
||||||
|
}
|
||||||
|
|
||||||
private void layoutHostToRoot(View root, ViewGroup host) {
|
private void layoutHostToRoot(View root, ViewGroup host) {
|
||||||
if (root == null || host == null || root.getWidth() <= 0 || root.getHeight() <= 0) {
|
if (root == null || host == null || root.getWidth() <= 0 || root.getHeight() <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -817,10 +853,15 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
public record RenderResult(
|
public record RenderResult(
|
||||||
boolean layoutChanged,
|
boolean layoutChanged,
|
||||||
boolean stockSourcesChanged,
|
boolean stockSourcesChanged,
|
||||||
boolean chipSourcesChanged
|
boolean chipSourcesChanged,
|
||||||
|
boolean shouldOwnLockscreen
|
||||||
) {
|
) {
|
||||||
public RenderResult(boolean layoutChanged, boolean stockSourcesChanged) {
|
public RenderResult(boolean layoutChanged, boolean stockSourcesChanged) {
|
||||||
this(layoutChanged, stockSourcesChanged, stockSourcesChanged);
|
this(layoutChanged, stockSourcesChanged, stockSourcesChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderResult(boolean layoutChanged, boolean stockSourcesChanged, boolean chipSourcesChanged) {
|
||||||
|
this(layoutChanged, stockSourcesChanged, chipSourcesChanged, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-4
@@ -168,6 +168,7 @@ final class UnlockedLayoutPlanner {
|
|||||||
}
|
}
|
||||||
ArrayList<LayoutItemKind> itemOrder = orderedKinds(settings.layoutItemOrder);
|
ArrayList<LayoutItemKind> itemOrder = orderedKinds(settings.layoutItemOrder);
|
||||||
splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings);
|
splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings);
|
||||||
|
sortBandsForItemOrder(bands, itemOrder);
|
||||||
LayoutEdges edges = layoutEdges(root, settings, anchors);
|
LayoutEdges edges = layoutEdges(root, settings, anchors);
|
||||||
int itemPadding = Math.max(0, settings.layoutItemPaddingPx);
|
int itemPadding = Math.max(0, settings.layoutItemPaddingPx);
|
||||||
solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding);
|
solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding);
|
||||||
@@ -198,6 +199,30 @@ final class UnlockedLayoutPlanner {
|
|||||||
carrierPlacement);
|
carrierPlacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sortBandsForItemOrder(
|
||||||
|
ArrayList<UnlockedLayoutBand> bands,
|
||||||
|
ArrayList<LayoutItemKind> itemOrder
|
||||||
|
) {
|
||||||
|
if (bands == null || bands.size() < 2 || itemOrder == null || itemOrder.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ArrayList<UnlockedLayoutBand> originalOrder = new ArrayList<>(bands);
|
||||||
|
bands.sort((left, right) -> {
|
||||||
|
int orderComparison = Integer.compare(
|
||||||
|
orderIndex(itemOrder, left != null ? left.kind : null),
|
||||||
|
orderIndex(itemOrder, right != null ? right.kind : null));
|
||||||
|
if (orderComparison != 0) {
|
||||||
|
return orderComparison;
|
||||||
|
}
|
||||||
|
return Integer.compare(originalOrder.indexOf(left), originalOrder.indexOf(right));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int orderIndex(ArrayList<LayoutItemKind> itemOrder, LayoutItemKind kind) {
|
||||||
|
int index = itemOrder != null ? itemOrder.indexOf(kind) : -1;
|
||||||
|
return index >= 0 ? index : Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
private void solveWithPackedLayout(
|
private void solveWithPackedLayout(
|
||||||
View root,
|
View root,
|
||||||
LayoutEdges edges,
|
LayoutEdges edges,
|
||||||
@@ -495,19 +520,25 @@ final class UnlockedLayoutPlanner {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Bounds bounds = ViewGeometry.boundsRelativeTo(root, collectedIcons.carrierView);
|
Bounds bounds = ViewGeometry.boundsRelativeTo(root, collectedIcons.carrierView);
|
||||||
if (bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
Bounds anchorBounds = bounds != null
|
||||||
return null;
|
? bounds
|
||||||
|
: ViewGeometry.boundsRelativeTo(root, collectedIcons.clockView);
|
||||||
|
if (anchorBounds == null || anchorBounds.height <= 0) {
|
||||||
|
anchorBounds = new Bounds(0, 0, Math.max(0, root.getWidth()), Math.max(1, root.getHeight()));
|
||||||
}
|
}
|
||||||
|
int sourceHeight = collectedIcons.carrierView.getHeight() > 0
|
||||||
|
? collectedIcons.carrierView.getHeight()
|
||||||
|
: collectedIcons.carrierView.getMeasuredHeight();
|
||||||
int baseline = collectedIcons.carrierView.getBaseline() > 0
|
int baseline = collectedIcons.carrierView.getBaseline() > 0
|
||||||
? collectedIcons.carrierView.getBaseline()
|
? collectedIcons.carrierView.getBaseline()
|
||||||
: Math.max(0, (bounds.height * 3) / 4);
|
: Math.max(0, ((sourceHeight > 0 ? sourceHeight : anchorBounds.height) * 3) / 4);
|
||||||
int verticalOffset = offsetScaler.clock(
|
int verticalOffset = offsetScaler.clock(
|
||||||
settings.layoutCarrierVerticalOffsetPx,
|
settings.layoutCarrierVerticalOffsetPx,
|
||||||
collectedIcons.carrierView);
|
collectedIcons.carrierView);
|
||||||
return ClockLayout.simpleText(
|
return ClockLayout.simpleText(
|
||||||
collectedIcons.carrierView,
|
collectedIcons.carrierView,
|
||||||
positionFor(settings.layoutCarrierPosition),
|
positionFor(settings.layoutCarrierPosition),
|
||||||
bounds.top + baseline - verticalOffset);
|
anchorBounds.top + baseline - verticalOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int statusCountForScene(SbtSettings settings, SceneKey scene) {
|
private int statusCountForScene(SbtSettings settings, SceneKey scene) {
|
||||||
@@ -707,6 +738,7 @@ final class UnlockedLayoutPlanner {
|
|||||||
) {
|
) {
|
||||||
int left = firstUsableInset(
|
int left = firstUsableInset(
|
||||||
rootWidth,
|
rootWidth,
|
||||||
|
leftInset(root, anchors != null ? anchors.clockView : null),
|
||||||
leftInset(root, anchors != null ? anchors.clockContainer : null),
|
leftInset(root, anchors != null ? anchors.clockContainer : null),
|
||||||
leftInset(root, anchors != null ? anchors.notificationRoot : null),
|
leftInset(root, anchors != null ? anchors.notificationRoot : null),
|
||||||
root != null ? root.getPaddingLeft() : 0);
|
root != null ? root.getPaddingLeft() : 0);
|
||||||
|
|||||||
+4
@@ -28,6 +28,10 @@ final class UnlockedStockSourceCollector {
|
|||||||
if (cached != null && cached.isUsable()) {
|
if (cached != null && cached.isUsable()) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
return refreshAnchors(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
RootAnchors refreshAnchors(View root) {
|
||||||
RootAnchors anchors = discoverAnchors(root);
|
RootAnchors anchors = discoverAnchors(root);
|
||||||
anchorsByRoot.put(root, anchors);
|
anchorsByRoot.put(root, anchors);
|
||||||
return anchors;
|
return anchors;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package se.ajpanton.statusbartweak.shell.settings;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -862,60 +861,6 @@ public final class SbtSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void restoreCredentialProtectedPreferences(Context context) {
|
|
||||||
if (context == null || Build.VERSION.SDK_INT < 24) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Context deviceContext = context.createDeviceProtectedStorageContext();
|
|
||||||
if (deviceContext == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SharedPreferences devicePrefs = deviceContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
Map<String, ?> deviceValues = devicePrefs.getAll();
|
|
||||||
if (deviceValues == null || deviceValues.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SharedPreferences normalPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
Map<String, ?> normalValues = normalPrefs.getAll();
|
|
||||||
SharedPreferences.Editor editor = normalPrefs.edit();
|
|
||||||
boolean changed = false;
|
|
||||||
for (Map.Entry<String, ?> entry : deviceValues.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
if (key == null || normalValues.containsKey(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Object value = entry.getValue();
|
|
||||||
if (value instanceof Boolean bool) {
|
|
||||||
editor.putBoolean(key, bool);
|
|
||||||
} else if (value instanceof Integer integer) {
|
|
||||||
editor.putInt(key, integer);
|
|
||||||
} else if (value instanceof Long longValue) {
|
|
||||||
editor.putLong(key, longValue);
|
|
||||||
} else if (value instanceof Float floatValue) {
|
|
||||||
editor.putFloat(key, floatValue);
|
|
||||||
} else if (value instanceof String string) {
|
|
||||||
editor.putString(key, string);
|
|
||||||
} else if (value instanceof Set<?> set) {
|
|
||||||
HashSet<String> strings = new HashSet<>();
|
|
||||||
for (Object item : set) {
|
|
||||||
if (item instanceof String stringItem) {
|
|
||||||
strings.add(stringItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editor.putStringSet(key, strings);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
editor.commit();
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasSettingsProvider(Context context) {
|
private static boolean hasSettingsProvider(Context context) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ public class SbtSettingsProvider extends ContentProvider {
|
|||||||
if (ctx == null) {
|
if (ctx == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
SbtSettings.restoreCredentialProtectedPreferences(ctx);
|
|
||||||
SharedPreferences prefs = ctx.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = ctx.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
if (METHOD_GET_UNLOCKED.equals(method)) {
|
if (METHOD_GET_UNLOCKED.equals(method)) {
|
||||||
int value = prefs.getInt(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW,
|
int value = prefs.getInt(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW,
|
||||||
|
|||||||
@@ -9,21 +9,23 @@ import android.view.MenuItem;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
import com.google.android.material.navigation.NavigationView;
|
import com.google.android.material.navigation.NavigationView;
|
||||||
|
|
||||||
import se.ajpanton.statusbartweak.R;
|
import se.ajpanton.statusbartweak.R;
|
||||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
|
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
|
||||||
private static final String EXTRA_OPEN_NAV_ITEM = "open_nav_item";
|
private static final String EXTRA_OPEN_NAV_ITEM = "open_nav_item";
|
||||||
|
|
||||||
private DrawerLayout drawerLayout;
|
private SafeInsetDrawerLayout drawerLayout;
|
||||||
private NavigationView navigationView;
|
private NavigationView navigationView;
|
||||||
|
private Integer navigationBasePaddingLeft;
|
||||||
|
|
||||||
public static Intent newOpenDebugIntent(Context context) {
|
public static Intent newOpenDebugIntent(Context context) {
|
||||||
Intent intent = new Intent(context, MainActivity.class);
|
Intent intent = new Intent(context, MainActivity.class);
|
||||||
@@ -35,7 +37,6 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
SbtSettings.restoreCredentialProtectedPreferences(this);
|
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||||
@@ -45,6 +46,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
|||||||
navigationView = findViewById(R.id.nav_view);
|
navigationView = findViewById(R.id.nav_view);
|
||||||
navigationView.setNavigationItemSelectedListener(this);
|
navigationView.setNavigationItemSelectedListener(this);
|
||||||
navigationView.getMenu().setGroupCheckable(0, true, true);
|
navigationView.getMenu().setGroupCheckable(0, true, true);
|
||||||
|
applyNavigationDrawerInsets();
|
||||||
|
|
||||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
||||||
this,
|
this,
|
||||||
@@ -182,4 +184,41 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
|||||||
intent.removeExtra(EXTRA_OPEN_NAV_ITEM);
|
intent.removeExtra(EXTRA_OPEN_NAV_ITEM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyNavigationDrawerInsets() {
|
||||||
|
drawerLayout.addOnLayoutChangeListener((view, left, top, right, bottom,
|
||||||
|
oldLeft, oldTop, oldRight, oldBottom) ->
|
||||||
|
applyNavigationDrawerMarginsFromRootInsets());
|
||||||
|
drawerLayout.post(this::applyNavigationDrawerMarginsFromRootInsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyNavigationDrawerMarginsFromRootInsets() {
|
||||||
|
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(drawerLayout);
|
||||||
|
if (insets == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Insets safeInsets = insets.getInsetsIgnoringVisibility(
|
||||||
|
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||||
|
applyNavigationDrawerSafeArea(safeInsets.left, safeInsets.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyNavigationDrawerSafeArea(int leftInset, int rightInset) {
|
||||||
|
int safeLeft = Math.max(Math.max(0, leftInset), Math.max(0, drawerLayout.getPaddingLeft()));
|
||||||
|
int safeRight = Math.max(Math.max(0, rightInset), Math.max(0, drawerLayout.getPaddingRight()));
|
||||||
|
drawerLayout.setDrawerSafeInsets(safeLeft, safeRight);
|
||||||
|
if (navigationBasePaddingLeft == null) {
|
||||||
|
navigationBasePaddingLeft = navigationView.getPaddingLeft();
|
||||||
|
}
|
||||||
|
int leftPadding = navigationBasePaddingLeft + safeLeft;
|
||||||
|
if (navigationView.getPaddingLeft() != leftPadding
|
||||||
|
|| Math.round(navigationView.getTranslationX()) != 0) {
|
||||||
|
navigationView.setTranslationX(0f);
|
||||||
|
navigationView.setPadding(
|
||||||
|
leftPadding,
|
||||||
|
navigationView.getPaddingTop(),
|
||||||
|
navigationView.getPaddingRight(),
|
||||||
|
navigationView.getPaddingBottom());
|
||||||
|
navigationView.requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package se.ajpanton.statusbartweak.shell.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
public class SafeInsetDrawerLayout extends DrawerLayout {
|
||||||
|
private int safeInsetLeft;
|
||||||
|
private int safeInsetRight;
|
||||||
|
|
||||||
|
public SafeInsetDrawerLayout(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeInsetDrawerLayout(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeInsetDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawerSafeInsets(int left, int right) {
|
||||||
|
int safeLeft = Math.max(0, left);
|
||||||
|
int safeRight = Math.max(0, right);
|
||||||
|
if (safeInsetLeft == safeLeft && safeInsetRight == safeRight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
safeInsetLeft = safeLeft;
|
||||||
|
safeInsetRight = safeRight;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dispatchDraw(Canvas canvas) {
|
||||||
|
if (safeInsetLeft == 0 && safeInsetRight == 0) {
|
||||||
|
super.dispatchDraw(canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int save = canvas.save();
|
||||||
|
canvas.clipRect(
|
||||||
|
safeInsetLeft,
|
||||||
|
0,
|
||||||
|
Math.max(safeInsetLeft, getWidth() - safeInsetRight),
|
||||||
|
getHeight());
|
||||||
|
super.dispatchDraw(canvas);
|
||||||
|
canvas.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
<se.ajpanton.statusbartweak.shell.ui.SafeInsetDrawerLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
@@ -38,4 +38,4 @@
|
|||||||
app:itemTextColor="@color/drawer_item_text_color"
|
app:itemTextColor="@color/drawer_item_text_color"
|
||||||
app:menu="@menu/drawer_menu" />
|
app:menu="@menu/drawer_menu" />
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</se.ajpanton.statusbartweak.shell.ui.SafeInsetDrawerLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user