MediaPlayer2: use protobuf instead of parcel in invoke()
Also fix MEDIA_INFO handling code to send the event to the client after handling internal update. (This fixes several cts test cases.) Test: pass MediaPlayer2Test Bug: 63934228 Change-Id: I5d4884353057a195b1f587694bfbf66cdf1fd23c
This commit is contained in:
@@ -679,6 +679,7 @@ java_library {
|
||||
|
||||
static_libs: [
|
||||
"framework-protos",
|
||||
"mediaplayer2-protos",
|
||||
"android.hidl.base-V1.0-java",
|
||||
"android.hardware.cas-V1.0-java",
|
||||
"android.hardware.contexthub-V1.0-java",
|
||||
|
||||
@@ -772,22 +772,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a generic method on the native player using opaque
|
||||
* parcels for the request and reply. Both payloads' format is a
|
||||
* convention between the java caller and the native player.
|
||||
* Must be called after setDataSource to make sure a native player
|
||||
* exists. On failure, a RuntimeException is thrown.
|
||||
*
|
||||
* @param request Parcel with the data for the extension. The
|
||||
* caller must use {@link #newRequest()} to get one.
|
||||
*
|
||||
* @param reply Output parcel with the data returned by the
|
||||
* native player.
|
||||
* {@hide}
|
||||
*/
|
||||
public void invoke(Parcel request, Parcel reply) { }
|
||||
|
||||
/**
|
||||
* Insert a task in the command queue to help the client to identify whether a batch
|
||||
* of commands has been finished. When this command is processed, a notification
|
||||
|
||||
@@ -25,6 +25,9 @@ import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.MediaPlayer2Proto;
|
||||
import android.media.MediaPlayer2Proto.PlayerMessage;
|
||||
import android.media.MediaPlayer2Proto.Value;
|
||||
import android.media.SubtitleController.Anchor;
|
||||
import android.media.SubtitleTrack.RenderingWidget;
|
||||
import android.net.Uri;
|
||||
@@ -49,6 +52,7 @@ import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import com.android.framework.protobuf.InvalidProtocolBufferException;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
@@ -72,6 +76,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -555,28 +560,30 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a generic method on the native player using opaque
|
||||
* parcels for the request and reply. Both payloads' format is a
|
||||
* Invoke a generic method on the native player using opaque protocol
|
||||
* buffer message for the request and reply. Both payloads' format is a
|
||||
* convention between the java caller and the native player.
|
||||
* Must be called after setDataSource or setPlaylist to make sure a native player
|
||||
* exists. On failure, a RuntimeException is thrown.
|
||||
*
|
||||
* @param request Parcel with the data for the extension. The
|
||||
* @param request PlayerMessage for the extension. The
|
||||
* caller must use {@link #newRequest()} to get one.
|
||||
*
|
||||
* @param reply Output parcel with the data returned by the
|
||||
* @return PlayerMessage with the data returned by the
|
||||
* native player.
|
||||
* {@hide}
|
||||
*/
|
||||
@Override
|
||||
public void invoke(Parcel request, Parcel reply) {
|
||||
int retcode = native_invoke(request, reply);
|
||||
reply.setDataPosition(0);
|
||||
if (retcode != 0) {
|
||||
throw new RuntimeException("failure code: " + retcode);
|
||||
private PlayerMessage invoke(PlayerMessage msg) {
|
||||
byte[] ret = _invoke(msg.toByteArray());
|
||||
if (ret == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return PlayerMessage.parseFrom(ret);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private native byte[] _invoke(byte[] request);
|
||||
|
||||
@Override
|
||||
public void notifyWhenCommandLabelReached(Object label) {
|
||||
addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
|
||||
@@ -683,16 +690,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
final String msg = "Scaling mode " + mode + " is not supported";
|
||||
throw new IllegalArgumentException(msg);
|
||||
}
|
||||
Parcel request = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
try {
|
||||
request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
|
||||
request.writeInt(mode);
|
||||
invoke(request, reply);
|
||||
} finally {
|
||||
request.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
PlayerMessage request = PlayerMessage.newBuilder()
|
||||
.addValues(Value.newBuilder()
|
||||
.setInt32Value(INVOKE_ID_SET_VIDEO_SCALE_MODE))
|
||||
.addValues(Value.newBuilder().setInt32Value(mode))
|
||||
.build();
|
||||
invoke(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1798,14 +1801,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
|
||||
private native void _setAuxEffectSendLevel(float level);
|
||||
|
||||
/*
|
||||
* @param request Parcel destinated to the media player.
|
||||
* @param reply[out] Parcel that will contain the reply.
|
||||
* @return The status code.
|
||||
*/
|
||||
private native final int native_invoke(Parcel request, Parcel reply);
|
||||
|
||||
|
||||
/*
|
||||
* @param update_only If true fetch only the set of metadata that have
|
||||
* changed since the last invocation of getMetadata.
|
||||
@@ -1886,18 +1881,18 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
final int mTrackType;
|
||||
final MediaFormat mFormat;
|
||||
|
||||
TrackInfoImpl(Parcel in) {
|
||||
mTrackType = in.readInt();
|
||||
// TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
|
||||
TrackInfoImpl(Iterator<Value> in) {
|
||||
mTrackType = in.next().getInt32Value();
|
||||
// TODO: build the full MediaFormat; currently we are using createSubtitleFormat
|
||||
// even for audio/video tracks, meaning we only set the mime and language.
|
||||
String mime = in.readString();
|
||||
String language = in.readString();
|
||||
String mime = in.next().getStringValue();
|
||||
String language = in.next().getStringValue();
|
||||
mFormat = MediaFormat.createSubtitleFormat(mime, language);
|
||||
|
||||
if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.next().getInt32Value());
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.next().getInt32Value());
|
||||
mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.next().getInt32Value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1952,23 +1947,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
out.append("}");
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to read a TrackInfoImpl from a Parcel.
|
||||
*/
|
||||
/* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR
|
||||
= new Parcelable.Creator<TrackInfoImpl>() {
|
||||
@Override
|
||||
public TrackInfoImpl createFromParcel(Parcel in) {
|
||||
return new TrackInfoImpl(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackInfoImpl[] newArray(int size) {
|
||||
return new TrackInfoImpl[size];
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// We would like domain specific classes with more informative names than the `first` and `second`
|
||||
@@ -2010,17 +1988,23 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
}
|
||||
|
||||
private TrackInfoImpl[] getInbandTrackInfoImpl() throws IllegalStateException {
|
||||
Parcel request = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
try {
|
||||
request.writeInt(INVOKE_ID_GET_TRACK_INFO);
|
||||
invoke(request, reply);
|
||||
TrackInfoImpl trackInfo[] = reply.createTypedArray(TrackInfoImpl.CREATOR);
|
||||
return trackInfo;
|
||||
} finally {
|
||||
request.recycle();
|
||||
reply.recycle();
|
||||
PlayerMessage request = PlayerMessage.newBuilder()
|
||||
.addValues(Value.newBuilder().setInt32Value(INVOKE_ID_GET_TRACK_INFO))
|
||||
.build();
|
||||
PlayerMessage response = invoke(request);
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
Iterator<Value> in = response.getValuesList().iterator();
|
||||
int size = in.next().getInt32Value();
|
||||
if (size == 0) {
|
||||
return null;
|
||||
}
|
||||
TrackInfoImpl trackInfo[] = new TrackInfoImpl[size];
|
||||
for (int i = 0; i < size; ++i) {
|
||||
trackInfo[i] = new TrackInfoImpl(in);
|
||||
}
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2481,26 +2465,24 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
}
|
||||
}
|
||||
|
||||
Parcel request = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
try {
|
||||
request.writeInt(INVOKE_ID_GET_SELECTED_TRACK);
|
||||
request.writeInt(trackType);
|
||||
invoke(request, reply);
|
||||
int inbandTrackIndex = reply.readInt();
|
||||
synchronized (mIndexTrackPairs) {
|
||||
for (int i = 0; i < mIndexTrackPairs.size(); i++) {
|
||||
Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
|
||||
if (p.first != null && p.first == inbandTrackIndex) {
|
||||
return i;
|
||||
}
|
||||
PlayerMessage request = PlayerMessage.newBuilder()
|
||||
.addValues(Value.newBuilder().setInt32Value(INVOKE_ID_GET_SELECTED_TRACK))
|
||||
.addValues(Value.newBuilder().setInt32Value(trackType))
|
||||
.build();
|
||||
PlayerMessage response = invoke(request);
|
||||
if (response == null) {
|
||||
return -1;
|
||||
}
|
||||
int inbandTrackIndex = response.getValues(0).getInt32Value();
|
||||
synchronized (mIndexTrackPairs) {
|
||||
for (int i = 0; i < mIndexTrackPairs.size(); i++) {
|
||||
Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
|
||||
if (p.first != null && p.first == inbandTrackIndex) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
} finally {
|
||||
request.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2617,16 +2599,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
|
||||
private void selectOrDeselectInbandTrack(int index, boolean select)
|
||||
throws IllegalStateException {
|
||||
Parcel request = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
try {
|
||||
request.writeInt(select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK);
|
||||
request.writeInt(index);
|
||||
invoke(request, reply);
|
||||
} finally {
|
||||
request.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
PlayerMessage request = PlayerMessage.newBuilder()
|
||||
.addValues(Value.newBuilder().setInt32Value(
|
||||
select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK))
|
||||
.addValues(Value.newBuilder().setInt32Value(index))
|
||||
.build();
|
||||
invoke(request);
|
||||
}
|
||||
|
||||
// Have to declare protected for finalize() since it is protected
|
||||
@@ -2935,20 +2913,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
|
||||
case MEDIA_INFO:
|
||||
{
|
||||
synchronized (mEventCbLock) {
|
||||
for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) {
|
||||
cb.first.execute(() -> cb.second.onInfo(
|
||||
mMediaPlayer, dsd, what, extra));
|
||||
}
|
||||
}
|
||||
|
||||
switch (msg.arg1) {
|
||||
case MEDIA_INFO_DATA_SOURCE_START:
|
||||
if (isCurrentSrcId) {
|
||||
prepareNextDataSource();
|
||||
}
|
||||
break;
|
||||
|
||||
case MEDIA_INFO_VIDEO_TRACK_LAGGING:
|
||||
Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
|
||||
break;
|
||||
@@ -2980,6 +2945,20 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
synchronized (mEventCbLock) {
|
||||
for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) {
|
||||
cb.first.execute(() -> cb.second.onInfo(
|
||||
mMediaPlayer, dsd, what, extra));
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.arg1 == MEDIA_INFO_DATA_SOURCE_START) {
|
||||
if (isCurrentSrcId) {
|
||||
prepareNextDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
// No real default action so far.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,8 +133,10 @@ cc_library_shared {
|
||||
"libmediaextractor",
|
||||
"libmediametrics",
|
||||
"libmediaplayer2",
|
||||
"libmediaplayer2-protos",
|
||||
"libmediautils",
|
||||
"libnetd_client",
|
||||
"libprotobuf-cpp-lite",
|
||||
"libstagefright_esds",
|
||||
"libstagefright_foundation",
|
||||
"libstagefright_httplive",
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
#include "android_util_Binder.h"
|
||||
#include <binder/Parcel.h>
|
||||
|
||||
#include "mediaplayer2.pb.h"
|
||||
|
||||
using android::media::MediaPlayer2Proto::PlayerMessage;
|
||||
|
||||
// Modular DRM begin
|
||||
#define FIND_CLASS(var, className) \
|
||||
var = env->FindClass(className); \
|
||||
@@ -196,6 +200,11 @@ void JNIMediaPlayer2Listener::notify(int64_t srcId, int msg, int ext1, int ext2,
|
||||
if (obj && obj->dataSize() > 0) {
|
||||
jobject jParcel = createJavaParcelObject(env);
|
||||
if (jParcel != NULL) {
|
||||
// TODO: replace the following Parcel usages with proto.
|
||||
// 1. MEDIA_DRM_INFO : DrmInfo
|
||||
// 2. MEDIA_TIMED_TEXT : TimedText
|
||||
// 2. MEDIA_SUBTITLE_DATA : SubtitleData
|
||||
// 4. MEDIA_META_DATA : TimedMetaData
|
||||
Parcel* nativeParcel = parcelForJavaObject(env, jParcel);
|
||||
nativeParcel->setData(obj->data(), obj->dataSize());
|
||||
env->CallStaticVoidMethod(mClass, fields.post_event, mObject,
|
||||
@@ -911,6 +920,9 @@ android_media_MediaPlayer2_setParameter(JNIEnv *env, jobject thiz, jint key, job
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: parcelForJavaObject() shouldn't be used since it's dependent on
|
||||
// framework's Parcel implementation. This setParameter() is used
|
||||
// only with AudioAttribute. Can this be used as jobject with JAudioTrack?
|
||||
Parcel *request = parcelForJavaObject(env, java_request);
|
||||
status_t err = mp->setParameter(key, *request);
|
||||
if (err == OK) {
|
||||
@@ -978,26 +990,35 @@ android_media_MediaPlayer2_setVolume(JNIEnv *env, jobject thiz, jfloat leftVolum
|
||||
process_media_player_call( env, thiz, mp->setVolume((float) leftVolume, (float) rightVolume), NULL, NULL );
|
||||
}
|
||||
|
||||
// Sends the request and reply parcels to the media player via the
|
||||
// binder interface.
|
||||
static jint
|
||||
android_media_MediaPlayer2_invoke(JNIEnv *env, jobject thiz,
|
||||
jobject java_request, jobject java_reply)
|
||||
{
|
||||
static jbyteArray
|
||||
android_media_MediaPlayer2_invoke(JNIEnv *env, jobject thiz, jbyteArray requestData) {
|
||||
sp<MediaPlayer2> media_player = getMediaPlayer(env, thiz);
|
||||
if (media_player == NULL ) {
|
||||
if (media_player == NULL) {
|
||||
jniThrowException(env, "java/lang/IllegalStateException", NULL);
|
||||
return UNKNOWN_ERROR;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Parcel *request = parcelForJavaObject(env, java_request);
|
||||
Parcel *reply = parcelForJavaObject(env, java_reply);
|
||||
// Get the byte[] pointer and data length.
|
||||
jbyte* pData = env->GetByteArrayElements(requestData, NULL);
|
||||
jsize pDataLen = env->GetArrayLength(requestData);
|
||||
|
||||
request->setDataPosition(0);
|
||||
// Deserialize from the byte stream.
|
||||
PlayerMessage request;
|
||||
PlayerMessage response;
|
||||
request.ParseFromArray(pData, pDataLen);
|
||||
|
||||
// Don't use process_media_player_call which use the async loop to
|
||||
// report errors, instead returns the status.
|
||||
return (jint) media_player->invoke(*request, reply);
|
||||
media_player->invoke(request, &response);
|
||||
|
||||
int size = response.ByteSize();
|
||||
jbyte* temp = new jbyte[size];
|
||||
response.SerializeToArray(temp, size);
|
||||
|
||||
// return the response as a byte array.
|
||||
jbyteArray out = env->NewByteArray(size);
|
||||
env->SetByteArrayRegion(out, 0, size, temp);
|
||||
delete[] temp;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Sends the new filter to the client.
|
||||
@@ -1512,7 +1533,7 @@ static const JNINativeMethod gMethods[] = {
|
||||
{"setLooping", "(Z)V", (void *)android_media_MediaPlayer2_setLooping},
|
||||
{"isLooping", "()Z", (void *)android_media_MediaPlayer2_isLooping},
|
||||
{"_setVolume", "(FF)V", (void *)android_media_MediaPlayer2_setVolume},
|
||||
{"native_invoke", "(Landroid/os/Parcel;Landroid/os/Parcel;)I",(void *)android_media_MediaPlayer2_invoke},
|
||||
{"_invoke", "([B)[B", (void *)android_media_MediaPlayer2_invoke},
|
||||
{"native_setMetadataFilter", "(Landroid/os/Parcel;)I", (void *)android_media_MediaPlayer2_setMetadataFilter},
|
||||
{"native_getMetadata", "(ZZLandroid/os/Parcel;)Z", (void *)android_media_MediaPlayer2_getMetadata},
|
||||
{"native_init", "()V", (void *)android_media_MediaPlayer2_native_init},
|
||||
|
||||
20
media/proto/Android.bp
Normal file
20
media/proto/Android.bp
Normal file
@@ -0,0 +1,20 @@
|
||||
java_library_static {
|
||||
name: "mediaplayer2-protos",
|
||||
host_supported: true,
|
||||
proto: {
|
||||
type: "lite",
|
||||
},
|
||||
srcs: ["mediaplayer2.proto"],
|
||||
no_framework_libs: true,
|
||||
jarjar_rules: "jarjar-rules.txt",
|
||||
}
|
||||
|
||||
cc_library_static {
|
||||
name: "libmediaplayer2-protos",
|
||||
host_supported: true,
|
||||
proto: {
|
||||
export_proto_headers: true,
|
||||
type: "lite",
|
||||
},
|
||||
srcs: ["mediaplayer2.proto"],
|
||||
}
|
||||
2
media/proto/jarjar-rules.txt
Normal file
2
media/proto/jarjar-rules.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
rule com.google.protobuf.** com.android.framework.protobuf.@1
|
||||
|
||||
53
media/proto/mediaplayer2.proto
Normal file
53
media/proto/mediaplayer2.proto
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.
|
||||
*/
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
|
||||
// C++ namespace: android::media:MediaPlayer2Proto:
|
||||
package android.media.MediaPlayer2Proto;
|
||||
|
||||
option java_package = "android.media";
|
||||
option java_outer_classname = "MediaPlayer2Proto";
|
||||
|
||||
message Value {
|
||||
// The kind of value.
|
||||
oneof kind {
|
||||
// Represents a boolean value.
|
||||
bool bool_value = 1;
|
||||
// Represents an int32 value.
|
||||
int32 int32_value = 2;
|
||||
// Represents an uint32 value.
|
||||
uint32 uint32_value = 3;
|
||||
// Represents an int64 value.
|
||||
int64 int64_value = 4;
|
||||
// Represents an uint64 value.
|
||||
uint64 uint64_value = 5;
|
||||
// Represents a float value.
|
||||
double float_value = 6;
|
||||
// Represents a double value.
|
||||
double double_value = 7;
|
||||
// Represents a string value.
|
||||
string string_value = 8;
|
||||
// Represents a bytes value.
|
||||
bytes bytes_value = 9;
|
||||
}
|
||||
}
|
||||
|
||||
message PlayerMessage {
|
||||
repeated Value values = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user