Merge "Notify TaskFragmentOrganizer onConfigurationChanged" into sc-v2-dev am: 3a00e2b213
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15084726 Change-Id: I78f66328370c32f8a14a0e51c77b87d528f4cc9e
This commit is contained in:
@@ -331,11 +331,10 @@ public class TaskInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@code true} if parameters that are important for task organizers have changed
|
* Returns {@code true} if the parameters that are important for task organizers are equal
|
||||||
* and {@link com.android.server.wm.TaskOrginizerController} needs to notify listeners
|
* between this {@link TaskInfo} and {@param that}.
|
||||||
* about that.
|
* @hide
|
||||||
* @hide
|
*/
|
||||||
*/
|
|
||||||
public boolean equalsForTaskOrganizer(@Nullable TaskInfo that) {
|
public boolean equalsForTaskOrganizer(@Nullable TaskInfo that) {
|
||||||
if (that == null) {
|
if (that == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
package android.window;
|
package android.window;
|
||||||
|
|
||||||
|
import static android.app.WindowConfiguration.WindowingMode;
|
||||||
|
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -94,6 +97,28 @@ public final class TaskFragmentInfo implements Parcelable {
|
|||||||
return mIsVisible;
|
return mIsVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WindowingMode
|
||||||
|
public int getWindowingMode() {
|
||||||
|
return mConfiguration.windowConfiguration.getWindowingMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the parameters that are important for task fragment organizers are
|
||||||
|
* equal between this {@link TaskFragmentInfo} and {@param that}.
|
||||||
|
*/
|
||||||
|
public boolean equalsForTaskFragmentOrganizer(@Nullable TaskFragmentInfo that) {
|
||||||
|
if (that == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mFragmentToken.equals(that.mFragmentToken)
|
||||||
|
&& mInitialComponentName.equals(that.mInitialComponentName)
|
||||||
|
&& mToken.equals(that.mToken)
|
||||||
|
&& mIsEmpty == that.mIsEmpty
|
||||||
|
&& mIsVisible == that.mIsVisible
|
||||||
|
&& getWindowingMode() == that.getWindowingMode();
|
||||||
|
}
|
||||||
|
|
||||||
private TaskFragmentInfo(Parcel in) {
|
private TaskFragmentInfo(Parcel in) {
|
||||||
mFragmentToken = in.readStrongBinder();
|
mFragmentToken = in.readStrongBinder();
|
||||||
mInitialComponentName = in.readTypedObject(ComponentName.CREATOR);
|
mInitialComponentName = in.readTypedObject(ComponentName.CREATOR);
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
|||||||
final ActivityTaskManagerService mAtmService;
|
final ActivityTaskManagerService mAtmService;
|
||||||
final ActivityTaskSupervisor mTaskSupervisor;
|
final ActivityTaskSupervisor mTaskSupervisor;
|
||||||
final RootWindowContainer mRootWindowContainer;
|
final RootWindowContainer mRootWindowContainer;
|
||||||
|
private final TaskFragmentOrganizerController mTaskFragmentOrganizerController;
|
||||||
|
|
||||||
// TODO(b/189384393): this is not set in TaskFragment so far. It should be passed from the
|
// TODO(b/189384393): this is not set in TaskFragment so far. It should be passed from the
|
||||||
// parent task when adding to the hierarchy
|
// parent task when adding to the hierarchy
|
||||||
@@ -272,6 +273,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
|||||||
mTaskSupervisor = atmService.mTaskSupervisor;
|
mTaskSupervisor = atmService.mTaskSupervisor;
|
||||||
mRootWindowContainer = mAtmService.mRootWindowContainer;
|
mRootWindowContainer = mAtmService.mRootWindowContainer;
|
||||||
mCreatedByOrganizer = createdByOrganizer;
|
mCreatedByOrganizer = createdByOrganizer;
|
||||||
|
mTaskFragmentOrganizerController =
|
||||||
|
mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAdjacentTaskFragment(TaskFragment taskFragment) {
|
void setAdjacentTaskFragment(TaskFragment taskFragment) {
|
||||||
@@ -1966,6 +1969,34 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
|||||||
return getTopChild().getActivityType();
|
return getTopChild().getActivityType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Configuration newParentConfig) {
|
||||||
|
super.onConfigurationChanged(newParentConfig);
|
||||||
|
|
||||||
|
if (mTaskFragmentOrganizer != null) {
|
||||||
|
// Parent config may have changed. The controller will check if there is any important
|
||||||
|
// config change for the organizer.
|
||||||
|
mTaskFragmentOrganizerController
|
||||||
|
.onTaskFragmentParentInfoChanged(mTaskFragmentOrganizer, this);
|
||||||
|
mTaskFragmentOrganizerController
|
||||||
|
.onTaskFragmentInfoChanged(mTaskFragmentOrganizer, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(b/190433129) call when TaskFragment is created from WCT#createTaskFragment
|
||||||
|
private void sendTaskFragmentAppeared() {
|
||||||
|
if (mTaskFragmentOrganizer != null) {
|
||||||
|
mTaskFragmentOrganizerController.onTaskFragmentAppeared(mTaskFragmentOrganizer, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(b/190433129) call when TaskFragment is removed from WCT#deleteTaskFragment
|
||||||
|
private void sendTaskFragmentVanished() {
|
||||||
|
if (mTaskFragmentOrganizer != null) {
|
||||||
|
mTaskFragmentOrganizerController.onTaskFragmentVanished(mTaskFragmentOrganizer, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link TaskFragmentInfo} with information from this TaskFragment. Should not be
|
* Returns a {@link TaskFragmentInfo} with information from this TaskFragment. Should not be
|
||||||
* called from {@link Task}.
|
* called from {@link Task}.
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
package com.android.server.wm;
|
package com.android.server.wm;
|
||||||
|
|
||||||
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
|
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
|
||||||
|
import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
|
||||||
|
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
@@ -27,11 +29,13 @@ import android.view.SurfaceControl;
|
|||||||
import android.window.ITaskFragmentOrganizer;
|
import android.window.ITaskFragmentOrganizer;
|
||||||
import android.window.ITaskFragmentOrganizerController;
|
import android.window.ITaskFragmentOrganizerController;
|
||||||
import android.window.TaskFragmentAppearedInfo;
|
import android.window.TaskFragmentAppearedInfo;
|
||||||
|
import android.window.TaskFragmentInfo;
|
||||||
|
|
||||||
import com.android.internal.protolog.common.ProtoLog;
|
import com.android.internal.protolog.common.ProtoLog;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores and manages the client {@link android.window.TaskFragmentOrganizer}.
|
* Stores and manages the client {@link android.window.TaskFragmentOrganizer}.
|
||||||
@@ -43,6 +47,10 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
|
|||||||
private final WindowManagerGlobalLock mGlobalLock;
|
private final WindowManagerGlobalLock mGlobalLock;
|
||||||
private final Set<ITaskFragmentOrganizer> mOrganizers = new ArraySet<>();
|
private final Set<ITaskFragmentOrganizer> mOrganizers = new ArraySet<>();
|
||||||
private final Map<ITaskFragmentOrganizer, DeathRecipient> mDeathRecipients = new ArrayMap<>();
|
private final Map<ITaskFragmentOrganizer, DeathRecipient> mDeathRecipients = new ArrayMap<>();
|
||||||
|
private final Map<TaskFragment, TaskFragmentInfo> mLastSentTaskFragmentInfos =
|
||||||
|
new WeakHashMap<>();
|
||||||
|
private final Map<TaskFragment, Configuration> mLastSentTaskFragmentParentConfigs =
|
||||||
|
new WeakHashMap<>();
|
||||||
|
|
||||||
private class DeathRecipient implements IBinder.DeathRecipient {
|
private class DeathRecipient implements IBinder.DeathRecipient {
|
||||||
final ITaskFragmentOrganizer mOrganizer;
|
final ITaskFragmentOrganizer mOrganizer;
|
||||||
@@ -118,43 +126,75 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
||||||
|
validateOrganizer(organizer);
|
||||||
|
|
||||||
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment appeared name=%s", tf.getName());
|
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment appeared name=%s", tf.getName());
|
||||||
|
final TaskFragmentInfo info = tf.getTaskFragmentInfo();
|
||||||
|
final SurfaceControl outSurfaceControl = new SurfaceControl(tf.getSurfaceControl(),
|
||||||
|
"TaskFragmentOrganizerController.onTaskFragmentInfoAppeared");
|
||||||
try {
|
try {
|
||||||
final SurfaceControl outSurfaceControl = new SurfaceControl(tf.getSurfaceControl(),
|
|
||||||
"TaskFragmentOrganizerController.onTaskFragmentInfoAppeared");
|
|
||||||
organizer.onTaskFragmentAppeared(
|
organizer.onTaskFragmentAppeared(
|
||||||
new TaskFragmentAppearedInfo(tf.getTaskFragmentInfo(), outSurfaceControl));
|
new TaskFragmentAppearedInfo(info, outSurfaceControl));
|
||||||
|
mLastSentTaskFragmentInfos.put(tf, info);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// Oh well...
|
// Oh well...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTaskFragmentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
void onTaskFragmentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
||||||
|
validateOrganizer(organizer);
|
||||||
|
|
||||||
|
// Check if the info is different from the last reported info.
|
||||||
|
final TaskFragmentInfo info = tf.getTaskFragmentInfo();
|
||||||
|
final TaskFragmentInfo lastInfo = mLastSentTaskFragmentInfos.get(tf);
|
||||||
|
if (info.equalsForTaskFragmentOrganizer(lastInfo) && configurationsAreEqualForOrganizer(
|
||||||
|
info.getConfiguration(), lastInfo.getConfiguration())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment info changed name=%s", tf.getName());
|
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment info changed name=%s", tf.getName());
|
||||||
try {
|
try {
|
||||||
organizer.onTaskFragmentInfoChanged(tf.getTaskFragmentInfo());
|
organizer.onTaskFragmentInfoChanged(tf.getTaskFragmentInfo());
|
||||||
|
mLastSentTaskFragmentInfos.put(tf, info);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// Oh well...
|
// Oh well...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTaskFragmentVanished(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
void onTaskFragmentVanished(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
||||||
|
validateOrganizer(organizer);
|
||||||
|
|
||||||
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment vanished name=%s", tf.getName());
|
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment vanished name=%s", tf.getName());
|
||||||
try {
|
try {
|
||||||
organizer.onTaskFragmentVanished(tf.getTaskFragmentInfo());
|
organizer.onTaskFragmentVanished(tf.getTaskFragmentInfo());
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// Oh well...
|
// Oh well...
|
||||||
}
|
}
|
||||||
|
mLastSentTaskFragmentInfos.remove(tf);
|
||||||
|
mLastSentTaskFragmentParentConfigs.remove(tf);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTaskFragmentParentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
void onTaskFragmentParentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
|
||||||
|
validateOrganizer(organizer);
|
||||||
|
|
||||||
|
// Check if the parent info is different from the last reported parent info.
|
||||||
|
if (tf.getParent() == null || tf.getParent().asTask() == null) {
|
||||||
|
mLastSentTaskFragmentParentConfigs.remove(tf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final Task parent = tf.getParent().asTask();
|
final Task parent = tf.getParent().asTask();
|
||||||
|
final Configuration parentConfig = parent.getConfiguration();
|
||||||
|
final Configuration lastParentConfig = mLastSentTaskFragmentParentConfigs.get(tf);
|
||||||
|
if (configurationsAreEqualForOrganizer(parentConfig, lastParentConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
|
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
|
||||||
"TaskFragment parent info changed name=%s parentTaskId=%d",
|
"TaskFragment parent info changed name=%s parentTaskId=%d",
|
||||||
tf.getName(), parent.mTaskId);
|
tf.getName(), parent.mTaskId);
|
||||||
try {
|
try {
|
||||||
organizer.onTaskFragmentParentInfoChanged(
|
organizer.onTaskFragmentParentInfoChanged(tf.getFragmentToken(), parentConfig);
|
||||||
tf.getFragmentToken(), parent.getConfiguration());
|
mLastSentTaskFragmentParentConfigs.put(tf, parentConfig);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
// Oh well...
|
// Oh well...
|
||||||
}
|
}
|
||||||
@@ -167,4 +207,17 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
|
|||||||
}
|
}
|
||||||
// TODO(b/190432728) move child activities of organized TaskFragment to leaf Task
|
// TODO(b/190432728) move child activities of organized TaskFragment to leaf Task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure that the organizer has been correctly registered to prevent any Sidecar
|
||||||
|
* implementation from organizing {@link TaskFragment} without registering first. In such case,
|
||||||
|
* we wouldn't register {@link DeathRecipient} for the organizer, and might not remove the
|
||||||
|
* {@link TaskFragment} after the organizer process died.
|
||||||
|
*/
|
||||||
|
private void validateOrganizer(ITaskFragmentOrganizer organizer) {
|
||||||
|
if (!mOrganizers.contains(organizer)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"TaskFragmentOrganizer has not been registered. Organizer=" + organizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,13 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANI
|
|||||||
import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
|
import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
|
||||||
import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
|
import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
|
||||||
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
|
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
|
||||||
import static com.android.server.wm.WindowOrganizerController.CONTROLLABLE_CONFIGS;
|
import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
|
||||||
import static com.android.server.wm.WindowOrganizerController.CONTROLLABLE_WINDOW_CONFIGS;
|
|
||||||
|
|
||||||
import android.annotation.Nullable;
|
import android.annotation.Nullable;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.app.ActivityManager.RunningTaskInfo;
|
import android.app.ActivityManager.RunningTaskInfo;
|
||||||
import android.app.WindowConfiguration;
|
import android.app.WindowConfiguration;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.content.pm.ParceledListSlice;
|
import android.content.pm.ParceledListSlice;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
@@ -69,13 +67,6 @@ import java.util.function.Consumer;
|
|||||||
class TaskOrganizerController extends ITaskOrganizerController.Stub {
|
class TaskOrganizerController extends ITaskOrganizerController.Stub {
|
||||||
private static final String TAG = "TaskOrganizerController";
|
private static final String TAG = "TaskOrganizerController";
|
||||||
|
|
||||||
/**
|
|
||||||
* Masks specifying which configurations are important to report back to an organizer when
|
|
||||||
* changed.
|
|
||||||
*/
|
|
||||||
private static final int REPORT_CONFIGS = CONTROLLABLE_CONFIGS;
|
|
||||||
private static final int REPORT_WINDOW_CONFIGS = CONTROLLABLE_WINDOW_CONFIGS;
|
|
||||||
|
|
||||||
// The set of modes that are currently supports
|
// The set of modes that are currently supports
|
||||||
// TODO: Remove once the task organizer can support all modes
|
// TODO: Remove once the task organizer can support all modes
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -790,18 +781,9 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub {
|
|||||||
mTmpTaskInfo.configuration.unset();
|
mTmpTaskInfo.configuration.unset();
|
||||||
task.fillTaskInfo(mTmpTaskInfo);
|
task.fillTaskInfo(mTmpTaskInfo);
|
||||||
|
|
||||||
boolean changed = !mTmpTaskInfo.equalsForTaskOrganizer(lastInfo);
|
boolean changed = !mTmpTaskInfo.equalsForTaskOrganizer(lastInfo)
|
||||||
if (!changed) {
|
|| !configurationsAreEqualForOrganizer(
|
||||||
int cfgChanges = mTmpTaskInfo.configuration.diff(lastInfo.configuration);
|
mTmpTaskInfo.configuration, lastInfo.configuration);
|
||||||
final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0
|
|
||||||
? (int) mTmpTaskInfo.configuration.windowConfiguration.diff(
|
|
||||||
lastInfo.configuration.windowConfiguration,
|
|
||||||
true /* compareUndefined */) : 0;
|
|
||||||
if ((winCfgChanges & REPORT_WINDOW_CONFIGS) == 0) {
|
|
||||||
cfgChanges &= ~ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
|
|
||||||
}
|
|
||||||
changed = (cfgChanges & REPORT_CONFIGS) != 0;
|
|
||||||
}
|
|
||||||
if (!(changed || force)) {
|
if (!(changed || force)) {
|
||||||
// mTmpTaskInfo will be reused next time.
|
// mTmpTaskInfo will be reused next time.
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -847,6 +847,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the configuration changes are important to report back to an organizer. */
|
||||||
|
static boolean configurationsAreEqualForOrganizer(
|
||||||
|
Configuration newConfig, @Nullable Configuration oldConfig) {
|
||||||
|
if (oldConfig == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int cfgChanges = newConfig.diff(oldConfig);
|
||||||
|
final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0
|
||||||
|
? (int) newConfig.windowConfiguration.diff(oldConfig.windowConfiguration,
|
||||||
|
true /* compareUndefined */) : 0;
|
||||||
|
if ((winCfgChanges & CONTROLLABLE_WINDOW_CONFIGS) == 0) {
|
||||||
|
cfgChanges &= ~ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
|
||||||
|
}
|
||||||
|
return (cfgChanges & CONTROLLABLE_CONFIGS) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void enforceTaskPermission(String func) {
|
private void enforceTaskPermission(String func) {
|
||||||
mService.enforceTaskPermission(func);
|
mService.enforceTaskPermission(func);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user