Merge "Show qr code in "Share your audio" dialog" into main
This commit is contained in:
@@ -18,6 +18,7 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -147,7 +148,7 @@ public class AudioSharingDialogFactory {
|
||||
/**
|
||||
* Sets the custom image of the dialog custom body.
|
||||
*
|
||||
* @param iconRes The text to be used for the title.
|
||||
* @param iconRes The iconRes to be used for the image.
|
||||
* @return This builder.
|
||||
*/
|
||||
@NonNull
|
||||
@@ -158,6 +159,20 @@ public class AudioSharingDialogFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom image of the dialog custom body.
|
||||
*
|
||||
* @param bitmap The bitmap to be used for the image.
|
||||
* @return This builder.
|
||||
*/
|
||||
@NonNull
|
||||
public AudioSharingDialogFactory.DialogBuilder setCustomImage(Bitmap bitmap) {
|
||||
ImageView image = mCustomBody.findViewById(R.id.description_image);
|
||||
image.setImageBitmap(bitmap);
|
||||
image.setVisibility(View.VISIBLE);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom message of the dialog custom body.
|
||||
*
|
||||
|
@@ -17,10 +17,13 @@
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE;
|
||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment.getQrCodeBitmap;
|
||||
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -48,6 +51,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
private static final String TAG = "AudioSharingDialog";
|
||||
|
||||
private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
|
||||
private static final String BUNDLE_KEY_BROADCAST_METADATA = "bundle_key_broadcast_metadata";
|
||||
|
||||
// The host creates an instance of this dialog fragment must implement this interface to receive
|
||||
// event callbacks.
|
||||
@@ -80,12 +84,14 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
*
|
||||
* @param host The Fragment this dialog will be hosted.
|
||||
* @param deviceItems The connected device items eligible for audio sharing.
|
||||
* @param metadata The audio sharing metadata, nullable.
|
||||
* @param listener The callback to handle the user action on this dialog.
|
||||
* @param eventData The eventData to log with for dialog onClick events.
|
||||
*/
|
||||
public static void show(
|
||||
@Nullable Fragment host,
|
||||
@NonNull List<AudioSharingDeviceItem> deviceItems,
|
||||
@Nullable BluetoothLeBroadcastMetadata metadata,
|
||||
@NonNull DialogEventListener listener,
|
||||
@NonNull Pair<Integer, Object>[] eventData) {
|
||||
if (host == null) {
|
||||
@@ -116,6 +122,9 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
Log.d(TAG, "Show up the dialog.");
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
|
||||
if (metadata != null) {
|
||||
bundle.putParcelable(BUNDLE_KEY_BROADCAST_METADATA, metadata);
|
||||
}
|
||||
AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment();
|
||||
dialogFrag.setArguments(bundle);
|
||||
dialogFrag.show(manager, TAG);
|
||||
@@ -150,7 +159,6 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
}
|
||||
if (deviceItems.isEmpty()) {
|
||||
builder.setTitle(R.string.audio_sharing_share_dialog_title)
|
||||
.setCustomImage(R.drawable.audio_sharing_guidance)
|
||||
.setCustomMessage(R.string.audio_sharing_dialog_connect_device_content)
|
||||
.setCustomPositiveButton(
|
||||
R.string.audio_sharing_pair_button_label,
|
||||
@@ -172,17 +180,29 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
launcher.setResultListener(sHost, SHARE_THEN_PAIR_REQUEST_CODE);
|
||||
}
|
||||
launcher.launch();
|
||||
})
|
||||
.setCustomNegativeButton(
|
||||
R.string.audio_sharing_qrcode_button_label,
|
||||
v -> {
|
||||
onCancelClick();
|
||||
new SubSettingLauncher(getContext())
|
||||
.setTitleRes(R.string.audio_streams_qr_code_page_title)
|
||||
.setDestination(AudioStreamsQrCodeFragment.class.getName())
|
||||
.setSourceMetricsCategory(getMetricsCategory())
|
||||
.launch();
|
||||
});
|
||||
BluetoothLeBroadcastMetadata metadata = arguments.getParcelable(
|
||||
BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class);
|
||||
Bitmap qrCodeBitmap = metadata == null ? null : getQrCodeBitmap(metadata,
|
||||
getContext()).orElse(null);
|
||||
if (qrCodeBitmap != null) {
|
||||
builder.setCustomImage(qrCodeBitmap)
|
||||
.setCustomNegativeButton(com.android.settings.R.string.cancel,
|
||||
v -> onCancelClick());
|
||||
} else {
|
||||
builder.setCustomImage(R.drawable.audio_sharing_guidance)
|
||||
.setCustomNegativeButton(
|
||||
R.string.audio_sharing_qrcode_button_label,
|
||||
v -> {
|
||||
onCancelClick();
|
||||
new SubSettingLauncher(getContext())
|
||||
.setTitleRes(R.string.audio_streams_qr_code_page_title)
|
||||
.setDestination(
|
||||
AudioStreamsQrCodeFragment.class.getName())
|
||||
.setSourceMetricsCategory(getMetricsCategory())
|
||||
.launch();
|
||||
});
|
||||
}
|
||||
} else if (deviceItems.size() == 1) {
|
||||
AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems);
|
||||
builder.setTitle(
|
||||
|
@@ -723,10 +723,16 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
cleanUpStatesForStartSharing();
|
||||
}
|
||||
};
|
||||
BluetoothLeBroadcastMetadata metadata = mBroadcast == null ? null
|
||||
: mBroadcast.getLatestBluetoothLeBroadcastMetadata();
|
||||
AudioSharingUtils.postOnMainThread(
|
||||
mContext,
|
||||
() -> AudioSharingDialogFragment.show(
|
||||
mFragment, mDeviceItemsForSharing, listener, eventData));
|
||||
mFragment,
|
||||
mDeviceItemsForSharing,
|
||||
metadata,
|
||||
listener,
|
||||
eventData));
|
||||
}
|
||||
|
||||
private void showErrorDialog() {
|
||||
|
@@ -18,6 +18,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -69,7 +70,7 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
|
||||
if (broadcastMetadata == null) {
|
||||
return;
|
||||
}
|
||||
Bitmap bm = getQrCodeBitmap(broadcastMetadata).orElse(null);
|
||||
Bitmap bm = getQrCodeBitmap(broadcastMetadata, getActivity()).orElse(null);
|
||||
if (bm == null) {
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +101,9 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
|
||||
/** Gets an optional bitmap from metadata. */
|
||||
public static Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata,
|
||||
Context context) {
|
||||
if (metadata == null) {
|
||||
Log.d(TAG, "getQrCodeBitmap: broadcastMetadata is empty!");
|
||||
return Optional.empty();
|
||||
@@ -113,7 +116,7 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
|
||||
Log.d(TAG, "getQrCodeBitmap: metadata : " + metadata);
|
||||
try {
|
||||
int qrcodeSize =
|
||||
getResources().getDimensionPixelSize(R.dimen.audio_streams_qrcode_size);
|
||||
context.getResources().getDimensionPixelSize(R.dimen.audio_streams_qrcode_size);
|
||||
Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
|
||||
return Optional.of(bitmap);
|
||||
} catch (WriterException e) {
|
||||
|
@@ -25,6 +25,7 @@ import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
@@ -44,6 +45,7 @@ import com.android.settings.R;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
|
||||
import org.junit.After;
|
||||
@@ -87,6 +89,11 @@ public class AudioSharingDialogFragmentTest {
|
||||
private static final Pair<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
|
||||
private static final Pair<Integer, Object>[] TEST_EVENT_DATA_LIST =
|
||||
new Pair[] {TEST_EVENT_DATA};
|
||||
private static final String METADATA_STR =
|
||||
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
|
||||
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
|
||||
private static final BluetoothLeBroadcastMetadata METADATA =
|
||||
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(METADATA_STR);
|
||||
|
||||
private Fragment mParent;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
@@ -123,7 +130,7 @@ public class AudioSharingDialogFragmentTest {
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
mParent, new ArrayList<>(), null, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNull();
|
||||
@@ -133,17 +140,18 @@ public class AudioSharingDialogFragmentTest {
|
||||
public void onCreateDialog_unattachedFragment_dialogNotExist() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingDialogFragment.show(
|
||||
new Fragment(), new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
new Fragment(), new ArrayList<>(), null, EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOn_noExtraConnectedDevice() {
|
||||
public void onCreateDialog_flagOn_qrCodeBitmapNull_noExtraConnectedDevice() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
mParent, new ArrayList<>(), null, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -176,6 +184,7 @@ public class AudioSharingDialogFragmentTest {
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
null,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onPositiveClick() {
|
||||
@@ -201,12 +210,13 @@ public class AudioSharingDialogFragmentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_noExtraConnectedDevice_showQRCode() {
|
||||
public void onCreateDialog_noExtraConnectedDevice_showQRCodeButton() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AtomicBoolean isQrCodeBtnClicked = new AtomicBoolean(false);
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
null,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
@@ -219,6 +229,8 @@ public class AudioSharingDialogFragmentTest {
|
||||
assertThat(dialog).isNotNull();
|
||||
Button qrCodeBtn = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(qrCodeBtn).isNotNull();
|
||||
assertThat(qrCodeBtn.getText().toString())
|
||||
.isEqualTo(mParent.getString(R.string.audio_sharing_qrcode_button_label));
|
||||
qrCodeBtn.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
@@ -231,12 +243,47 @@ public class AudioSharingDialogFragmentTest {
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_noExtraConnectedDevice_hasMetadata_showCancelButton() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AtomicBoolean isCancelBtnClicked = new AtomicBoolean(false);
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
METADATA,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
isCancelBtnClicked.set(true);
|
||||
}
|
||||
},
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
ImageView image = dialog.findViewById(R.id.description_image);
|
||||
assertThat(image).isNotNull();
|
||||
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(cancelBtn).isNotNull();
|
||||
cancelBtn.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
assertThat(isCancelBtnClicked.get()).isTrue();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOn_singleExtraConnectedDevice() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
AudioSharingDialogFragment.show(mParent, list, null, EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -273,7 +320,8 @@ public class AudioSharingDialogFragmentTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
AudioSharingDialogFragment.show(mParent, list, null, EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -300,6 +348,7 @@ public class AudioSharingDialogFragmentTest {
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
list,
|
||||
null,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onItemClick(@NonNull AudioSharingDeviceItem item) {
|
||||
@@ -332,7 +381,8 @@ public class AudioSharingDialogFragmentTest {
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
list.add(TEST_DEVICE_ITEM2);
|
||||
list.add(TEST_DEVICE_ITEM3);
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
AudioSharingDialogFragment.show(mParent, list, null, EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -371,6 +421,7 @@ public class AudioSharingDialogFragmentTest {
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
list,
|
||||
null,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
|
@@ -79,6 +79,7 @@ import com.android.settings.widget.SettingsMainSwitchBar;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.bluetooth.BluetoothCallback;
|
||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
|
||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
|
||||
@@ -543,6 +544,12 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
public void onBroadcastMetadataChanged_singleActiveDevice_showJoinAudioSharingDialog() {
|
||||
FeatureFlagUtils.setEnabled(
|
||||
mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
|
||||
String metadataStr =
|
||||
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
|
||||
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
|
||||
// Use real metadata as the dialog will display a qr code image
|
||||
BluetoothLeBroadcastMetadata realMetadata =
|
||||
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(metadataStr);
|
||||
when(mBtnView.isEnabled()).thenReturn(true);
|
||||
when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2));
|
||||
when(mAssistant.getAllSources(any(BluetoothDevice.class))).thenReturn(ImmutableList.of());
|
||||
@@ -556,8 +563,8 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
AudioSharingProgressDialogFragment.class.getName());
|
||||
|
||||
when(mBroadcast.isEnabled(null)).thenReturn(true);
|
||||
when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
|
||||
mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
|
||||
when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(realMetadata);
|
||||
mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, realMetadata);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
|
Reference in New Issue
Block a user