mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Change MediaMetadata update priority to favour MediaItem values.
The static and dynamic metadata now build up in a list, such that when the MediaMetadata is built, they are applied in an event order. This means that newer/fresher values will overwrite older ones. The MediaItem values are then applied at the end, as they take priority over any other. #minor-release PiperOrigin-RevId: 405383177
This commit is contained in:
parent
2b97455a8c
commit
cd6c2e989f
@ -25,6 +25,9 @@
|
|||||||
* Fix `mediaMetadata` being reset when media is repeated
|
* Fix `mediaMetadata` being reset when media is repeated
|
||||||
([#9458](https://github.com/google/ExoPlayer/issues/9458)).
|
([#9458](https://github.com/google/ExoPlayer/issues/9458)).
|
||||||
* Remove final dependency on `jcenter()`.
|
* Remove final dependency on `jcenter()`.
|
||||||
|
* Adjust `ExoPlayer` `MediaMetadata` update priority, such that values
|
||||||
|
input through the `MediaItem.MediaMetadata` are used above media
|
||||||
|
derived values.
|
||||||
* Video:
|
* Video:
|
||||||
* Fix bug in `MediaCodecVideoRenderer` that resulted in re-using a
|
* Fix bug in `MediaCodecVideoRenderer` that resulted in re-using a
|
||||||
released `Surface` when playing without an app-provided `Surface`
|
released `Surface` when playing without an app-provided `Surface`
|
||||||
|
@ -383,6 +383,108 @@ public final class MediaMetadata implements Bundleable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
|
||||||
|
public Builder populate(@Nullable MediaMetadata mediaMetadata) {
|
||||||
|
if (mediaMetadata == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (mediaMetadata.title != null) {
|
||||||
|
setTitle(mediaMetadata.title);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.artist != null) {
|
||||||
|
setArtist(mediaMetadata.artist);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.albumTitle != null) {
|
||||||
|
setAlbumTitle(mediaMetadata.albumTitle);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.albumArtist != null) {
|
||||||
|
setAlbumArtist(mediaMetadata.albumArtist);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.displayTitle != null) {
|
||||||
|
setDisplayTitle(mediaMetadata.displayTitle);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.subtitle != null) {
|
||||||
|
setSubtitle(mediaMetadata.subtitle);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.description != null) {
|
||||||
|
setDescription(mediaMetadata.description);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.mediaUri != null) {
|
||||||
|
setMediaUri(mediaMetadata.mediaUri);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.userRating != null) {
|
||||||
|
setUserRating(mediaMetadata.userRating);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.overallRating != null) {
|
||||||
|
setOverallRating(mediaMetadata.overallRating);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.artworkData != null) {
|
||||||
|
setArtworkData(mediaMetadata.artworkData, mediaMetadata.artworkDataType);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.artworkUri != null) {
|
||||||
|
setArtworkUri(mediaMetadata.artworkUri);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.trackNumber != null) {
|
||||||
|
setTrackNumber(mediaMetadata.trackNumber);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.totalTrackCount != null) {
|
||||||
|
setTotalTrackCount(mediaMetadata.totalTrackCount);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.folderType != null) {
|
||||||
|
setFolderType(mediaMetadata.folderType);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.isPlayable != null) {
|
||||||
|
setIsPlayable(mediaMetadata.isPlayable);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.year != null) {
|
||||||
|
setRecordingYear(mediaMetadata.year);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.recordingYear != null) {
|
||||||
|
setRecordingYear(mediaMetadata.recordingYear);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.recordingMonth != null) {
|
||||||
|
setRecordingMonth(mediaMetadata.recordingMonth);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.recordingDay != null) {
|
||||||
|
setRecordingDay(mediaMetadata.recordingDay);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.releaseYear != null) {
|
||||||
|
setReleaseYear(mediaMetadata.releaseYear);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.releaseMonth != null) {
|
||||||
|
setReleaseMonth(mediaMetadata.releaseMonth);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.releaseDay != null) {
|
||||||
|
setReleaseDay(mediaMetadata.releaseDay);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.writer != null) {
|
||||||
|
setWriter(mediaMetadata.writer);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.composer != null) {
|
||||||
|
setComposer(mediaMetadata.composer);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.conductor != null) {
|
||||||
|
setConductor(mediaMetadata.conductor);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.discNumber != null) {
|
||||||
|
setDiscNumber(mediaMetadata.discNumber);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.totalDiscCount != null) {
|
||||||
|
setTotalDiscCount(mediaMetadata.totalDiscCount);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.genre != null) {
|
||||||
|
setGenre(mediaMetadata.genre);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.compilation != null) {
|
||||||
|
setCompilation(mediaMetadata.compilation);
|
||||||
|
}
|
||||||
|
if (mediaMetadata.extras != null) {
|
||||||
|
setExtras(mediaMetadata.extras);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns a new {@link MediaMetadata} instance with the current builder values. */
|
/** Returns a new {@link MediaMetadata} instance with the current builder values. */
|
||||||
public MediaMetadata build() {
|
public MediaMetadata build() {
|
||||||
return new MediaMetadata(/* builder= */ this);
|
return new MediaMetadata(/* builder= */ this);
|
||||||
|
@ -2065,7 +2065,9 @@ public interface Player {
|
|||||||
*
|
*
|
||||||
* <p>This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the
|
* <p>This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the
|
||||||
* static and dynamic metadata from the {@link TrackSelection#getFormat(int) track selections'
|
* static and dynamic metadata from the {@link TrackSelection#getFormat(int) track selections'
|
||||||
* formats} and {@link Listener#onMetadata(Metadata)}.
|
* formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in the {@link
|
||||||
|
* MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or
|
||||||
|
* dynamic metadata.
|
||||||
*/
|
*/
|
||||||
MediaMetadata getMediaMetadata();
|
MediaMetadata getMediaMetadata();
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@ import org.junit.runner.RunWith;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class MediaMetadataTest {
|
public class MediaMetadataTest {
|
||||||
|
|
||||||
|
private static final String EXTRAS_KEY = "exampleKey";
|
||||||
|
private static final String EXTRAS_VALUE = "exampleValue";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void builder_minimal_correctDefaults() {
|
public void builder_minimal_correctDefaults() {
|
||||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
|
||||||
@ -91,41 +94,62 @@ public class MediaMetadataTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void roundTripViaBundle_yieldsEqualInstance() {
|
public void populate_populatesEveryField() {
|
||||||
Bundle extras = new Bundle();
|
MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata();
|
||||||
extras.putString("exampleKey", "exampleValue");
|
MediaMetadata populated = new MediaMetadata.Builder().populate(mediaMetadata).build();
|
||||||
|
|
||||||
MediaMetadata mediaMetadata =
|
// If this assertion fails, it's likely that a field is not being updated in
|
||||||
new MediaMetadata.Builder()
|
// MediaMetadata.Builder#populate(MediaMetadata).
|
||||||
.setTitle("title")
|
assertThat(populated).isEqualTo(mediaMetadata);
|
||||||
.setAlbumArtist("the artist")
|
assertThat(populated.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
|
||||||
.setMediaUri(Uri.parse("https://www.google.com"))
|
}
|
||||||
.setUserRating(new HeartRating(false))
|
|
||||||
.setOverallRating(new PercentageRating(87.4f))
|
@Test
|
||||||
.setArtworkData(
|
public void roundTripViaBundle_yieldsEqualInstance() {
|
||||||
new byte[] {-88, 12, 3, 2, 124, -54, -33, 69}, MediaMetadata.PICTURE_TYPE_MEDIA)
|
MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata();
|
||||||
.setTrackNumber(4)
|
|
||||||
.setTotalTrackCount(12)
|
|
||||||
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
|
|
||||||
.setIsPlayable(true)
|
|
||||||
.setRecordingYear(2000)
|
|
||||||
.setRecordingMonth(11)
|
|
||||||
.setRecordingDay(23)
|
|
||||||
.setReleaseYear(2001)
|
|
||||||
.setReleaseMonth(1)
|
|
||||||
.setReleaseDay(2)
|
|
||||||
.setComposer("Composer")
|
|
||||||
.setConductor("Conductor")
|
|
||||||
.setWriter("Writer")
|
|
||||||
.setDiscNumber(1)
|
|
||||||
.setTotalDiscCount(3)
|
|
||||||
.setGenre("Pop")
|
|
||||||
.setCompilation("Amazing songs.")
|
|
||||||
.setExtras(extras) // Extras is not implemented in MediaMetadata.equals(Object o).
|
|
||||||
.build();
|
|
||||||
|
|
||||||
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
|
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
|
||||||
assertThat(fromBundle).isEqualTo(mediaMetadata);
|
assertThat(fromBundle).isEqualTo(mediaMetadata);
|
||||||
assertThat(fromBundle.extras.getString("exampleKey")).isEqualTo("exampleValue");
|
// Extras is not implemented in MediaMetadata.equals(Object o).
|
||||||
|
assertThat(fromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaMetadata getFullyPopulatedMediaMetadata() {
|
||||||
|
Bundle extras = new Bundle();
|
||||||
|
extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
|
||||||
|
|
||||||
|
return new MediaMetadata.Builder()
|
||||||
|
.setTitle("title")
|
||||||
|
.setArtist("artist")
|
||||||
|
.setAlbumTitle("album title")
|
||||||
|
.setAlbumArtist("album artist")
|
||||||
|
.setDisplayTitle("display title")
|
||||||
|
.setSubtitle("subtitle")
|
||||||
|
.setDescription("description")
|
||||||
|
.setMediaUri(Uri.parse("https://www.google.com"))
|
||||||
|
.setUserRating(new HeartRating(false))
|
||||||
|
.setOverallRating(new PercentageRating(87.4f))
|
||||||
|
.setArtworkData(
|
||||||
|
new byte[] {-88, 12, 3, 2, 124, -54, -33, 69}, MediaMetadata.PICTURE_TYPE_MEDIA)
|
||||||
|
.setArtworkUri(Uri.parse("https://www.google.com"))
|
||||||
|
.setTrackNumber(4)
|
||||||
|
.setTotalTrackCount(12)
|
||||||
|
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
|
||||||
|
.setIsPlayable(true)
|
||||||
|
.setRecordingYear(2000)
|
||||||
|
.setRecordingMonth(11)
|
||||||
|
.setRecordingDay(23)
|
||||||
|
.setReleaseYear(2001)
|
||||||
|
.setReleaseMonth(1)
|
||||||
|
.setReleaseDay(2)
|
||||||
|
.setComposer("Composer")
|
||||||
|
.setConductor("Conductor")
|
||||||
|
.setWriter("Writer")
|
||||||
|
.setDiscNumber(1)
|
||||||
|
.setTotalDiscCount(3)
|
||||||
|
.setGenre("Pop")
|
||||||
|
.setCompilation("Amazing songs.")
|
||||||
|
.setExtras(extras)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
|||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||||
@ -111,6 +112,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
private MediaMetadata mediaMetadata;
|
private MediaMetadata mediaMetadata;
|
||||||
private MediaMetadata playlistMetadata;
|
private MediaMetadata playlistMetadata;
|
||||||
|
|
||||||
|
// MediaMetadata built from static (TrackGroup Format) and dynamic (onMetadata(Metadata)) metadata
|
||||||
|
// sources.
|
||||||
|
private MediaMetadata staticAndDynamicMediaMetadata;
|
||||||
|
|
||||||
// Playback information when there is no pending seek/set source operation.
|
// Playback information when there is no pending seek/set source operation.
|
||||||
private PlaybackInfo playbackInfo;
|
private PlaybackInfo playbackInfo;
|
||||||
|
|
||||||
@ -229,6 +234,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
.build();
|
.build();
|
||||||
mediaMetadata = MediaMetadata.EMPTY;
|
mediaMetadata = MediaMetadata.EMPTY;
|
||||||
playlistMetadata = MediaMetadata.EMPTY;
|
playlistMetadata = MediaMetadata.EMPTY;
|
||||||
|
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
|
||||||
maskingWindowIndex = C.INDEX_UNSET;
|
maskingWindowIndex = C.INDEX_UNSET;
|
||||||
playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
|
playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
|
||||||
playbackInfoUpdateListener =
|
playbackInfoUpdateListener =
|
||||||
@ -986,8 +992,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onMetadata(Metadata metadata) {
|
public void onMetadata(Metadata metadata) {
|
||||||
MediaMetadata newMediaMetadata =
|
staticAndDynamicMediaMetadata =
|
||||||
mediaMetadata.buildUpon().populateFromMetadata(metadata).build();
|
staticAndDynamicMediaMetadata.buildUpon().populateFromMetadata(metadata).build();
|
||||||
|
|
||||||
|
MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata();
|
||||||
|
|
||||||
if (newMediaMetadata.equals(mediaMetadata)) {
|
if (newMediaMetadata.equals(mediaMetadata)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1235,12 +1244,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
.windowIndex;
|
.windowIndex;
|
||||||
mediaItem = newPlaybackInfo.timeline.getWindow(windowIndex, window).mediaItem;
|
mediaItem = newPlaybackInfo.timeline.getWindow(windowIndex, window).mediaItem;
|
||||||
}
|
}
|
||||||
newMediaMetadata = mediaItem != null ? mediaItem.mediaMetadata : MediaMetadata.EMPTY;
|
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
|
||||||
}
|
}
|
||||||
if (mediaItemTransitioned
|
if (mediaItemTransitioned
|
||||||
|| !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
|| !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
||||||
newMediaMetadata =
|
staticAndDynamicMediaMetadata =
|
||||||
newMediaMetadata.buildUpon().populateFromMetadata(newPlaybackInfo.staticMetadata).build();
|
staticAndDynamicMediaMetadata
|
||||||
|
.buildUpon()
|
||||||
|
.populateFromMetadata(newPlaybackInfo.staticMetadata)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
newMediaMetadata = buildUpdatedMediaMetadata();
|
||||||
}
|
}
|
||||||
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
|
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
|
||||||
mediaMetadata = newMediaMetadata;
|
mediaMetadata = newMediaMetadata;
|
||||||
@ -1794,6 +1808,24 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
return positionUs;
|
return positionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link MediaMetadata} from the main sources.
|
||||||
|
*
|
||||||
|
* <p>{@link MediaItem} {@link MediaMetadata} is prioritized, with any gaps/missing fields
|
||||||
|
* populated by metadata from static ({@link TrackGroup} {@link Format}) and dynamic ({@link
|
||||||
|
* #onMetadata(Metadata)}) sources.
|
||||||
|
*/
|
||||||
|
private MediaMetadata buildUpdatedMediaMetadata() {
|
||||||
|
@Nullable MediaItem mediaItem = getCurrentMediaItem();
|
||||||
|
|
||||||
|
if (mediaItem == null) {
|
||||||
|
return staticAndDynamicMediaMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaItem metadata is prioritized over metadata within the media.
|
||||||
|
return staticAndDynamicMediaMetadata.buildUpon().populate(mediaItem.mediaMetadata).build();
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPlaying(PlaybackInfo playbackInfo) {
|
private static boolean isPlaying(PlaybackInfo playbackInfo) {
|
||||||
return playbackInfo.playbackState == Player.STATE_READY
|
return playbackInfo.playbackState == Player.STATE_READY
|
||||||
&& playbackInfo.playWhenReady
|
&& playbackInfo.playWhenReady
|
||||||
|
@ -11181,6 +11181,46 @@ public final class ExoPlayerTest {
|
|||||||
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
|
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setMediaItem_withMediaMetadata_updatesMediaMetadata() {
|
||||||
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build();
|
||||||
|
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
player.setMediaItem(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("id")
|
||||||
|
.setUri(Uri.EMPTY)
|
||||||
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playingMedia_withNoMetadata_doesNotUpdateMediaMetadata() throws Exception {
|
||||||
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build();
|
||||||
|
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("id")
|
||||||
|
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
|
||||||
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(mediaItem);
|
||||||
|
|
||||||
|
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
|
||||||
|
|
||||||
|
player.prepare();
|
||||||
|
TestPlayerRunHelper.playUntilPosition(
|
||||||
|
player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
|
||||||
|
player.stop();
|
||||||
|
|
||||||
|
shadowOf(Looper.getMainLooper()).idle();
|
||||||
|
|
||||||
|
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user