Implement a playback speed toggle for voice notes.

fork-5.53.8
Alex Hart 2021-06-30 16:07:00 -03:00
rodzic e20d6b63cf
commit 2d7c043398
29 zmienionych plików z 754 dodań i 175 usunięć

Wyświetl plik

@ -75,6 +75,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onChatSessionRefreshLearnMoreClicked();
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);

Wyświetl plik

@ -110,7 +110,7 @@ public final class AudioView extends FrameLayout {
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
progressAndPlay.getBackground().setColorFilter(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK), PorterDuff.Mode.SRC_IN);
setProgressAndPlayBackgroundTint(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK));
} finally {
if (typedArray != null) {
typedArray.recycle();
@ -130,6 +130,10 @@ public final class AudioView extends FrameLayout {
EventBus.getDefault().unregister(this);
}
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
return playbackStateObserver;
}
@ -215,6 +219,7 @@ public final class AudioView extends FrameLayout {
onProgress(voiceNotePlaybackState.getUri(),
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
voiceNotePlaybackState.getPlayheadPositionMillis());
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
}
private void onDuration(@NonNull Uri uri, long durationMillis) {
@ -274,6 +279,10 @@ public final class AudioView extends FrameLayout {
}
}
private void onSpeedChanged(@NonNull Uri uri, float speed) {
callbacks.onSpeedChanged(speed, isTarget(uri));
}
private boolean isTarget(@NonNull Uri uri) {
return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
}
@ -451,6 +460,8 @@ public final class AudioView extends FrameLayout {
if (callbacks != null) {
if (wasPlaying) {
callbacks.onSeekTo(audioSlide.getUri(), getProgress());
} else {
callbacks.onProgressUpdated(durationMillis, Math.round(durationMillis * getProgress()));
}
}
}
@ -475,6 +486,7 @@ public final class AudioView extends FrameLayout {
void onPause(@NonNull Uri audioUri);
void onSeekTo(@NonNull Uri audioUri, double progress);
void onStopAndReset(@NonNull Uri audioUri);
void onSpeedChanged(float speed, boolean isPlaying);
void onProgressUpdated(long durationMillis, long playheadMillis);
}
}

Wyświetl plik

@ -1,28 +1,33 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.ApplicationContext;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
@ -30,7 +35,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
@ -40,17 +44,24 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class ConversationItemFooter extends LinearLayout {
public class ConversationItemFooter extends ConstraintLayout {
private TextView dateView;
private TextView simView;
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private View audioSpace;
private TextView audioDuration;
private LottieAnimationView revealDot;
private TextView dateView;
private TextView simView;
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private TextView audioDuration;
private LottieAnimationView revealDot;
private PlaybackSpeedToggleTextView playbackSpeedToggleTextView;
private boolean isOutgoing;
private boolean hasShrunkDate;
private OnTouchDelegateChangedListener onTouchDelegateChangedListener;
private final Rect speedToggleHitRect = new Rect();
private final int touchTargetSize = ViewUtil.dpToPx(48);
public ConversationItemFooter(Context context) {
super(context);
@ -68,24 +79,55 @@ public class ConversationItemFooter extends LinearLayout {
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.conversation_item_footer, this);
dateView = findViewById(R.id.footer_date);
simView = findViewById(R.id.footer_sim_info);
timerView = findViewById(R.id.footer_expiration_timer);
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
deliveryStatusView = findViewById(R.id.footer_delivery_status);
audioDuration = findViewById(R.id.footer_audio_duration);
audioSpace = findViewById(R.id.footer_audio_duration_space);
revealDot = findViewById(R.id.footer_revealed_dot);
final TypedArray typedArray;
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
} else {
typedArray = null;
}
final @LayoutRes int contentId;
if (typedArray != null) {
int mode = typedArray.getInt(R.styleable.ConversationItemFooter_footer_mode, 0);
isOutgoing = mode == 0;
if (isOutgoing) {
contentId = R.layout.conversation_item_footer_outgoing;
} else {
contentId = R.layout.conversation_item_footer_incoming;
}
} else {
contentId = R.layout.conversation_item_footer_outgoing;
isOutgoing = true;
}
inflate(getContext(), contentId, this);
dateView = findViewById(R.id.footer_date);
simView = findViewById(R.id.footer_sim_info);
timerView = findViewById(R.id.footer_expiration_timer);
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
deliveryStatusView = findViewById(R.id.footer_delivery_status);
audioDuration = findViewById(R.id.footer_audio_duration);
revealDot = findViewById(R.id.footer_revealed_dot);
playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle);
if (typedArray != null) {
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
typedArray.recycle();
}
dateView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (oldLeft != left || oldRight != right) {
notifyTouchDelegateChanged(getPlaybackSpeedToggleTouchDelegateRect(), playbackSpeedToggleTextView);
}
});
}
public void setOnTouchDelegateChangedListener(@Nullable OnTouchDelegateChangedListener onTouchDelegateChangedListener) {
this.onTouchDelegateChangedListener = onTouchDelegateChangedListener;
}
@Override
@ -108,6 +150,20 @@ public class ConversationItemFooter extends LinearLayout {
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
}
public void setPlaybackSpeedListener(@Nullable PlaybackSpeedToggleTextView.PlaybackSpeedListener playbackSpeedListener) {
playbackSpeedToggleTextView.setPlaybackSpeedListener(playbackSpeedListener);
}
public void setAudioPlaybackSpeed(float playbackSpeed, boolean isPlaying) {
if (isPlaying) {
showPlaybackSpeedToggle();
} else {
hidePlaybackSpeedToggle();
}
playbackSpeedToggleTextView.setCurrentSpeed(playbackSpeed);
}
public void setTextColor(int color) {
dateView.setTextColor(color);
simView.setTextColor(color);
@ -155,6 +211,84 @@ public class ConversationItemFooter extends LinearLayout {
}
}
private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) {
if (onTouchDelegateChangedListener != null) {
onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate);
}
}
private void showPlaybackSpeedToggle() {
if (hasShrunkDate) {
return;
}
hasShrunkDate = true;
playbackSpeedToggleTextView.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(150L)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
playbackSpeedToggleTextView.setClickable(true);
}
});
if (isOutgoing) {
dateView.setMaxWidth(ViewUtil.dpToPx(28));
} else {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this);
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, ViewUtil.dpToPx(40));
constraintSet.applyTo(this);
}
}
private void hidePlaybackSpeedToggle() {
if (!hasShrunkDate) {
return;
}
hasShrunkDate = false;
playbackSpeedToggleTextView.animate()
.alpha(0f)
.scaleX(0.5f)
.scaleY(0.5f)
.setDuration(150L).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
playbackSpeedToggleTextView.setClickable(false);
playbackSpeedToggleTextView.clearRequestedSpeed();
}
});
if (isOutgoing) {
dateView.setMaxWidth(Integer.MAX_VALUE);
} else {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this);
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, -1);
constraintSet.applyTo(this);
}
}
private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() {
playbackSpeedToggleTextView.getHitRect(speedToggleHitRect);
int widthOffset = (touchTargetSize - speedToggleHitRect.width()) / 2;
int heightOffset = (touchTargetSize - speedToggleHitRect.height()) / 2;
speedToggleHitRect.top -= heightOffset;
speedToggleHitRect.left -= widthOffset;
speedToggleHitRect.right += widthOffset;
speedToggleHitRect.bottom += heightOffset;
return speedToggleHitRect;
}
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
@ -189,7 +323,7 @@ public class ConversationItemFooter extends LinearLayout {
simView.setText(getContext().getString(R.string.ConversationItem_from_s, subscriptionInfo.get().getDisplayName()));
simView.setVisibility(View.VISIBLE);
} else if (subscriptionInfo.isPresent()) {
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
simView.setVisibility(View.VISIBLE);
} else {
simView.setVisibility(View.GONE);
@ -218,7 +352,7 @@ public class ConversationItemFooter extends LinearLayout {
boolean mms = messageRecord.isMms();
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
});
@ -245,7 +379,7 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setNone();
}
} else {
if (!messageRecord.isOutgoing()) {
if (!messageRecord.isOutgoing()) {
deliveryStatusView.setNone();
} else if (messageRecord.isPending()) {
deliveryStatusView.setPending();
@ -264,11 +398,6 @@ public class ConversationItemFooter extends LinearLayout {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
if (messageRecord.isOutgoing()) {
moveAudioViewsForOutgoing();
} else {
moveAudioViewsForIncoming();
}
showAudioDurationViews();
if (messageRecord.getViewedReceiptCount() > 0) {
@ -284,41 +413,19 @@ public class ConversationItemFooter extends LinearLayout {
}
}
private void moveAudioViewsForOutgoing() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace, 0);
addView(revealDot, 0);
addView(audioDuration, 0);
int padStart = ViewUtil.dpToPx(60);
int padLeft = ViewUtil.isLtr(this) ? padStart : 0;
int padRight = ViewUtil.isRtl(this) ? padStart : 0;
audioDuration.setPadding(padLeft, 0, padRight, 0);
}
private void moveAudioViewsForIncoming() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace);
addView(revealDot);
addView(audioDuration);
audioDuration.setPadding(0, 0, 0, 0);
}
private void showAudioDurationViews() {
audioSpace.setVisibility(View.VISIBLE);
audioDuration.setVisibility(View.GONE);
audioDuration.setVisibility(View.VISIBLE);
revealDot.setVisibility(View.VISIBLE);
playbackSpeedToggleTextView.setVisibility(View.VISIBLE);
}
private void hideAudioDurationViews() {
audioSpace.setVisibility(View.GONE);
audioDuration.setVisibility(View.GONE);
revealDot.setVisibility(View.GONE);
playbackSpeedToggleTextView.setVisibility(View.GONE);
}
public interface OnTouchDelegateChangedListener {
void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView);
}
}

Wyświetl plik

@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.components
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.AppCompatTextView
import org.thoughtcrime.securesms.R
class PlaybackSpeedToggleTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val speeds: IntArray = context.resources.getIntArray(R.array.PlaybackSpeedToggleTextView__speeds)
private val labels: Array<String> = context.resources.getStringArray(R.array.PlaybackSpeedToggleTextView__speed_labels)
private var currentSpeedIndex = 0
private var requestedSpeed: Float? = null
var playbackSpeedListener: PlaybackSpeedListener? = null
init {
text = getCurrentLabel()
super.setOnClickListener {
currentSpeedIndex = getNextSpeedIndex()
text = getCurrentLabel()
requestedSpeed = getCurrentSpeed()
playbackSpeedListener?.onPlaybackSpeedChanged(getCurrentSpeed())
}
isClickable = false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (isClickable) {
when (event?.action) {
MotionEvent.ACTION_DOWN -> zoomIn()
MotionEvent.ACTION_UP -> zoomOut()
MotionEvent.ACTION_CANCEL -> zoomOut()
}
}
return super.onTouchEvent(event)
}
fun clearRequestedSpeed() {
requestedSpeed = null
}
fun setCurrentSpeed(speed: Float) {
if (speed == getCurrentSpeed() || (requestedSpeed != null && requestedSpeed != speed)) {
if (requestedSpeed == speed) {
requestedSpeed = null
}
return
}
requestedSpeed = null
val outOf100 = (speed * 100).toInt()
val index = speeds.indexOf(outOf100)
if (index != -1) {
currentSpeedIndex = index
text = getCurrentLabel()
} else {
throw IllegalArgumentException("Invalid Speed $speed")
}
}
private fun getNextSpeedIndex(): Int = (currentSpeedIndex + 1) % speeds.size
private fun getCurrentSpeed(): Float = speeds[currentSpeedIndex] / 100f
private fun getCurrentLabel(): String = labels[currentSpeedIndex]
private fun zoomIn() {
animate()
.setInterpolator(DecelerateInterpolator())
.setDuration(150L)
.scaleX(1.2f)
.scaleY(1.2f)
}
private fun zoomOut() {
animate()
.setInterpolator(DecelerateInterpolator())
.setDuration(150L)
.scaleX(1f)
.scaleY(1f)
}
override fun setOnClickListener(l: OnClickListener?) {
throw UnsupportedOperationException()
}
interface PlaybackSpeedListener {
fun onPlaybackSpeedChanged(speed: Float)
}
}

Wyświetl plik

@ -170,6 +170,15 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
}
}
public void setPlaybackSpeed(@NonNull Uri audioSlideUri, float playbackSpeed) {
if (isCurrentTrack(audioSlideUri)) {
Bundle bundle = new Bundle();
bundle.putFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, playbackSpeed);
getMediaController().sendCommand(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, bundle, null);
}
}
private boolean isCurrentTrack(@NonNull Uri uri) {
MediaMetadataCompat metadataCompat = getMediaController().getMetadata();
@ -235,6 +244,8 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
VoiceNotePlaybackState previousState = voiceNotePlaybackState.getValue();
long position = mediaController.getPlaybackState().getPosition();
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
Bundle extras = mediaController.getExtras();
float speed = extras != null ? extras.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) : 1f;
if (previousState != null && Objects.equals(mediaUri, previousState.getUri())) {
if (position < 0 && previousState.getPlayheadPositionMillis() >= 0) {
@ -247,7 +258,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
}
if (duration > 0 && position >= 0 && position <= duration) {
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri, position, duration, autoReset));
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri, position, duration, autoReset, speed));
}
sendEmptyMessageDelayed(0, 50);

Wyświetl plik

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.components.voice
import android.os.Bundle
import android.os.ResultReceiver
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController
class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters) : DefaultPlaybackController() {
override fun getCommands(): Array<String> {
return arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED)
}
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) {
if (command == VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) {
val speed = extras?.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) ?: 1f
player.playbackParameters = PlaybackParameters(speed)
voiceNotePlaybackParameters.setSpeed(speed)
}
}
}

Wyświetl plik

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.components.voice;
import android.os.Bundle;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.PlaybackParameters;
import org.signal.core.util.logging.Log;
public final class VoiceNotePlaybackParameters {
private final MediaSessionCompat mediaSessionCompat;
VoiceNotePlaybackParameters(@NonNull MediaSessionCompat mediaSessionCompat) {
this.mediaSessionCompat = mediaSessionCompat;
}
@NonNull PlaybackParameters getParameters() {
float speed = getSpeed();
return new PlaybackParameters(speed);
}
void setSpeed(float speed) {
Bundle extras = new Bundle();
extras.putFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, speed);
mediaSessionCompat.setExtras(extras);
}
private float getSpeed() {
Bundle extras = mediaSessionCompat.getController().getExtras();
if (extras == null) {
return 1f;
} else {
return extras.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f);
}
}
}

Wyświetl plik

@ -46,11 +46,12 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
private final Context context;
private final SimpleExoPlayer player;
private final Context context;
private final SimpleExoPlayer player;
private final VoiceNoteQueueDataAdapter queueDataAdapter;
private final AttachmentMediaSourceFactory mediaSourceFactory;
private final ConcatenatingMediaSource dataSource;
private final ConcatenatingMediaSource dataSource;
private final VoiceNotePlaybackParameters voiceNotePlaybackParameters;
private boolean canLoadMore;
private Uri latestUri = Uri.EMPTY;
@ -58,13 +59,15 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
VoiceNotePlaybackPreparer(@NonNull Context context,
@NonNull SimpleExoPlayer player,
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
@NonNull AttachmentMediaSourceFactory mediaSourceFactory)
@NonNull AttachmentMediaSourceFactory mediaSourceFactory,
@NonNull VoiceNotePlaybackParameters voiceNotePlaybackParameters)
{
this.context = context;
this.player = player;
this.queueDataAdapter = queueDataAdapter;
this.mediaSourceFactory = mediaSourceFactory;
this.dataSource = new ConcatenatingMediaSource();
this.dataSource = new ConcatenatingMediaSource();
this.voiceNotePlaybackParameters = voiceNotePlaybackParameters;
}
@Override
@ -119,7 +122,10 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
@Override
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.getWindowCount() >= window) {
player.setPlayWhenReady(false);
player.setPlaybackParameters(voiceNotePlaybackParameters.getParameters());
player.seekTo(window, (long) (player.getDuration() * progress));
player.setPlayWhenReady(true);
player.removeListener(this);
}
}

Wyświetl plik

@ -7,6 +7,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Process;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
@ -26,6 +27,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
@ -35,15 +37,13 @@ import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
@ -54,6 +54,10 @@ import java.util.List;
*/
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private static final int ACTION_AWAIT_SPEED = 0;
public static final String ACTION_NEXT_PLAYBACK_SPEED = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.next_playback_speed";
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
private static final String EMPTY_ROOT_ID = "empty-root-id";
private static final int LOAD_MORE_THRESHOLD = 2;
@ -73,7 +77,9 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private VoiceNoteQueueDataAdapter queueDataAdapter;
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
private VoiceNoteProximityManager voiceNoteProximityManager;
private boolean isForegroundService;
private boolean isForegroundService;
private VoiceNotePlaybackParameters voiceNotePlaybackParameters;
private Handler handler;
private final LoadControl loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(Integer.MAX_VALUE,
@ -87,9 +93,11 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
super.onCreate();
mediaSession = new MediaSessionCompat(this, TAG);
voiceNotePlaybackParameters = new VoiceNotePlaybackParameters(mediaSession);
stateBuilder = new PlaybackStateCompat.Builder()
.setActions(SUPPORTED_ACTIONS);
mediaSessionConnector = new MediaSessionConnector(mediaSession, null);
.setActions(SUPPORTED_ACTIONS)
.addCustomAction(ACTION_NEXT_PLAYBACK_SPEED, "speed", R.drawable.ic_toggle_24);
mediaSessionConnector = new MediaSessionConnector(mediaSession, new VoiceNotePlaybackController(voiceNotePlaybackParameters));
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
queueDataAdapter = new VoiceNoteQueueDataAdapter();
@ -100,7 +108,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory, voiceNotePlaybackParameters);
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
mediaSession.setPlaybackState(stateBuilder.build());
@ -150,6 +158,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
private class VoiceNotePlayerEventListener implements Player.EventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
@ -182,9 +191,19 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
sendViewedReceiptForCurrentWindowIndex();
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
PlaybackParameters playbackParameters = getPlaybackParametersForWindowPosition(currentWindowIndex);
final float speed = playbackParameters != null ? playbackParameters.speed : 1f;
if (speed != player.getPlaybackParameters().speed) {
player.setPlayWhenReady(false);
player.setPlaybackParameters(playbackParameters);
player.seekTo(currentWindowIndex, 1);
player.setPlayWhenReady(true);
}
}
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
@ -202,6 +221,18 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
}
private @Nullable PlaybackParameters getPlaybackParametersForWindowPosition(int currentWindowIndex) {
if (isAudioMessage(currentWindowIndex)) {
return voiceNotePlaybackParameters.getParameters();
} else {
return null;
}
}
private boolean isAudioMessage(int currentWindowIndex) {
return currentWindowIndex % 2 == 0;
}
private void sendViewedReceiptForCurrentWindowIndex() {
if (player.getPlaybackState() == Player.STATE_READY &&
player.getPlayWhenReady() &&

Wyświetl plik

@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.components.voice;
import android.net.Uri;
import androidx.annotation.NonNull;
/**
* Domain-level state object representing the state of the currently playing voice note.
*/
public class VoiceNotePlaybackState {
public static final VoiceNotePlaybackState NONE = new VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false);
private final Uri uri;
private final long playheadPositionMillis;
private final long trackDuration;
private final boolean autoReset;
public VoiceNotePlaybackState(@NonNull Uri uri, long playheadPositionMillis, long trackDuration, boolean autoReset) {
this.uri = uri;
this.playheadPositionMillis = playheadPositionMillis;
this.trackDuration = trackDuration;
this.autoReset = autoReset;
}
/**
* @return Uri of the currently playing AudioSlide
*/
public Uri getUri() {
return uri;
}
/**
* @return The last known playhead position
*/
public long getPlayheadPositionMillis() {
return playheadPositionMillis;
}
/**
* @return The track duration in ms
*/
public long getTrackDuration() {
return trackDuration;
}
/**
* @return true if we should reset the currently playing clip.
*/
public boolean isAutoReset() {
return autoReset;
}
}

Wyświetl plik

@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.components.voice
import android.net.Uri
/**
* Domain-level state object representing the state of the currently playing voice note.
*/
data class VoiceNotePlaybackState(
/**
* @return Uri of the currently playing AudioSlide
*/
val uri: Uri,
/**
* @return The last known playhead position
*/
val playheadPositionMillis: Long,
/**
* @return The track duration in ms
*/
val trackDuration: Long,
/**
* @return true if we should reset the currently playing clip.
*/
val isAutoReset: Boolean,
/**
* @return The current playback speed factor
*/
val speed: Float
) {
companion object {
@JvmField
val NONE = VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false, 1f)
}
}

Wyświetl plik

@ -1594,6 +1594,11 @@ public class ConversationFragment extends LoggingFragment {
voiceNoteMediaController.seekToPosition(uri, progress);
}
@Override
public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
}
@Override
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);

Wyświetl plik

@ -42,6 +42,7 @@ import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
@ -61,6 +62,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.source.MediaSource;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
@ -76,6 +78,7 @@ import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
@ -119,7 +122,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.Projection;
@ -203,15 +205,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private int defaultBubbleColorForWallpaper;
private int measureCalls;
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
private final UrlClickListener urlClickListener = new UrlClickListener();
private final Rect thumbnailMaskingRect = new Rect();
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
private final UrlClickListener urlClickListener = new UrlClickListener();
private final Rect thumbnailMaskingRect = new Rect();
private final TouchDelegateChangedListener touchDelegateChangedListener = new TouchDelegateChangedListener();
private final Context context;
@ -268,6 +271,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyText.setOnLongClickListener(passthroughClickListener);
bodyText.setOnClickListener(passthroughClickListener);
footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener);
}
@Override
@ -407,6 +411,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public void onRecipientChanged(@NonNull Recipient modified) {
if (conversationRecipient.getId().equals(modified.getId())) {
setBubbleState(messageRecord, modified, modified.hasWallpaper(), colorizer);
if (audioViewStub.resolved()) {
setAudioViewTint(messageRecord);
}
}
if (recipient.getId().equals(modified.getId())) {
@ -519,13 +527,15 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setAudioViewTint(MessageRecord messageRecord) {
if (hasAudio(messageRecord)) {
if (!messageRecord.isOutgoing()) {
if (DynamicTheme.isDarkTheme(context)) {
audioViewStub.get().setTint(Color.WHITE);
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_foreground_tint));
if (hasWallpaper) {
audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_play_pause_background_tint_wallpaper));
} else {
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.core_grey_60));
audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_play_pause_background_tint_normal));
}
} else {
audioViewStub.get().setTint(Color.WHITE);
audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.transparent_white_20));
}
}
}
@ -741,6 +751,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
footer.setPlaybackSpeedListener(null);
if (isViewOnceMessage(messageRecord) && !messageRecord.isRemoteDelete()) {
revealableStub.get().setVisibility(VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
@ -823,7 +835,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, false);
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, true);
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
@ -837,6 +849,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setPlaybackSpeedListener(new AudioPlaybackSpeedToggleListener());
footer.setVisibility(VISIBLE);
} else if (hasDocument(messageRecord)) {
documentViewStub.get().setVisibility(View.VISIBLE);
@ -1804,6 +1817,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private final class TouchDelegateChangedListener implements ConversationItemFooter.OnTouchDelegateChangedListener {
@Override
public void onTouchDelegateChanged(@NonNull @NotNull Rect delegateRect, @NonNull @NotNull View delegateView) {
offsetDescendantRectToMyCoords(footer, delegateRect);
setTouchDelegate(new TouchDelegate(delegateRect, delegateView));
}
}
private final class UrlClickListener implements UrlClickHandler {
@Override
@ -1831,6 +1852,22 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public void updateDrawState(@NonNull TextPaint ds) { }
}
private final class AudioPlaybackSpeedToggleListener implements PlaybackSpeedToggleTextView.PlaybackSpeedListener {
@Override
public void onPlaybackSpeedChanged(float speed) {
if (eventListener == null || !audioViewStub.resolved()) {
return;
}
Uri uri = audioViewStub.get().getAudioSlideUri();
if (uri == null) {
return;
}
eventListener.onVoiceNotePlaybackSpeedChanged(uri, speed);
}
}
private final class AudioViewCallbacks implements AudioView.Callbacks {
@Override
@ -1859,6 +1896,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
throw new UnsupportedOperationException();
}
@Override
public void onSpeedChanged(float speed, boolean isPlaying) {
footer.setAudioPlaybackSpeed(speed, isPlaying);
}
@Override
public void onProgressUpdated(long durationMillis, long playheadMillis) {
footer.setAudioDuration(durationMillis, playheadMillis);

Wyświetl plik

@ -576,6 +576,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
audioItemListener.onStopAndReset(audioUri);
}
@Override
public void onSpeedChanged(float speed, boolean isPlaying) {
}
@Override
public void onProgressUpdated(long durationMillis, long playheadMillis) {
}

Wyświetl plik

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@color/green"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/footer_audio_duration"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="0:00"
tools:visibility="visible" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/footer_revealed_dot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_audio_duration"
app:layout_constraintTop_toTopOf="parent"
app:lottie_rawRes="@raw/lottie_played"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/date_and_expiry_wrapper"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/footer_date"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="none"
android:ellipsize="end"
android:linksClickable="false"
android:maxLines="1"
android:textColor="@color/signal_text_secondary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_expiration_timer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="3m" />
<org.thoughtcrime.securesms.components.ExpirationTimerView
android:id="@+id/footer_expiration_timer"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/footer_date"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/footer_sim_info"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:autoLink="none"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
android:linksClickable="false"
android:maxWidth="140dp"
android:maxLines="1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_insecure_indicator"
app:layout_constraintTop_toTopOf="parent"
tools:text="to SIM1" />
<ImageView
android:id="@+id/footer_insecure_indicator"
android:layout_width="12dp"
android:layout_height="11dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:contentDescription="@string/conversation_item__secure_message_description"
android:src="@drawable/ic_unlocked_white_18dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/date_and_expiry_wrapper"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/footer_delivery_status"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView
android:id="@+id/footer_audio_playback_speed_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginEnd="16dp"
android:alpha="0"
android:background="@drawable/round_background"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:minWidth="30dp"
android:minHeight="20dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
android:textColor="@color/signal_text_secondary"
android:visibility="gone"
android:lineSpacingExtra="1sp"
app:backgroundTint="@color/transparent_black_08"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:alpha="1"
tools:text="1x"
tools:visibility="visible" />
</merge>

Wyświetl plik

@ -6,14 +6,19 @@
android:layout_height="wrap_content"
android:gravity="center_vertical|end"
android:orientation="horizontal"
tools:parentTag="org.thoughtcrime.securesms.components.ConversationItemFooter">
tools:background="@color/green"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/footer_audio_duration"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="0:00"
tools:visibility="visible" />
@ -22,18 +27,34 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/footer_audio_duration"
app:layout_constraintTop_toTopOf="parent"
app:lottie_rawRes="@raw/lottie_played"
tools:visibility="visible" />
<Space
android:id="@+id/footer_audio_duration_space"
<org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView
android:id="@+id/footer_audio_playback_speed_toggle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:alpha="0"
android:background="@drawable/round_background"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:lineSpacingExtra="1sp"
android:minWidth="30dp"
android:minHeight="20dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
android:textColor="@color/core_white"
android:visibility="gone"
app:backgroundTint="@color/transparent_white_20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_date"
app:layout_constraintTop_toTopOf="parent"
tools:alpha="1"
tools:text="1x"
tools:visibility="visible" />
<TextView
@ -41,9 +62,15 @@
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:autoLink="none"
android:ellipsize="end"
android:linksClickable="false"
android:maxLines="1"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_expiration_timer"
app:layout_constraintTop_toTopOf="parent"
tools:text="30m" />
<org.thoughtcrime.securesms.components.ExpirationTimerView
@ -51,8 +78,11 @@
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_sim_info"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
@ -61,7 +91,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:autoLink="none"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
@ -69,6 +99,9 @@
android:maxWidth="140dp"
android:maxLines="1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_insecure_indicator"
app:layout_constraintTop_toTopOf="parent"
tools:text="to SIM1"
tools:visibility="visible" />
@ -77,10 +110,13 @@
android:layout_width="12dp"
android:layout_height="11dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/conversation_item__secure_message_description"
android:src="@drawable/ic_unlocked_white_18dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/footer_delivery_status"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.DeliveryStatusView
@ -88,6 +124,8 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</merge>

Wyświetl plik

@ -190,7 +190,7 @@
android:alpha="0.7"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="start"
app:footer_mode="incoming"
app:footer_icon_color="@color/conversation_item_sent_text_secondary_color"
app:footer_reveal_dot_color="@color/conversation_item_sent_text_secondary_color"
app:footer_text_color="@color/conversation_item_sent_text_secondary_color" />
@ -207,8 +207,8 @@
android:paddingBottom="3dp"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="start"
android:visibility="gone"
app:footer_mode="incoming"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />

Wyświetl plik

@ -121,7 +121,7 @@
android:alpha="0.7"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="start"
app:footer_mode="incoming"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />
@ -135,8 +135,8 @@
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="start"
android:visibility="gone"
app:footer_mode="incoming"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />

Wyświetl plik

@ -141,7 +141,7 @@
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="end"
app:footer_mode="outgoing"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />
@ -158,8 +158,8 @@
android:paddingBottom="3dp"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="end"
android:visibility="gone"
app:footer_mode="outgoing"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />

Wyświetl plik

@ -72,7 +72,7 @@
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="end"
app:footer_mode="outgoing"
app:footer_icon_color="@color/conversation_item_sent_text_secondary_color"
app:footer_reveal_dot_color="@color/conversation_item_sent_text_secondary_color"
app:footer_text_color="@color/conversation_item_sent_text_secondary_color" />
@ -86,8 +86,8 @@
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="end"
android:visibility="gone"
app:footer_mode="outgoing"
app:footer_icon_color="@color/signal_icon_tint_secondary"
app:footer_reveal_dot_color="@color/signal_icon_tint_secondary"
app:footer_text_color="@color/signal_text_secondary" />

Wyświetl plik

@ -42,7 +42,7 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:gravity="end"
app:footer_mode="outgoing"
app:footer_text_color="@color/signal_text_toolbar_subtitle"
app:footer_reveal_dot_color="@color/signal_text_toolbar_subtitle"
app:footer_icon_color="@color/signal_text_toolbar_subtitle"/>

Wyświetl plik

@ -34,10 +34,10 @@
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:layout_gravity="end"
android:gravity="end"
android:clipChildren="false"
android:clipToPadding="false"
android:alpha="0.7"
app:footer_mode="incoming"
app:footer_text_color="@color/signal_text_secondary"
app:footer_reveal_dot_color="@color/signal_text_secondary"
app:footer_icon_color="@color/signal_icon_tint_secondary"/>

Wyświetl plik

@ -34,9 +34,9 @@
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:layout_gravity="end"
android:gravity="end"
android:clipChildren="false"
android:clipToPadding="false"
app:footer_mode="outgoing"
app:footer_text_color="@color/conversation_item_sent_text_secondary_color"
app:footer_reveal_dot_color="@color/conversation_item_sent_text_secondary_color"
app:footer_icon_color="@color/conversation_item_sent_text_secondary_color"/>

Wyświetl plik

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
@ -58,13 +58,13 @@
android:id="@+id/contact_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="4dp"
android:layout_gravity="end"
android:gravity="end"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:elevation="9dp"
android:orientation="horizontal" />
android:orientation="horizontal"
app:footer_mode="outgoing" />
<TextView
android:id="@+id/contact_action_button"

Wyświetl plik

@ -80,6 +80,9 @@
<color name="conversation_item_recv_icon_color">@color/core_grey_25</color>
<color name="conversation_item_quote_text_color">@color/core_grey_05</color>
<color name="conversation_item_wallpaper_bubble_color">@color/core_grey_95</color>
<color name="conversation_item_incoming_audio_foreground_tint">@color/core_grey_15</color>
<color name="conversation_item_incoming_audio_play_pause_background_tint_normal">@color/core_grey_60</color>
<color name="conversation_item_incoming_audio_play_pause_background_tint_wallpaper">@color/core_grey_80</color>
<color name="wallpaper_bubble_color">@color/transparent_black_80</color>

Wyświetl plik

@ -401,5 +401,16 @@
<item>@string/SoundsAndNotificationsSettingsFragment__always_notify</item>
<item>@string/SoundsAndNotificationsSettingsFragment__do_not_notify</item>
</string-array>
<integer-array name="PlaybackSpeedToggleTextView__speeds">
<item>100</item>
<item>200</item>
<item>50</item>
</integer-array>
<string-array name="PlaybackSpeedToggleTextView__speed_labels">
<item>@string/PlaybackSpeedToggleTextView__1x</item>
<item>@string/PlaybackSpeedToggleTextView__2x</item>
<item>@string/PlaybackSpeedToggleTextView__p5x</item>
</string-array>
</resources>

Wyświetl plik

@ -170,6 +170,10 @@
<attr name="footer_text_color" format="color" />
<attr name="footer_icon_color" format="color" />
<attr name="footer_reveal_dot_color" format="color" />
<attr name="footer_mode" format="enum">
<enum name="outgoing" value="0" />
<enum name="incoming" value="1" />
</attr>
</declare-styleable>
<declare-styleable name="ConversationItemThumbnail">

Wyświetl plik

@ -80,6 +80,9 @@
<color name="conversation_item_recv_icon_color">@color/core_grey_60</color>
<color name="conversation_item_quote_text_color">@color/core_grey_90</color>
<color name="conversation_item_wallpaper_bubble_color">@color/core_white</color>
<color name="conversation_item_incoming_audio_foreground_tint">@color/core_grey_60</color>
<color name="conversation_item_incoming_audio_play_pause_background_tint_normal">@color/transparent_white_80</color>
<color name="conversation_item_incoming_audio_play_pause_background_tint_wallpaper">@color/core_grey_05</color>
<color name="wallpaper_bubble_color">@color/transparent_white_80</color>

Wyświetl plik

@ -3595,6 +3595,11 @@
<!-- StickerKeyboard -->
<string name="StickerKeyboard__recently_used">Recently used</string>
<!-- PlaybackSpeedToggleTextView -->
<string name="PlaybackSpeedToggleTextView__p5x">.5x</string>
<string name="PlaybackSpeedToggleTextView__1x">1x</string>
<string name="PlaybackSpeedToggleTextView__2x">2x</string>
<!-- EOF -->
</resources>