Implement correct video story sound behaviour.
This commit is contained in:
parent
521bd2cce4
commit
1cfa5c31f2
13 changed files with 389 additions and 0 deletions
|
@ -10,6 +10,7 @@ class VideoControlsDelegate {
|
|||
|
||||
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
|
||||
private var player: Player? = null
|
||||
private var isMuted: Boolean = true
|
||||
|
||||
fun getPlayerState(uri: Uri): PlayerState? {
|
||||
val player: Player? = this.player
|
||||
|
@ -41,9 +42,29 @@ class VideoControlsDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
fun mute() {
|
||||
isMuted = true
|
||||
player?.videoPlayer?.mute()
|
||||
}
|
||||
|
||||
fun unmute() {
|
||||
isMuted = false
|
||||
player?.videoPlayer?.unmute()
|
||||
}
|
||||
|
||||
fun hasAudioStream(): Boolean {
|
||||
return player?.videoPlayer?.hasAudioTrack() ?: false
|
||||
}
|
||||
|
||||
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?, isGif: Boolean) {
|
||||
player = Player(uri, videoPlayer, isGif)
|
||||
|
||||
if (isMuted) {
|
||||
videoPlayer?.mute()
|
||||
} else {
|
||||
videoPlayer?.unmute()
|
||||
}
|
||||
|
||||
if (playWhenReady[uri] == true) {
|
||||
playWhenReady[uri] = false
|
||||
videoPlayer?.play()
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withClip
|
||||
import androidx.media.AudioManagerCompat
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
|
||||
/**
|
||||
* Displays a vertical volume bar.
|
||||
*/
|
||||
class StoryVolumeBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val minimum: Int
|
||||
private val maximum: Int
|
||||
|
||||
private var level: Int
|
||||
private val bounds: Rect = Rect()
|
||||
private val clipBoundsF: RectF = RectF()
|
||||
private val clipPath: Path = Path()
|
||||
private val clipRadius = DimensionUnit.DP.toPixels(4f)
|
||||
|
||||
private val backgroundPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.transparent_black_40)
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val foregroundPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.core_white)
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
init {
|
||||
val audioManager = ServiceUtil.getAudioManager(context)
|
||||
|
||||
if (isInEditMode) {
|
||||
minimum = 0
|
||||
maximum = 100
|
||||
level = 50
|
||||
} else {
|
||||
minimum = AudioManagerCompat.getStreamMinVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
maximum = AudioManagerCompat.getStreamMaxVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
level = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLevel(level: Int) {
|
||||
this.level = level
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
backgroundPaint.strokeWidth = (measuredWidth - paddingLeft - paddingRight).toFloat()
|
||||
foregroundPaint.strokeWidth = backgroundPaint.strokeWidth
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.getClipBounds(bounds)
|
||||
clipBoundsF.set(bounds)
|
||||
clipPath.reset()
|
||||
clipPath.addRoundRect(clipBoundsF, clipRadius, clipRadius, Path.Direction.CW)
|
||||
|
||||
canvas.withClip(clipPath) {
|
||||
canvas.drawLine(bounds.exactCenterX(), bounds.top.toFloat(), bounds.exactCenterX(), bounds.bottom.toFloat(), backgroundPaint)
|
||||
|
||||
val fillPercent: Float = (level - minimum) / (maximum - minimum.toFloat())
|
||||
val fillHeight = bounds.height() * fillPercent
|
||||
|
||||
canvas.drawLine(bounds.exactCenterX(), bounds.bottom.toFloat() - fillHeight, bounds.exactCenterX(), bounds.bottom.toFloat(), foregroundPaint)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Displays a volume bar along with an indicator specifiying whether or not
|
||||
* a given video contains sound.
|
||||
*/
|
||||
class StoryVolumeOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : ConstraintLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.story_volume_overlay_view, this)
|
||||
}
|
||||
|
||||
private val videoHasNoAudioIndicator: View = findViewById(R.id.story_no_audio_indicator)
|
||||
private val volumeBar: StoryVolumeBar = findViewById(R.id.story_volume_bar)
|
||||
|
||||
fun setVideoHaNoAudio(videoHasNoAudio: Boolean) {
|
||||
videoHasNoAudioIndicator.visible = videoHasNoAudio
|
||||
}
|
||||
|
||||
fun setVolumeLevel(level: Int) {
|
||||
volumeBar.setLevel(level)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver
|
||||
|
||||
/**
|
||||
* Stories are to start muted, and once unmuted, remain as such until the
|
||||
* user backgrounds the application.
|
||||
*/
|
||||
object StoryMutePolicy : AppForegroundObserver.Listener {
|
||||
var isContentMuted: Boolean = true
|
||||
|
||||
fun initialize() {
|
||||
ApplicationDependencies.getAppForegroundObserver().addListener(this)
|
||||
}
|
||||
|
||||
override fun onBackground() {
|
||||
isContentMuted = true
|
||||
}
|
||||
}
|
|
@ -2,17 +2,26 @@ package org.thoughtcrime.securesms.stories.viewer
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.media.AudioManagerCompat
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner {
|
||||
|
||||
private val viewModel: StoryVolumeViewModel by viewModels()
|
||||
|
||||
override lateinit var voiceNoteMediaController: VoiceNoteMediaController
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
|
@ -21,6 +30,8 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
StoryMutePolicy.initialize()
|
||||
|
||||
supportPostponeEnterTransition()
|
||||
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
@ -33,6 +44,15 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (StoryMutePolicy.isContentMuted) {
|
||||
viewModel.mute()
|
||||
} else {
|
||||
viewModel.unmute()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
|
@ -54,6 +74,25 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
|||
.commit()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val audioManager = ServiceUtil.getAudioManager(this)
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
val maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
StoryMutePolicy.isContentMuted = false
|
||||
viewModel.onVolumeUp(min(maxVolume, audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + 1))
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
val minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
viewModel.onVolumeDown(max(minVolume, audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - 1))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGS = "story.viewer.args"
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
data class StoryVolumeState(
|
||||
val isMuted: Boolean = true,
|
||||
val level: Int = -1
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class StoryVolumeViewModel : ViewModel() {
|
||||
private val store = RxStore(StoryVolumeState())
|
||||
|
||||
val state: Flowable<StoryVolumeState> = store.stateFlowable
|
||||
val snapshot: StoryVolumeState get() = store.state
|
||||
|
||||
fun mute() {
|
||||
store.update { it.copy(isMuted = true) }
|
||||
}
|
||||
|
||||
fun unmute() {
|
||||
store.update { it.copy(isMuted = false) }
|
||||
}
|
||||
|
||||
fun onVolumeDown(level: Int) {
|
||||
store.update { it.copy(level = level) }
|
||||
}
|
||||
|
||||
fun onVolumeUp(level: Int) {
|
||||
store.update { it.copy(isMuted = false, level = level) }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
|
@ -7,6 +8,7 @@ import android.content.Context
|
|||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -54,9 +56,11 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
|
||||
import org.thoughtcrime.securesms.stories.StorySlateView
|
||||
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerState
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoriesSharedElementCrossFaderView
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.group.StoryGroupReplyBottomSheetDialogFragment
|
||||
|
@ -67,7 +71,9 @@ import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsBottomSheetDial
|
|||
import org.thoughtcrime.securesms.util.AvatarUtil
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
||||
|
@ -87,6 +93,8 @@ class StoryViewerPageFragment :
|
|||
StoriesSharedElementCrossFaderView.Callback,
|
||||
StoryFirstTimeNavigationView.Callback {
|
||||
|
||||
private val activityViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
|
||||
private lateinit var progressBar: SegmentedProgressBar
|
||||
private lateinit var storySlate: StorySlateView
|
||||
private lateinit var viewsAndReplies: MaterialButton
|
||||
|
@ -101,6 +109,10 @@ class StoryViewerPageFragment :
|
|||
private lateinit var chrome: List<View>
|
||||
private var animatorSet: AnimatorSet? = null
|
||||
|
||||
private var volumeInAnimator: Animator? = null
|
||||
private var volumeOutAnimator: Animator? = null
|
||||
private var volumeDebouncer: Debouncer = Debouncer(3, TimeUnit.SECONDS)
|
||||
|
||||
private val viewModel: StoryViewerPageViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
StoryViewerPageViewModel.Factory(storyRecipientId, initialStoryId, isUnviewedOnly, StoryViewerPageRepository(requireContext()))
|
||||
|
@ -135,6 +147,12 @@ class StoryViewerPageFragment :
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = requireListener()
|
||||
|
||||
if (activityViewModel.snapshot.isMuted) {
|
||||
videoControlsDelegate.mute()
|
||||
} else {
|
||||
videoControlsDelegate.unmute()
|
||||
}
|
||||
|
||||
val closeView: View = view.findViewById(R.id.close)
|
||||
val senderAvatar: AvatarImageView = view.findViewById(R.id.sender_avatar)
|
||||
val groupAvatar: AvatarImageView = view.findViewById(R.id.group_avatar)
|
||||
|
@ -150,6 +168,7 @@ class StoryViewerPageFragment :
|
|||
val reactionAnimationView: OnReactionSentView = view.findViewById(R.id.on_reaction_sent_view)
|
||||
val storyGradientTop: View = view.findViewById(R.id.story_gradient_top)
|
||||
val storyGradientBottom: View = view.findViewById(R.id.story_gradient_bottom)
|
||||
val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay)
|
||||
|
||||
blurContainer = view.findViewById(R.id.story_blur_container)
|
||||
storyContentContainer = view.findViewById(R.id.story_content_container)
|
||||
|
@ -267,6 +286,29 @@ class StoryViewerPageFragment :
|
|||
viewModel.setIsUserScrollingParent(isScrolling)
|
||||
}
|
||||
|
||||
lifecycleDisposable += activityViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { volumeState ->
|
||||
if (volumeState.isMuted) {
|
||||
videoControlsDelegate.mute()
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!viewModel.hasPost() || !viewModel.getPost().content.isVideo() || volumeState.level < 0) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!volumeState.isMuted) {
|
||||
videoControlsDelegate.unmute()
|
||||
}
|
||||
|
||||
val audioManager = ServiceUtil.getAudioManager(requireContext())
|
||||
if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) != volumeState.level) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volumeState.level, 0)
|
||||
storyVolumeOverlayView.setVolumeLevel(volumeState.level)
|
||||
storyVolumeOverlayView.setVideoHaNoAudio(!videoControlsDelegate.hasAudioStream())
|
||||
displayStoryVolumeOverlayForTimeout(storyVolumeOverlayView)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += sharedViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { parentState ->
|
||||
if (parentState.pages.size <= parentState.page) {
|
||||
viewModel.setIsSelectedPage(false)
|
||||
|
@ -421,6 +463,8 @@ class StoryViewerPageFragment :
|
|||
it.cleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
volumeDebouncer.clear()
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
|
@ -455,6 +499,26 @@ class StoryViewerPageFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun displayStoryVolumeOverlayForTimeout(view: View) {
|
||||
if (volumeInAnimator?.isRunning != true) {
|
||||
volumeOutAnimator?.cancel()
|
||||
volumeInAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1f).apply {
|
||||
duration = 200
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
volumeDebouncer.publish {
|
||||
if (volumeOutAnimator?.isRunning != true) {
|
||||
volumeInAnimator?.cancel()
|
||||
volumeOutAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f).apply {
|
||||
duration = 200
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideChromeImmediate() {
|
||||
animatorSet?.cancel()
|
||||
chrome.map {
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.util;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A class that will throttle the number of runnables executed to be at most once every specified
|
||||
* interval. However, it could be longer if events are published consistently.
|
||||
|
@ -17,6 +19,10 @@ public class Debouncer {
|
|||
private final Handler handler;
|
||||
private final long threshold;
|
||||
|
||||
public Debouncer(long threshold, TimeUnit timeUnit) {
|
||||
this(timeUnit.toMillis(threshold));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
|
||||
* {@code threshold} milliseconds.
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
|||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
@ -41,6 +42,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -136,6 +138,36 @@ public class VideoPlayer extends FrameLayout {
|
|||
exoPlayer.setPlayWhenReady(autoplay);
|
||||
}
|
||||
|
||||
public void mute() {
|
||||
if (exoPlayer != null && exoPlayer.getAudioComponent() != null) {
|
||||
exoPlayer.getAudioComponent().setVolume(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void unmute() {
|
||||
if (exoPlayer != null && exoPlayer.getAudioComponent() != null) {
|
||||
exoPlayer.getAudioComponent().setVolume(1f);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAudioTrack() {
|
||||
if (exoPlayer != null) {
|
||||
TrackGroupArray trackGroupArray = exoPlayer.getCurrentTrackGroups();
|
||||
if (trackGroupArray != null) {
|
||||
for (int i = 0; i < trackGroupArray.length; i++) {
|
||||
for (int j = 0; j < trackGroupArray.get(i).length; j++) {
|
||||
String sampleMimeType = trackGroupArray.get(i).getFormat(j).sampleMimeType;
|
||||
if (MediaUtil.isAudioType(sampleMimeType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return exoPlayer != null;
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_speaker_off_outline_24.xml
Normal file
9
app/src/main/res/drawable/ic_speaker_off_outline_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20.061,12.001L22.53,14.471L21.47,15.531L19,13.062L16.53,15.531L15.47,14.471L17.939,12.001L15.47,9.531L16.53,8.471L19,10.94L21.47,8.471L22.53,9.531L20.061,12.001ZM13.5,2.637C13.376,2.637 13.256,2.684 13.165,2.769L8,7.501H4C3.47,7.501 2.961,7.711 2.586,8.087C2.211,8.462 2,8.97 2,9.501V14.501C2,15.031 2.211,15.54 2.586,15.915C2.961,16.29 3.47,16.501 4,16.501H8L13.162,21.233C13.253,21.317 13.373,21.364 13.497,21.365C13.63,21.365 13.757,21.312 13.851,21.218C13.944,21.125 13.997,20.997 13.997,20.865V3.138C13.997,3.005 13.945,2.878 13.852,2.785C13.759,2.691 13.632,2.638 13.5,2.637V2.637ZM12.75,19.251L11.34,17.528L8.583,15.001H4C3.867,15.001 3.74,14.948 3.646,14.854C3.553,14.76 3.5,14.633 3.5,14.501V9.501C3.5,9.368 3.553,9.241 3.646,9.147C3.74,9.053 3.867,9.001 4,9.001H8.583L11.34,6.474L12.75,4.751L12.5,7.501V16.501L12.75,19.251Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -68,6 +68,14 @@
|
|||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
|
||||
android:id="@+id/story_volume_overlay"
|
||||
android:alpha="0"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
|
33
app/src/main/res/layout/story_volume_overlay_view.xml
Normal file
33
app/src/main/res/layout/story_volume_overlay_view.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?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="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/story_no_audio_indicator"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@color/transparent_black_40"
|
||||
android:scaleType="centerInside"
|
||||
android:tint="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/story_volume_bar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
|
||||
app:srcCompat="@drawable/ic_speaker_off_outline_24" />
|
||||
|
||||
<org.thoughtcrime.securesms.stories.StoryVolumeBar
|
||||
android:id="@+id/story_volume_bar"
|
||||
android:layout_width="6dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</merge>
|
Loading…
Add table
Reference in a new issue