2015-10-21 22:32:29 +00:00
|
|
|
package org.thoughtcrime.securesms.audio;
|
|
|
|
|
|
|
|
import android.content.Context;
|
2017-03-20 19:22:26 +00:00
|
|
|
import android.hardware.Sensor;
|
|
|
|
import android.hardware.SensorEvent;
|
|
|
|
import android.hardware.SensorEventListener;
|
|
|
|
import android.hardware.SensorManager;
|
2015-10-21 22:32:29 +00:00
|
|
|
import android.media.AudioManager;
|
2018-07-24 20:11:23 +00:00
|
|
|
import android.net.Uri;
|
2017-03-26 14:55:43 +00:00
|
|
|
import android.os.Build;
|
2015-10-21 22:32:29 +00:00
|
|
|
import android.os.Handler;
|
|
|
|
import android.os.Message;
|
2017-03-26 14:55:43 +00:00
|
|
|
import android.os.PowerManager;
|
|
|
|
import android.os.PowerManager.WakeLock;
|
2015-10-21 22:32:29 +00:00
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.support.annotation.Nullable;
|
|
|
|
import android.util.Pair;
|
2015-11-18 22:52:26 +00:00
|
|
|
import android.widget.Toast;
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-12-11 21:03:44 +00:00
|
|
|
import com.google.android.exoplayer2.C;
|
2018-07-24 20:11:23 +00:00
|
|
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
|
|
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
|
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
|
|
import com.google.android.exoplayer2.ExoPlayer;
|
|
|
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
|
|
import com.google.android.exoplayer2.LoadControl;
|
|
|
|
import com.google.android.exoplayer2.Player;
|
|
|
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
2018-12-11 21:03:44 +00:00
|
|
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
2018-07-24 20:11:23 +00:00
|
|
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
|
|
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
|
|
import com.google.android.exoplayer2.source.MediaSource;
|
|
|
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
|
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.BuildConfig;
|
2015-11-18 22:52:26 +00:00
|
|
|
import org.thoughtcrime.securesms.R;
|
2016-11-26 20:10:14 +00:00
|
|
|
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
2018-08-01 15:09:24 +00:00
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
2015-10-21 22:32:29 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
2017-03-26 14:55:43 +00:00
|
|
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
2015-10-21 22:32:29 +00:00
|
|
|
import org.thoughtcrime.securesms.util.Util;
|
2016-03-23 17:34:41 +00:00
|
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
|
|
|
2017-03-20 19:22:26 +00:00
|
|
|
public class AudioSlidePlayer implements SensorEventListener {
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
|
|
|
|
|
|
|
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
|
|
|
|
2017-03-26 14:55:43 +00:00
|
|
|
private final @NonNull Context context;
|
|
|
|
private final @NonNull AudioSlide slide;
|
|
|
|
private final @NonNull Handler progressEventHandler;
|
|
|
|
private final @NonNull AudioManager audioManager;
|
|
|
|
private final @NonNull SensorManager sensorManager;
|
|
|
|
private final @NonNull Sensor proximitySensor;
|
|
|
|
private final @Nullable WakeLock wakeLock;
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
private @NonNull WeakReference<Listener> listener;
|
2018-07-24 20:11:23 +00:00
|
|
|
private @Nullable SimpleExoPlayer mediaPlayer;
|
2016-11-26 20:10:14 +00:00
|
|
|
private @Nullable AttachmentServer audioAttachmentServer;
|
2017-03-20 19:22:26 +00:00
|
|
|
private long startTime;
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
|
|
|
@NonNull AudioSlide slide,
|
|
|
|
@NonNull Listener listener)
|
|
|
|
{
|
|
|
|
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
|
|
|
playing.get().setListener(listener);
|
|
|
|
return playing.get();
|
|
|
|
} else {
|
2018-01-25 03:17:44 +00:00
|
|
|
return new AudioSlidePlayer(context, slide, listener);
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private AudioSlidePlayer(@NonNull Context context,
|
|
|
|
@NonNull AudioSlide slide,
|
|
|
|
@NonNull Listener listener)
|
|
|
|
{
|
|
|
|
this.context = context;
|
|
|
|
this.slide = slide;
|
|
|
|
this.listener = new WeakReference<>(listener);
|
|
|
|
this.progressEventHandler = new ProgressEventHandler(this);
|
2017-03-20 19:22:26 +00:00
|
|
|
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
|
|
|
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
|
|
|
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
2017-03-26 14:55:43 +00:00
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
|
|
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
|
|
|
} else {
|
|
|
|
this.wakeLock = null;
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void play(final double progress) throws IOException {
|
2017-03-20 19:22:26 +00:00
|
|
|
play(progress, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void play(final double progress, boolean earpiece) throws IOException {
|
2015-10-21 22:32:29 +00:00
|
|
|
if (this.mediaPlayer != null) return;
|
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
|
|
|
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
2018-01-25 03:17:44 +00:00
|
|
|
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
|
2017-03-20 19:22:26 +00:00
|
|
|
this.startTime = System.currentTimeMillis();
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
audioAttachmentServer.start();
|
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
mediaPlayer.prepare(createMediaSource(audioAttachmentServer.getUri()));
|
|
|
|
mediaPlayer.setPlayWhenReady(true);
|
2018-12-11 21:03:44 +00:00
|
|
|
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
|
|
|
.setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC)
|
|
|
|
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
|
|
|
.build());
|
|
|
|
mediaPlayer.addListener(new Player.EventListener() {
|
|
|
|
|
|
|
|
boolean started = false;
|
2018-07-24 20:11:23 +00:00
|
|
|
|
2015-10-21 22:32:29 +00:00
|
|
|
@Override
|
2018-07-24 20:11:23 +00:00
|
|
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
2018-12-11 21:03:44 +00:00
|
|
|
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
2018-07-24 20:11:23 +00:00
|
|
|
switch (playbackState) {
|
|
|
|
case Player.STATE_READY:
|
2018-12-11 21:03:44 +00:00
|
|
|
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
2018-07-24 20:11:23 +00:00
|
|
|
synchronized (AudioSlidePlayer.this) {
|
|
|
|
if (mediaPlayer == null) return;
|
2015-10-23 23:54:58 +00:00
|
|
|
|
2018-12-11 21:03:44 +00:00
|
|
|
if (started) {
|
|
|
|
Log.d(TAG, "Already started. Ignoring.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
started = true;
|
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
if (progress > 0) {
|
|
|
|
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
2015-10-23 23:54:58 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
setPlaying(AudioSlidePlayer.this);
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
notifyOnStart();
|
|
|
|
progressEventHandler.sendEmptyMessage(0);
|
|
|
|
break;
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
case Player.STATE_ENDED:
|
2018-12-11 21:03:44 +00:00
|
|
|
Log.i(TAG, "onComplete");
|
2018-07-24 20:11:23 +00:00
|
|
|
synchronized (AudioSlidePlayer.this) {
|
|
|
|
mediaPlayer = null;
|
2015-11-07 01:48:37 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
if (audioAttachmentServer != null) {
|
|
|
|
audioAttachmentServer.stop();
|
|
|
|
audioAttachmentServer = null;
|
|
|
|
}
|
2017-03-20 19:22:26 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
2017-05-22 16:43:33 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
if (wakeLock != null && wakeLock.isHeld()) {
|
2019-05-08 11:45:57 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
|
|
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
|
|
|
}
|
2018-07-24 20:11:23 +00:00
|
|
|
}
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
notifyOnStop();
|
|
|
|
progressEventHandler.removeMessages(0);
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2018-07-24 20:11:23 +00:00
|
|
|
public void onPlayerError(ExoPlaybackException error) {
|
|
|
|
Log.w(TAG, "MediaPlayer Error: " + error);
|
2015-11-18 22:52:26 +00:00
|
|
|
|
|
|
|
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
|
|
|
|
|
|
|
synchronized (AudioSlidePlayer.this) {
|
|
|
|
mediaPlayer = null;
|
|
|
|
|
|
|
|
if (audioAttachmentServer != null) {
|
|
|
|
audioAttachmentServer.stop();
|
|
|
|
audioAttachmentServer = null;
|
|
|
|
}
|
2017-03-20 19:22:26 +00:00
|
|
|
|
|
|
|
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
2017-05-22 16:43:33 +00:00
|
|
|
|
|
|
|
if (wakeLock != null && wakeLock.isHeld()) {
|
2019-05-08 11:45:57 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
|
|
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
|
|
|
}
|
2017-05-22 16:43:33 +00:00
|
|
|
}
|
2015-11-18 22:52:26 +00:00
|
|
|
}
|
|
|
|
|
2015-10-21 22:32:29 +00:00
|
|
|
notifyOnStop();
|
2015-11-18 22:52:26 +00:00
|
|
|
progressEventHandler.removeMessages(0);
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
});
|
2018-07-24 20:11:23 +00:00
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
private MediaSource createMediaSource(@NonNull Uri uri) {
|
|
|
|
return new ExtractorMediaSource.Factory(new DefaultDataSourceFactory(context, BuildConfig.USER_AGENT))
|
|
|
|
.setExtractorsFactory(new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true))
|
|
|
|
.createMediaSource(uri);
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
|
2015-10-23 23:54:58 +00:00
|
|
|
public synchronized void stop() {
|
2018-08-02 13:25:33 +00:00
|
|
|
Log.i(TAG, "Stop called!");
|
2015-10-21 22:32:29 +00:00
|
|
|
|
|
|
|
removePlaying(this);
|
|
|
|
|
|
|
|
if (this.mediaPlayer != null) {
|
|
|
|
this.mediaPlayer.stop();
|
2017-03-20 19:22:26 +00:00
|
|
|
this.mediaPlayer.release();
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.audioAttachmentServer != null) {
|
|
|
|
this.audioAttachmentServer.stop();
|
|
|
|
}
|
|
|
|
|
2017-03-20 19:22:26 +00:00
|
|
|
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
|
|
|
|
2015-10-21 22:32:29 +00:00
|
|
|
this.mediaPlayer = null;
|
|
|
|
this.audioAttachmentServer = null;
|
|
|
|
}
|
|
|
|
|
2015-10-24 00:00:51 +00:00
|
|
|
public synchronized static void stopAll() {
|
|
|
|
if (playing.isPresent()) {
|
|
|
|
playing.get().stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-23 23:54:58 +00:00
|
|
|
public void setListener(@NonNull Listener listener) {
|
|
|
|
this.listener = new WeakReference<>(listener);
|
|
|
|
|
2018-07-24 20:11:23 +00:00
|
|
|
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
2015-10-23 23:54:58 +00:00
|
|
|
notifyOnStart();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull AudioSlide getAudioSlide() {
|
|
|
|
return slide;
|
|
|
|
}
|
|
|
|
|
2017-03-20 19:22:26 +00:00
|
|
|
|
2015-10-21 22:32:29 +00:00
|
|
|
private Pair<Double, Integer> getProgress() {
|
|
|
|
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
|
|
|
return new Pair<>(0D, 0);
|
|
|
|
} else {
|
|
|
|
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
2018-07-24 20:11:23 +00:00
|
|
|
(int) mediaPlayer.getCurrentPosition());
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void notifyOnStart() {
|
|
|
|
Util.runOnMain(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
getListener().onStart();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private void notifyOnStop() {
|
|
|
|
Util.runOnMain(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
getListener().onStop();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private void notifyOnProgress(final double progress, final long millis) {
|
|
|
|
Util.runOnMain(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
getListener().onProgress(progress, millis);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private @NonNull Listener getListener() {
|
|
|
|
Listener listener = this.listener.get();
|
|
|
|
|
|
|
|
if (listener != null) return listener;
|
|
|
|
else return new Listener() {
|
|
|
|
@Override
|
|
|
|
public void onStart() {}
|
|
|
|
@Override
|
|
|
|
public void onStop() {}
|
|
|
|
@Override
|
|
|
|
public void onProgress(double progress, long millis) {}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
|
|
|
if (playing.isPresent() && playing.get() != player) {
|
|
|
|
playing.get().notifyOnStop();
|
|
|
|
playing.get().stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
playing = Optional.of(player);
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
|
|
|
if (playing.isPresent() && playing.get() == player) {
|
|
|
|
playing = Optional.absent();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-20 19:22:26 +00:00
|
|
|
@Override
|
|
|
|
public void onSensorChanged(SensorEvent event) {
|
|
|
|
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
|
2018-07-24 20:11:23 +00:00
|
|
|
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
|
2017-03-20 19:22:26 +00:00
|
|
|
|
|
|
|
int streamType;
|
|
|
|
|
|
|
|
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
|
|
|
|
streamType = AudioManager.STREAM_VOICE_CALL;
|
|
|
|
} else {
|
|
|
|
streamType = AudioManager.STREAM_MUSIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (streamType == AudioManager.STREAM_VOICE_CALL &&
|
|
|
|
mediaPlayer.getAudioStreamType() != streamType &&
|
|
|
|
!audioManager.isWiredHeadsetOn())
|
|
|
|
{
|
|
|
|
double position = mediaPlayer.getCurrentPosition();
|
|
|
|
double duration = mediaPlayer.getDuration();
|
|
|
|
double progress = position / duration;
|
|
|
|
|
2017-03-26 14:55:43 +00:00
|
|
|
if (wakeLock != null) wakeLock.acquire();
|
2017-03-20 19:22:26 +00:00
|
|
|
stop();
|
|
|
|
try {
|
|
|
|
play(progress, true);
|
|
|
|
} catch (IOException e) {
|
|
|
|
Log.w(TAG, e);
|
|
|
|
}
|
|
|
|
} else if (streamType == AudioManager.STREAM_MUSIC &&
|
|
|
|
mediaPlayer.getAudioStreamType() != streamType &&
|
|
|
|
System.currentTimeMillis() - startTime > 500)
|
|
|
|
{
|
2017-03-26 14:55:43 +00:00
|
|
|
if (wakeLock != null) wakeLock.release();
|
2017-03-20 19:22:26 +00:00
|
|
|
stop();
|
|
|
|
notifyOnStop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2015-10-21 22:32:29 +00:00
|
|
|
public interface Listener {
|
2018-12-11 21:03:44 +00:00
|
|
|
void onStart();
|
|
|
|
void onStop();
|
|
|
|
void onProgress(double progress, long millis);
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private static class ProgressEventHandler extends Handler {
|
|
|
|
|
|
|
|
private final WeakReference<AudioSlidePlayer> playerReference;
|
|
|
|
|
|
|
|
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
|
|
|
this.playerReference = new WeakReference<>(player);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void handleMessage(Message msg) {
|
|
|
|
AudioSlidePlayer player = playerReference.get();
|
|
|
|
|
2018-12-11 21:03:44 +00:00
|
|
|
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
2015-10-21 22:32:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Pair<Double, Integer> progress = player.getProgress();
|
|
|
|
player.notifyOnProgress(progress.first, progress.second);
|
|
|
|
sendEmptyMessageDelayed(0, 50);
|
|
|
|
}
|
2018-12-11 21:03:44 +00:00
|
|
|
|
|
|
|
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
|
|
|
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
|
|
|
}
|
2015-10-21 22:32:29 +00:00
|
|
|
}
|
|
|
|
}
|