kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement a playback speed toggle for voice notes.
rodzic
e20d6b63cf
commit
2d7c043398
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() &&
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue