Let the activity embedded in ActivityView can be directly touched
The goal is to eliminate the latency caused by forwarding the event. In this CL, we put an input portal window behind an ActivityView, and subtract the touchable region of the the ActivityView, so that the user can touch through the portal window, and let the touch arrive on the embedded activity. Bug: 120675821 Test: Manual test with ActivityViewTest Test: atest CtsActivityManagerDeviceTestCases:ActivityViewTest Change-Id: I6776387b65cf6a7d2ea60cb00ffdd38be22081ac
This commit is contained in:
@@ -27,15 +27,12 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.IWindowManager;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceSession;
|
||||
@@ -50,9 +47,7 @@ import dalvik.system.CloseGuard;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity container that allows launching activities into itself and does input forwarding.
|
||||
* <p>Creation of this view is only allowed to callers who have
|
||||
* {@link android.Manifest.permission#INJECT_EVENTS} permission.
|
||||
* Activity container that allows launching activities into itself.
|
||||
* <p>Activity launching into this container is restricted by the same rules that apply to launching
|
||||
* on VirtualDisplays.
|
||||
* @hide
|
||||
@@ -75,9 +70,8 @@ public class ActivityView extends ViewGroup {
|
||||
private StateCallback mActivityViewCallback;
|
||||
|
||||
private IActivityTaskManager mActivityTaskManager;
|
||||
private IInputForwarder mInputForwarder;
|
||||
// Temp container to store view coordinates on screen.
|
||||
private final int[] mLocationOnScreen = new int[2];
|
||||
// Temp container to store view coordinates in window.
|
||||
private final int[] mLocationInWindow = new int[2];
|
||||
|
||||
private TaskStackListener mTaskStackListener;
|
||||
|
||||
@@ -264,7 +258,7 @@ public class ActivityView extends ViewGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
|
||||
* Triggers an update of {@link ActivityView}'s location in window to properly set touch exclude
|
||||
* regions and avoid focus switches by touches on this view.
|
||||
*/
|
||||
public void onLocationChanged() {
|
||||
@@ -279,45 +273,14 @@ public class ActivityView extends ViewGroup {
|
||||
/** Send current location and size to the WM to set tap exclude region for this view. */
|
||||
private void updateLocation() {
|
||||
try {
|
||||
getLocationOnScreen(mLocationOnScreen);
|
||||
getLocationInWindow(mLocationInWindow);
|
||||
WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
|
||||
mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
|
||||
mLocationInWindow[0], mLocationInWindow[1], getWidth(), getHeight());
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return injectInputEvent(event) || super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
|
||||
if (injectInputEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectInputEvent(MotionEvent event) {
|
||||
if (mInputForwarder != null) {
|
||||
try {
|
||||
// The touch event that the ActivityView gets is in View space, but the event needs
|
||||
// to get forwarded in screen space. This offsets the touch event by the location
|
||||
// the ActivityView is on screen and sends it to the input forwarder.
|
||||
getLocationOnScreen(mLocationOnScreen);
|
||||
event.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
|
||||
return mInputForwarder.forwardEvent(event);
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class SurfaceCallback implements SurfaceHolder.Callback {
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
||||
@@ -400,7 +363,6 @@ public class ActivityView extends ViewGroup {
|
||||
}
|
||||
|
||||
mTmpTransaction.show(mRootSurfaceControl).apply();
|
||||
mInputForwarder = InputManager.getInstance().createInputForwarder(displayId);
|
||||
mTaskStackListener = new TaskStackListenerImpl();
|
||||
try {
|
||||
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
|
||||
@@ -416,9 +378,6 @@ public class ActivityView extends ViewGroup {
|
||||
|
||||
mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
|
||||
|
||||
if (mInputForwarder != null) {
|
||||
mInputForwarder = null;
|
||||
}
|
||||
cleanTapExcludeRegion();
|
||||
|
||||
if (mTaskStackListener != null) {
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
package android.view;
|
||||
|
||||
import static android.view.Display.INVALID_DISPLAY;
|
||||
|
||||
import android.graphics.Region;
|
||||
import android.os.IBinder;
|
||||
import android.view.IWindow;
|
||||
import android.view.InputChannel;
|
||||
|
||||
/**
|
||||
* Functions as a handle for a window that can receive input.
|
||||
@@ -94,6 +94,10 @@ public final class InputWindowHandle {
|
||||
// Display this input is on.
|
||||
public int displayId;
|
||||
|
||||
// If this value is set to a valid display ID, it indicates this window is a portal which
|
||||
// transports the touch of this window to the display indicated by portalToDisplayId.
|
||||
public int portalToDisplayId = INVALID_DISPLAY;
|
||||
|
||||
private native void nativeDispose();
|
||||
|
||||
public InputWindowHandle(InputApplicationHandle inputApplicationHandle,
|
||||
|
||||
@@ -55,6 +55,7 @@ static struct {
|
||||
jfieldID ownerUid;
|
||||
jfieldID inputFeatures;
|
||||
jfieldID displayId;
|
||||
jfieldID portalToDisplayId;
|
||||
} gInputWindowHandleClassInfo;
|
||||
|
||||
static Mutex gHandleMutex;
|
||||
@@ -154,6 +155,8 @@ bool NativeInputWindowHandle::updateInfo() {
|
||||
gInputWindowHandleClassInfo.inputFeatures);
|
||||
mInfo.displayId = env->GetIntField(obj,
|
||||
gInputWindowHandleClassInfo.displayId);
|
||||
mInfo.portalToDisplayId = env->GetIntField(obj,
|
||||
gInputWindowHandleClassInfo.portalToDisplayId);
|
||||
|
||||
jobject inputApplicationHandleObj = env->GetObjectField(obj,
|
||||
gInputWindowHandleClassInfo.inputApplicationHandle);
|
||||
@@ -307,6 +310,9 @@ int register_android_server_InputWindowHandle(JNIEnv* env) {
|
||||
|
||||
GET_FIELD_ID(gInputWindowHandleClassInfo.displayId, clazz,
|
||||
"displayId", "I");
|
||||
|
||||
GET_FIELD_ID(gInputWindowHandleClassInfo.portalToDisplayId, clazz,
|
||||
"portalToDisplayId", "I");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
|
||||
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.view.Display.FLAG_PRIVATE;
|
||||
import static android.view.Display.INVALID_DISPLAY;
|
||||
import static android.view.InsetsState.TYPE_IME;
|
||||
import static android.view.InsetsState.TYPE_NAVIGATION_BAR;
|
||||
import static android.view.InsetsState.TYPE_TOP_BAR;
|
||||
@@ -46,6 +47,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
|
||||
import static android.view.WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE;
|
||||
import static android.view.WindowManager.LayoutParams.NEEDS_MENU_UNSET;
|
||||
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
|
||||
@@ -143,9 +145,11 @@ import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.Region.Op;
|
||||
import android.hardware.display.DisplayManagerInternal;
|
||||
import android.os.Binder;
|
||||
import android.os.Debug;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Trace;
|
||||
@@ -160,6 +164,7 @@ import android.view.DisplayInfo;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputChannel;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputWindowHandle;
|
||||
import android.view.InsetsState.InternalInsetType;
|
||||
import android.view.MagnificationSpec;
|
||||
import android.view.Surface;
|
||||
@@ -517,6 +522,9 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
|
||||
private final InsetsStateController mInsetsStateController;
|
||||
|
||||
private SurfaceControl mParentSurfaceControl;
|
||||
private InputWindowHandle mPortalWindowHandle;
|
||||
|
||||
// Last systemUiVisibility we received from status bar.
|
||||
private int mLastStatusBarVisibility = 0;
|
||||
// Last systemUiVisibility we dispatched to windows.
|
||||
@@ -2411,10 +2419,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
win.getTouchableRegion(mTmpRegion);
|
||||
mTouchExcludeRegion.op(mTmpRegion, Region.Op.UNION);
|
||||
}
|
||||
for (int i = mTapExcludeProvidingWindows.size() - 1; i >= 0; i--) {
|
||||
final WindowState win = mTapExcludeProvidingWindows.valueAt(i);
|
||||
win.amendTapExcludeRegion(mTouchExcludeRegion);
|
||||
}
|
||||
amendWindowTapExcludeRegion(mTouchExcludeRegion);
|
||||
// TODO(multi-display): Support docked stacks on secondary displays.
|
||||
if (mDisplayId == DEFAULT_DISPLAY && getSplitScreenPrimaryStack() != null) {
|
||||
mDividerControllerLocked.getTouchRegion(mTmpRect);
|
||||
@@ -2426,6 +2431,18 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Union the region with all the tap exclude region provided by windows on this display.
|
||||
*
|
||||
* @param inOutRegion The region to be amended.
|
||||
*/
|
||||
void amendWindowTapExcludeRegion(Region inOutRegion) {
|
||||
for (int i = mTapExcludeProvidingWindows.size() - 1; i >= 0; i--) {
|
||||
final WindowState win = mTapExcludeProvidingWindows.valueAt(i);
|
||||
win.amendTapExcludeRegion(inOutRegion);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void switchUser() {
|
||||
super.switchUser();
|
||||
@@ -3583,6 +3600,13 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
private void updateBounds() {
|
||||
calculateBounds(mDisplayInfo, mTmpBounds);
|
||||
setBounds(mTmpBounds);
|
||||
if (mPortalWindowHandle != null && mParentSurfaceControl != null) {
|
||||
mPortalWindowHandle.touchableRegion.getBounds(mTmpRect);
|
||||
if (!mTmpBounds.equals(mTmpRect)) {
|
||||
mPortalWindowHandle.touchableRegion.set(mTmpBounds);
|
||||
mPendingTransaction.setInputWindowInfo(mParentSurfaceControl, mPortalWindowHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determines the current display bounds based on the current state
|
||||
@@ -4827,15 +4851,43 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
|| mWmService.mForceDesktopModeOnExternalDisplays;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Re-parent the DisplayContent's top surfaces, {@link #mWindowingLayer} and
|
||||
* {@link #mOverlayLayer} to the specified surfaceControl.
|
||||
*
|
||||
* @param surfaceControlHandle The new SurfaceControl, where the DisplayContent's
|
||||
* surfaces will be re-parented to.
|
||||
* @param sc The new SurfaceControl, where the DisplayContent's surfaces will be re-parented to.
|
||||
*/
|
||||
void reparentDisplayContent(SurfaceControl sc) {
|
||||
mPendingTransaction.reparent(mWindowingLayer, sc)
|
||||
.reparent(mOverlayLayer, sc);
|
||||
mParentSurfaceControl = sc;
|
||||
if (mPortalWindowHandle == null) {
|
||||
mPortalWindowHandle = createPortalWindowHandle(sc.toString());
|
||||
}
|
||||
mPendingTransaction.setInputWindowInfo(sc, mPortalWindowHandle)
|
||||
.reparent(mWindowingLayer, sc).reparent(mOverlayLayer, sc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a portal window handle for input. This window transports any touch to the display
|
||||
* indicated by {@link InputWindowHandle#portalToDisplayId} if the touch hits this window.
|
||||
*
|
||||
* @param name The name of the portal window handle.
|
||||
* @return the new portal window handle.
|
||||
*/
|
||||
private InputWindowHandle createPortalWindowHandle(String name) {
|
||||
// Let surface flinger to set the display ID of this input window handle because we don't
|
||||
// know which display the parent surface control is on.
|
||||
final InputWindowHandle portalWindowHandle = new InputWindowHandle(
|
||||
null /* inputApplicationHandle */, null /* clientWindow */, INVALID_DISPLAY);
|
||||
portalWindowHandle.name = name;
|
||||
portalWindowHandle.token = new Binder();
|
||||
portalWindowHandle.layoutParamsFlags =
|
||||
FLAG_SPLIT_TOUCH | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL;
|
||||
getBounds(mTmpBounds);
|
||||
portalWindowHandle.touchableRegion.set(mTmpBounds);
|
||||
portalWindowHandle.scaleFactor = 1f;
|
||||
portalWindowHandle.ownerPid = Process.myPid();
|
||||
portalWindowHandle.ownerUid = Process.myUid();
|
||||
portalWindowHandle.portalToDisplayId = mDisplayId;
|
||||
return portalWindowHandle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ class TapExcludeRegionHolder {
|
||||
void amendRegion(Region region, Rect boundingRegion) {
|
||||
for (int i = mTapExcludeRects.size() - 1; i>= 0 ; --i) {
|
||||
final Rect rect = mTapExcludeRects.valueAt(i);
|
||||
rect.intersect(boundingRegion);
|
||||
if (boundingRegion != null) {
|
||||
rect.intersect(boundingRegion);
|
||||
}
|
||||
region.union(rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,18 @@ public class TaskTapPointerEventListener implements PointerEventListener {
|
||||
// method target window will lose the focus.
|
||||
return;
|
||||
}
|
||||
final Region windowTapExcludeRegion = Region.obtain();
|
||||
mDisplayContent.amendWindowTapExcludeRegion(windowTapExcludeRegion);
|
||||
if (windowTapExcludeRegion.contains(x, y)) {
|
||||
windowTapExcludeRegion.recycle();
|
||||
// The user is tapping on the window tap exclude region. We don't move this
|
||||
// display to top. A window tap exclude region, for example, may be set by an
|
||||
// ActivityView, and the region would match the bounds of both the ActivityView
|
||||
// and the virtual display in it. In this case, we would take the tap that is on
|
||||
// the embedded virtual display instead of this display.
|
||||
return;
|
||||
}
|
||||
windowTapExcludeRegion.recycle();
|
||||
WindowContainer parent = mDisplayContent.getParent();
|
||||
if (parent != null && parent.getTopChild() != mDisplayContent) {
|
||||
parent.positionChildAt(WindowContainer.POSITION_TOP, mDisplayContent,
|
||||
@@ -81,9 +93,6 @@ public class TaskTapPointerEventListener implements PointerEventListener {
|
||||
|
||||
@Override
|
||||
public void onPointerEvent(MotionEvent motionEvent) {
|
||||
if (motionEvent.getDisplayId() != getDisplayId()) {
|
||||
return;
|
||||
}
|
||||
switch (motionEvent.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
final int x = (int) motionEvent.getX();
|
||||
|
||||
@@ -6551,8 +6551,13 @@ public class WindowManagerService extends IWindowManager.Stub
|
||||
|
||||
/**
|
||||
* Update a tap exclude region with a rectangular area in the window identified by the provided
|
||||
* id. Touches on this region will not switch focus to this window. Passing an empty rect will
|
||||
* remove the area from the exclude region of this window.
|
||||
* id. Touches down on this region will not:
|
||||
* <ol>
|
||||
* <li>Switch focus to this window.</li>
|
||||
* <li>Move the display of this window to top.</li>
|
||||
* <li>Send the touch events to this window.</li>
|
||||
* </ol>
|
||||
* Passing an empty rect will remove the area from the exclude region of this window.
|
||||
*/
|
||||
void updateTapExcludeRegion(IWindow client, int regionId, int left, int top, int width,
|
||||
int height) {
|
||||
|
||||
@@ -536,7 +536,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
||||
private final Point mSurfacePosition = new Point();
|
||||
|
||||
/**
|
||||
* A region inside of this window to be excluded from touch-related focus switches.
|
||||
* A region inside of this window to be excluded from touch.
|
||||
*/
|
||||
private TapExcludeRegionHolder mTapExcludeRegionHolder;
|
||||
|
||||
@@ -2168,6 +2168,24 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
||||
}
|
||||
region.set(mTmpRect);
|
||||
cropRegionToStackBoundsIfNeeded(region);
|
||||
subtractTouchExcludeRegionIfNeeded(region);
|
||||
} else if (modal && mTapExcludeRegionHolder != null) {
|
||||
final Region touchExcludeRegion = Region.obtain();
|
||||
amendTapExcludeRegion(touchExcludeRegion);
|
||||
if (!touchExcludeRegion.isEmpty()) {
|
||||
// Remove touch modal because there are some areas that cannot be touched.
|
||||
flags |= FLAG_NOT_TOUCH_MODAL;
|
||||
// Give it a large touchable region at first because it was touch modal. The window
|
||||
// might be moved on the display, so the touchable region should be large enough to
|
||||
// ensure it covers the whole display, no matter where it is moved.
|
||||
getDisplayContent().getBounds(mTmpRect);
|
||||
final int dw = mTmpRect.width();
|
||||
final int dh = mTmpRect.height();
|
||||
region.set(-dw, -dh, dw + dw, dh + dh);
|
||||
// Subtract the area that cannot be touched.
|
||||
region.op(touchExcludeRegion, Region.Op.DIFFERENCE);
|
||||
}
|
||||
touchExcludeRegion.recycle();
|
||||
} else {
|
||||
// Not modal or full screen modal
|
||||
getTouchableRegion(region);
|
||||
@@ -2837,6 +2855,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
||||
}
|
||||
}
|
||||
cropRegionToStackBoundsIfNeeded(outRegion);
|
||||
subtractTouchExcludeRegionIfNeeded(outRegion);
|
||||
}
|
||||
|
||||
private void cropRegionToStackBoundsIfNeeded(Region region) {
|
||||
@@ -2854,6 +2873,22 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
||||
region.op(mTmpRect, Region.Op.INTERSECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this window has areas that cannot be touched, we subtract those areas from its touchable
|
||||
* region.
|
||||
*/
|
||||
private void subtractTouchExcludeRegionIfNeeded(Region touchableRegion) {
|
||||
if (mTapExcludeRegionHolder == null) {
|
||||
return;
|
||||
}
|
||||
final Region touchExcludeRegion = Region.obtain();
|
||||
amendTapExcludeRegion(touchExcludeRegion);
|
||||
if (!touchExcludeRegion.isEmpty()) {
|
||||
touchableRegion.op(touchExcludeRegion, Region.Op.DIFFERENCE);
|
||||
}
|
||||
touchExcludeRegion.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a focus change. Must be called with no locks held, and consistently
|
||||
* from the same serialized thread (such as dispatched from a handler).
|
||||
@@ -4716,11 +4751,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|
||||
mTapExcludeRegionHolder.updateRegion(regionId, left, top, width, height);
|
||||
// Trigger touch exclude region update on current display.
|
||||
currentDisplay.updateTouchExcludeRegion();
|
||||
// Trigger touchable region update for this window.
|
||||
currentDisplay.getInputMonitor().updateInputWindowsLw(true /* force */);
|
||||
}
|
||||
|
||||
/** Union the region with current tap exclude region that this window provides. */
|
||||
/**
|
||||
* Union the region with current tap exclude region that this window provides.
|
||||
*
|
||||
* @param region The region to be amended. It is on the screen coordinates.
|
||||
*/
|
||||
void amendTapExcludeRegion(Region region) {
|
||||
mTapExcludeRegionHolder.amendRegion(region, getBounds());
|
||||
final Region tempRegion = Region.obtain();
|
||||
mTmpRect.set(mWindowFrames.mFrame);
|
||||
mTmpRect.offsetTo(0, 0);
|
||||
mTapExcludeRegionHolder.amendRegion(tempRegion, mTmpRect);
|
||||
// The region held by the holder is on the window coordinates. We need to translate it to
|
||||
// the screen coordinates.
|
||||
tempRegion.translate(mWindowFrames.mFrame.left, mWindowFrames.mFrame.top);
|
||||
region.op(tempRegion, Region.Op.UNION);
|
||||
tempRegion.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user