Merge "Remove clip adjustment to fix bottom screen flicker issue. Add a flicker test to verify animation background is no longer visible through the transition after the fix." into udc-qpr-dev am: 3dfd9e6c02 am: 6abf7ba9de
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23660587 Change-Id: Ie606e3be4a0d4a60597b8b85d1805c9028956f1a Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -50,7 +50,7 @@ class ActivityEmbeddingAnimationAdapter {
|
||||
final SurfaceControl mLeash;
|
||||
/** Area in absolute coordinate that the animation surface shouldn't go beyond. */
|
||||
@NonNull
|
||||
private final Rect mWholeAnimationBounds = new Rect();
|
||||
final Rect mWholeAnimationBounds = new Rect();
|
||||
/**
|
||||
* Area in absolute coordinate that should represent all the content to show for this window.
|
||||
* This should be the end bounds for opening window, and start bounds for closing window in case
|
||||
@@ -229,20 +229,7 @@ class ActivityEmbeddingAnimationAdapter {
|
||||
mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y);
|
||||
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
|
||||
t.setAlpha(mLeash, mTransformation.getAlpha());
|
||||
|
||||
// The following applies an inverse scale to the clip-rect so that it crops "after" the
|
||||
// scale instead of before.
|
||||
mVecs[1] = mVecs[2] = 0;
|
||||
mVecs[0] = mVecs[3] = 1;
|
||||
mTransformation.getMatrix().mapVectors(mVecs);
|
||||
mVecs[0] = 1.f / mVecs[0];
|
||||
mVecs[3] = 1.f / mVecs[3];
|
||||
final Rect clipRect = mTransformation.getClipRect();
|
||||
mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
|
||||
mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
|
||||
mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
|
||||
mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
|
||||
t.setCrop(mLeash, mRect);
|
||||
t.setWindowCrop(mLeash, mWholeAnimationBounds.width(), mWholeAnimationBounds.height());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.ClipRectAnimation;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
@@ -189,14 +188,6 @@ class ActivityEmbeddingAnimationSpec {
|
||||
startBounds.top - endBounds.top, 0);
|
||||
endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
|
||||
endSet.addAnimation(endTranslate);
|
||||
// The end leash is resizing, we should update the window crop based on the clip rect.
|
||||
final Rect startClip = new Rect(startBounds);
|
||||
final Rect endClip = new Rect(endBounds);
|
||||
startClip.offsetTo(0, 0);
|
||||
endClip.offsetTo(0, 0);
|
||||
final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
|
||||
clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
|
||||
endSet.addAnimation(clipAnim);
|
||||
endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
|
||||
parentBounds.height());
|
||||
endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
package com.android.server.wm.flicker.activityembedding
|
||||
|
||||
import android.tools.device.flicker.legacy.LegacyFlickerTest
|
||||
import android.platform.test.annotations.Presubmit
|
||||
import android.tools.common.traces.component.ComponentNameMatcher
|
||||
import com.android.server.wm.flicker.BaseTest
|
||||
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) {
|
||||
val testApp = ActivityEmbeddingAppHelper(instrumentation)
|
||||
@@ -29,4 +32,14 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(
|
||||
// The test should only be run on devices that support ActivityEmbedding.
|
||||
ActivityEmbeddingAppHelper.assumeActivityEmbeddingSupportedDevice()
|
||||
}
|
||||
|
||||
/** Asserts the background animation layer is never visible during bounds change transition. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun backgroundLayerNeverVisible() {
|
||||
val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
|
||||
flicker.assertLayers {
|
||||
isInvisible(backgroundColorLayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.wm.flicker.activityembedding
|
||||
|
||||
import android.platform.test.annotations.Presubmit
|
||||
import android.tools.common.datatypes.Rect
|
||||
import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
|
||||
import android.tools.device.flicker.legacy.FlickerBuilder
|
||||
import android.tools.device.flicker.legacy.LegacyFlickerTest
|
||||
import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
|
||||
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
|
||||
import androidx.test.filters.RequiresDevice
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
/**
|
||||
* Test changing split ratio at runtime on a horizona split.
|
||||
*
|
||||
* Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B
|
||||
* windows are equal in size. B is on the top and A is on the bottom.
|
||||
* Transitions:
|
||||
* Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B.
|
||||
*
|
||||
* To run this test: `atest FlickerTests:HorizontalSplitChangeRatioTest`
|
||||
*/
|
||||
@RequiresDevice
|
||||
@RunWith(Parameterized::class)
|
||||
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) :
|
||||
ActivityEmbeddingTestBase(flicker) {
|
||||
/** {@inheritDoc} */
|
||||
override val transition: FlickerBuilder.() -> Unit = {
|
||||
setup {
|
||||
tapl.setExpectedRotationCheckEnabled(false)
|
||||
testApp.launchViaIntent(wmHelper)
|
||||
testApp.launchSecondaryActivityHorizontally(wmHelper)
|
||||
startDisplayBounds =
|
||||
wmHelper.currentState.layerState.physicalDisplayBounds
|
||||
?: error("Display not found")
|
||||
}
|
||||
transitions {
|
||||
testApp.changeSecondaryActivityRatio(wmHelper)
|
||||
}
|
||||
teardown {
|
||||
tapl.goHome()
|
||||
testApp.exit(wmHelper)
|
||||
}
|
||||
}
|
||||
|
||||
/** Assert the Main activity window is always visible. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun mainActivityWindowIsAlwaysVisible() {
|
||||
flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
|
||||
}
|
||||
|
||||
/** Assert the Main activity window is always visible. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun mainActivityLayerIsAlwaysVisible() {
|
||||
flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
|
||||
}
|
||||
|
||||
/** Assert the Secondary activity window is always visible. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun secondaryActivityWindowIsAlwaysVisible() {
|
||||
flicker.assertWm {
|
||||
isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
|
||||
}
|
||||
|
||||
/** Assert the Secondary activity window is always visible. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun secondaryActivityLayerIsAlwaysVisible() {
|
||||
flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
|
||||
}
|
||||
|
||||
/** Assert the Main and Secondary activity change height during the transition. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun secondaryActivityAdjustsHeightRuntime() {
|
||||
flicker.assertLayersStart {
|
||||
val topLayerRegion =
|
||||
this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
|
||||
val bottomLayerRegion =
|
||||
this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
|
||||
// Compare dimensions of two splits, given we're using default split attributes,
|
||||
// both activities take up the same visible size on the display.
|
||||
check { "height" }
|
||||
.that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height)
|
||||
check { "width" }
|
||||
.that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
|
||||
topLayerRegion.notOverlaps(bottomLayerRegion.region)
|
||||
// Layers of two activities sum to be fullscreen size on display.
|
||||
topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
|
||||
}
|
||||
|
||||
flicker.assertLayersEnd {
|
||||
val topLayerRegion =
|
||||
this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
|
||||
val bottomLayerRegion =
|
||||
this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
|
||||
// Compare dimensions of two splits, given we're using default split attributes,
|
||||
// both activities take up the same visible size on the display.
|
||||
check { "height" }
|
||||
.that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height)
|
||||
check { "height" }
|
||||
.that(
|
||||
topLayerRegion.region.height / 0.3f -
|
||||
bottomLayerRegion.region.height / 0.7f)
|
||||
.isLower(0.1f)
|
||||
check { "width" }
|
||||
.that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
|
||||
topLayerRegion.notOverlaps(bottomLayerRegion.region)
|
||||
// Layers of two activities sum to be fullscreen size on display.
|
||||
topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** {@inheritDoc} */
|
||||
private var startDisplayBounds = Rect.EMPTY
|
||||
/**
|
||||
* Creates the test configurations.
|
||||
*
|
||||
* See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
|
||||
* navigation modes.
|
||||
*/
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
|
||||
}
|
||||
}
|
||||
@@ -142,14 +142,6 @@ class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) :
|
||||
}
|
||||
}
|
||||
|
||||
/** Assert the background animation layer is never visible during transition. */
|
||||
@Presubmit
|
||||
@Test
|
||||
fun backgroundLayerNeverVisible() {
|
||||
val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
|
||||
flicker.assertLayers { isInvisible(backgroundColorLayer) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** {@inheritDoc} */
|
||||
private var startDisplayBounds = Rect.EMPTY
|
||||
|
||||
@@ -45,18 +45,22 @@ constructor(
|
||||
* based on the split pair rule.
|
||||
*/
|
||||
fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
|
||||
val launchButton =
|
||||
uiDevice.wait(
|
||||
Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
|
||||
FIND_TIMEOUT
|
||||
)
|
||||
require(launchButton != null) { "Can't find launch secondary activity button on screen." }
|
||||
launchButton.click()
|
||||
wmHelper
|
||||
.StateSyncBuilder()
|
||||
.withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.waitForAndVerify()
|
||||
launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_button")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the button to launch the secondary activity in RTL, which should split with the main
|
||||
* activity based on the split pair rule.
|
||||
*/
|
||||
fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
|
||||
launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the button to launch the secondary activity in a horizontal split.
|
||||
*/
|
||||
fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) {
|
||||
launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button")
|
||||
}
|
||||
|
||||
/** Clicks the button to launch a third activity over a secondary activity. */
|
||||
@@ -101,16 +105,38 @@ constructor(
|
||||
*/
|
||||
fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
|
||||
val finishButton =
|
||||
uiDevice.wait(
|
||||
Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
|
||||
FIND_TIMEOUT
|
||||
)
|
||||
uiDevice.wait(
|
||||
Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
|
||||
FIND_TIMEOUT
|
||||
)
|
||||
require(finishButton != null) { "Can't find finish secondary activity button on screen." }
|
||||
finishButton.click()
|
||||
wmHelper
|
||||
.StateSyncBuilder()
|
||||
.withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
|
||||
.waitForAndVerify()
|
||||
.StateSyncBuilder()
|
||||
.withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
|
||||
.waitForAndVerify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the button to toggle the split ratio of secondary activity.
|
||||
*/
|
||||
fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) {
|
||||
val launchButton =
|
||||
uiDevice.wait(
|
||||
Until.findObject(
|
||||
By.res(getPackage(),
|
||||
"toggle_split_ratio_button")),
|
||||
FIND_TIMEOUT
|
||||
)
|
||||
require(launchButton != null) {
|
||||
"Can't find toggle ratio for secondary activity button on screen."
|
||||
}
|
||||
launchButton.click()
|
||||
wmHelper
|
||||
.StateSyncBuilder()
|
||||
.withAppTransitionIdle()
|
||||
.withTransitionSnapshotGone()
|
||||
.waitForAndVerify()
|
||||
}
|
||||
|
||||
fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) {
|
||||
@@ -149,25 +175,19 @@ constructor(
|
||||
.waitForAndVerify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the button to launch the secondary activity in RTL, which should split with the main
|
||||
* activity based on the split pair rule.
|
||||
*/
|
||||
fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
|
||||
private fun launchSecondaryActivityFromButton(
|
||||
wmHelper: WindowManagerStateHelper, buttonName: String) {
|
||||
val launchButton =
|
||||
uiDevice.wait(
|
||||
Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")),
|
||||
FIND_TIMEOUT
|
||||
)
|
||||
uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT)
|
||||
require(launchButton != null) {
|
||||
"Can't find launch secondary activity rtl button on screen."
|
||||
"Can't find launch secondary activity button : " + buttonName + "on screen."
|
||||
}
|
||||
launchButton.click()
|
||||
wmHelper
|
||||
.StateSyncBuilder()
|
||||
.withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.waitForAndVerify()
|
||||
.StateSyncBuilder()
|
||||
.withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
|
||||
.waitForAndVerify()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
android:tag="RIGHT_TO_LEFT"
|
||||
android:text="Launch Secondary Activity in RTL" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/launch_secondary_activity_horizontally_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:onClick="launchSecondaryActivity"
|
||||
android:tag="BOTTOM_TO_TOP"
|
||||
android:text="Launch Secondary Activity Horizontally" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/launch_placeholder_split_button"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
android:onClick="launchThirdActivity"
|
||||
android:text="Launch a third activity" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/toggle_split_ratio_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:textOn="Ratio 0.5"
|
||||
android:textOff="Ratio 0.3"
|
||||
android:checked="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/secondary_enter_pip_button"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.window.embedding.ActivityFilter;
|
||||
import androidx.window.embedding.ActivityRule;
|
||||
import androidx.window.embedding.EmbeddingAspectRatio;
|
||||
@@ -152,6 +153,9 @@ public class ActivityEmbeddingMainActivity extends Activity {
|
||||
if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) {
|
||||
return LayoutDirection.LEFT_TO_RIGHT;
|
||||
}
|
||||
if (layoutDirectionStr.equals(LayoutDirection.BOTTOM_TO_TOP.toString())) {
|
||||
return LayoutDirection.BOTTOM_TO_TOP;
|
||||
}
|
||||
if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) {
|
||||
return LayoutDirection.RIGHT_TO_LEFT;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ import android.app.PictureInPictureParams;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.window.embedding.SplitAttributes;
|
||||
import androidx.window.embedding.SplitAttributesCalculatorParams;
|
||||
import androidx.window.embedding.SplitController;
|
||||
|
||||
/**
|
||||
* Activity to be used as the secondary activity to split with
|
||||
@@ -29,18 +34,41 @@ import android.view.View;
|
||||
*/
|
||||
public class ActivityEmbeddingSecondaryActivity extends Activity {
|
||||
|
||||
private SplitController mSplitController;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_embedding_secondary_activity_layout);
|
||||
findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
|
||||
findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
});
|
||||
mSplitController = SplitController.getInstance(this);
|
||||
final ToggleButton splitRatio = findViewById(R.id.toggle_split_ratio_button);
|
||||
mSplitController.setSplitAttributesCalculator(params -> {
|
||||
return new SplitAttributes.Builder()
|
||||
.setSplitType(
|
||||
SplitAttributes.SplitType.ratio(
|
||||
splitRatio.isChecked() ? 0.7f : 0.5f)
|
||||
)
|
||||
.setLayoutDirection(
|
||||
params.getDefaultSplitAttributes()
|
||||
.getLayoutDirection())
|
||||
.build();
|
||||
});
|
||||
splitRatio.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// This triggers a recalcuation of splitatributes.
|
||||
mSplitController.invalidateTopVisibleSplitAttributes();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user