Add polish to voice note bubbles.

This commit is contained in:
Alex Hart
2020-10-22 16:22:21 -03:00
committed by Cody Henthorne
parent b4b1e5b605
commit ac54b5cbdf
24 changed files with 272 additions and 46 deletions

View File

@@ -61,6 +61,7 @@ public final class AudioView extends FrameLayout {
@ColorInt private final int waveFormPlayedBarsColor;
@ColorInt private final int waveFormUnplayedBarsColor;
@ColorInt private final int waveFormThumbTint;
@Nullable private SlideClickListener downloadListener;
private int backwardsCounter;
@@ -107,6 +108,9 @@ public final class AudioView extends FrameLayout {
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
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);
} finally {
if (typedArray != null) {
typedArray.recycle();
@@ -132,10 +136,15 @@ public final class AudioView extends FrameLayout {
public void setAudio(final @NonNull AudioSlide audio,
final @Nullable Callbacks callbacks,
final boolean showControls)
final boolean showControls,
final boolean forceHideDuration)
{
this.callbacks = callbacks;
if (duration != null) {
duration.setVisibility(View.VISIBLE);
}
if (seekBar instanceof WaveFormSeekBarView) {
if (audioSlide != null && !Objects.equals(audioSlide.getUri(), audio.getUri())) {
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
@@ -150,9 +159,11 @@ public final class AudioView extends FrameLayout {
seekBar.setEnabled(false);
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
circleProgress.setVisibility(View.GONE);
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(progressAndPlay);
seekBar.setEnabled(false);
circleProgress.setVisibility(View.VISIBLE);
circleProgress.spin();
} else {
seekBar.setEnabled(true);
@@ -164,13 +175,13 @@ public final class AudioView extends FrameLayout {
if (seekBar instanceof WaveFormSeekBarView) {
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
if (android.os.Build.VERSION.SDK_INT >= 23) {
new AudioWaveForm(getContext(), audio).getWaveForm(
data -> {
if (duration != null) {
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
updateProgress(0, 0);
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
updateProgress(0, 0);
if (!forceHideDuration && duration != null) {
duration.setVisibility(VISIBLE);
}
waveFormView.setWaveData(data.getWaveForm());
@@ -183,12 +194,21 @@ public final class AudioView extends FrameLayout {
}
}
}
if (forceHideDuration && duration != null) {
duration.setVisibility(View.GONE);
}
}
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
this.downloadListener = listener;
}
public @Nullable Uri getAudioSlideUri() {
if (audioSlide != null) return audioSlide.getUri();
else return null;
}
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
onProgress(voiceNotePlaybackState.getUri(),
@@ -274,6 +294,10 @@ public final class AudioView extends FrameLayout {
}
private void updateProgress(float progress, long millis) {
if (callbacks != null) {
callbacks.onProgressUpdated(durationMillis, millis);
}
if (duration != null && durationMillis > 0) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
@@ -336,7 +360,7 @@ public final class AudioView extends FrameLayout {
if (!smallView || seekBar.getProgress() == 0) {
circleProgress.setInstantProgress(1);
}
circleProgress.setVisibility(VISIBLE);
circleProgress.setVisibility(GONE);
playPauseButton.setVisibility(VISIBLE);
controlToggle.displayQuick(progressAndPlay);
}
@@ -435,5 +459,6 @@ public final class AudioView extends FrameLayout {
void onPause(@NonNull Uri audioUri);
void onSeekTo(@NonNull Uri audioUri, double progress);
void onStopAndReset(@NonNull Uri audioUri);
void onProgressUpdated(long durationMillis, long playheadMillis);
}
}

View File

@@ -5,6 +5,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
@@ -15,18 +16,26 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
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.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class ConversationItemFooter extends LinearLayout {
@@ -36,6 +45,9 @@ public class ConversationItemFooter extends LinearLayout {
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private View audioSpace;
private TextView audioDuration;
private LottieAnimationView revealDot;
public ConversationItemFooter(Context context) {
super(context);
@@ -60,11 +72,15 @@ public class ConversationItemFooter extends LinearLayout {
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);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
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();
}
}
@@ -81,11 +97,18 @@ public class ConversationItemFooter extends LinearLayout {
presentTimer(messageRecord);
presentInsecureIndicator(messageRecord);
presentDeliveryStatus(messageRecord);
hideAudioDurationViews();
}
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis);
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
}
public void setTextColor(int color) {
dateView.setTextColor(color);
simView.setTextColor(color);
audioDuration.setTextColor(color);
}
public void setIconColor(int color) {
@@ -94,6 +117,14 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setTint(color);
}
public void setRevealDotColor(int color) {
revealDot.addValueCallback(
new KeyPath("**"),
LottieProperty.COLOR_FILTER,
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
);
}
public void setOnlyShowSendingStatus(boolean onlyShowSending, MessageRecord messageRecord) {
this.onlyShowSendingStatus = onlyShowSending;
presentDeliveryStatus(messageRecord);
@@ -204,4 +235,64 @@ public class ConversationItemFooter extends LinearLayout {
}
}
}
private void presentAudioDuration(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
if (messageRecord.isOutgoing()) {
moveAudioViewsForOutgoing();
} else {
moveAudioViewsForIncoming();
}
showAudioDurationViews();
} else {
hideAudioDurationViews();
}
} else {
hideAudioDurationViews();
}
}
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 = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? padStart : 0;
int padRight = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 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.VISIBLE);
if (FeatureFlags.viewedReceipts()) {
revealDot.setVisibility(View.VISIBLE);
}
}
private void hideAudioDurationViews() {
audioSpace.setVisibility(View.GONE);
audioDuration.setVisibility(View.GONE);
revealDot.setVisibility(View.GONE);
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.animation.Interpolator;
@@ -13,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatSeekBar;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
@@ -65,9 +67,12 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
barWidth = getResources().getDimensionPixelSize(R.dimen.wave_form_bar_width);
}
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor) {
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor, @ColorInt int thumbTint) {
this.playedBarColor = playedBarColor;
this.unplayedBarColor = unplayedBarColor;
getThumb().setColorFilter(thumbTint, PorterDuff.Mode.SRC_IN);
invalidate();
}

View File

@@ -88,6 +88,8 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
@Override
public void onPrepareFromUri(final Uri uri, Bundle extras) {
Log.d(TAG, "onPrepareFromUri: " + uri);
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
double progress = extras.getDouble(VoiceNoteMediaController.EXTRA_PROGRESS, 0);
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);

View File

@@ -10,6 +10,7 @@ import android.os.Bundle;
import android.os.Process;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
@@ -162,7 +163,16 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
@Override
public void onPositionDiscontinuity(int reason) {
int currentWindowIndex = player.getCurrentWindowIndex();
int currentWindowIndex = player.getCurrentWindowIndex();
if (currentWindowIndex == C.INDEX_UNSET) {
return;
}
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
}
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();

View File

@@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
@@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -127,6 +129,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
@@ -408,6 +411,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
cancelPulseOutlinerAnimation();
if (eventListener != null && audioViewStub.resolved()) {
Log.d(TAG, "unbind: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri());
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
}
@@ -659,6 +663,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
boolean showControls = !messageRecord.isFailed();
if (eventListener != null && audioViewStub.resolved()) {
Log.d(TAG, "setMediaAttributes: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri());
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
@@ -744,13 +749,15 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), new AudioViewCallbacks(), showControls);
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, false);
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
if (eventListener != null) {
Log.d(TAG, "setMediaAttributes: registered listener for audio slide " + audioViewStub.get().getAudioSlideUri());
eventListener.onRegisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
} else {
Log.w(TAG, "setMediaAttributes: could not register listener for audio slide " + audioViewStub.get().getAudioSlideUri());
}
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -1572,6 +1579,11 @@ public class ConversationItem extends LinearLayout implements BindableConversati
public void onStopAndReset(@NonNull Uri audioUri) {
throw new UnsupportedOperationException();
}
@Override
public void onProgressUpdated(long durationMillis, long playheadMillis) {
footer.setAudioDuration(durationMillis, playheadMillis);
}
}
private void handleMessageApproval() {

View File

@@ -440,7 +440,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).getMmsId();
audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver());
audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true);
audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true, true);
audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver());
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
@@ -520,6 +520,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
public void onStopAndReset(@NonNull Uri audioUri) {
audioItemListener.onStopAndReset(audioUri);
}
@Override
public void onProgressUpdated(long durationMillis, long playheadMillis) {
}
}
interface ItemClickListener {

View File

@@ -277,7 +277,7 @@ public class AttachmentManager {
attachmentViewStub.get().setVisibility(View.VISIBLE);
if (slide.hasAudio()) {
audioView.setAudio((AudioSlide) slide, null, false);
audioView.setAudio((AudioSlide) slide, null, false, false);
removableMediaView.display(audioView, false);
result.set(true);
} else if (slide.hasDocument()) {

View File

@@ -59,6 +59,7 @@ public final class FeatureFlags {
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
public static final String RESEARCH_MEGAPHONE_1 = "research.megaphone.1";
public static final String MODERN_PROFILE_SHARING = "android.modernProfileSharing";
private static final String VIEWED_RECEIPTS = "android.viewed.receipts";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -75,7 +76,8 @@ public final class FeatureFlags {
VERIFY_V2,
CLIENT_EXPIRATION,
RESEARCH_MEGAPHONE_1,
MODERN_PROFILE_SHARING
MODERN_PROFILE_SHARING,
VIEWED_RECEIPTS
);
/**
@@ -243,6 +245,13 @@ public final class FeatureFlags {
return getBoolean(MODERN_PROFILE_SHARING, false);
}
/**
* Whether the user should display the content revealed dot in voice notes.
*/
public static boolean viewedReceipts() {
return getBoolean(VIEWED_RECEIPTS, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);