mirror of
https://github.com/quik-sms/quik.git
synced 2026-11-15 02:28:52 +01:00
added feature to render and play audio/* mime type mms attachments
This commit is contained in:
@@ -27,6 +27,8 @@ fun MmsPart.isImage() = ContentType.isImageType(type)
|
||||
|
||||
fun MmsPart.isVideo() = ContentType.isVideoType(type)
|
||||
|
||||
fun MmsPart.isAudio() = ContentType.isAudioType(type)
|
||||
|
||||
fun MmsPart.isText() = ContentType.TEXT_PLAIN == type
|
||||
|
||||
fun MmsPart.isVCard() = ContentType.TEXT_VCARD == type
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.moez.QKSMS.common
|
||||
|
||||
import android.media.MediaPlayer
|
||||
|
||||
// singleton class for a media player that can be re-used throughout the app
|
||||
// because we probably don't ever want to play multiple media files at the
|
||||
// same time
|
||||
object QkMediaPlayer : MediaPlayer() {
|
||||
private var onCompletionListener: OnCompletionListener? = null
|
||||
|
||||
enum class PlayingState {
|
||||
Stopped,
|
||||
Playing,
|
||||
Paused
|
||||
}
|
||||
private var playingState = PlayingState.Stopped
|
||||
|
||||
override fun start() {
|
||||
playingState = PlayingState.Playing
|
||||
super.start()
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
playingState = PlayingState.Paused
|
||||
super.pause()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
if (playingState != PlayingState.Stopped)
|
||||
stop()
|
||||
onCompletionListener = null
|
||||
super.reset()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
playingState = PlayingState.Stopped
|
||||
super.stop()
|
||||
onCompletionListener?.onCompletion(this) // and manually call on completion handler
|
||||
}
|
||||
|
||||
override fun setOnCompletionListener(listener: OnCompletionListener?) {
|
||||
onCompletionListener = listener
|
||||
super.setOnCompletionListener(listener)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.jakewharton.rxbinding2.view.clicks
|
||||
import com.jakewharton.rxbinding2.widget.textChanges
|
||||
import com.moez.QKSMS.common.QkMediaPlayer
|
||||
import com.uber.autodispose.android.lifecycle.scope
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import dagger.android.AndroidInjection
|
||||
@@ -231,6 +232,14 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
|
||||
activityVisibleIntent.onNext(false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
// stop any playing audio
|
||||
QkMediaPlayer.reset()
|
||||
}
|
||||
|
||||
|
||||
override fun render(state: ComposeState) {
|
||||
if (state.hasError) {
|
||||
finish()
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* This file is part of QKSMS.
|
||||
*
|
||||
* QKSMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QKSMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package dev.octoshrimpy.quik.feature.compose.part
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import com.moez.QKSMS.common.QkMediaPlayer
|
||||
import dev.octoshrimpy.quik.R
|
||||
import dev.octoshrimpy.quik.common.base.QkViewHolder
|
||||
import dev.octoshrimpy.quik.common.util.Colors
|
||||
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
|
||||
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
|
||||
import dev.octoshrimpy.quik.common.util.extensions.setTint
|
||||
import dev.octoshrimpy.quik.common.widget.BubbleImageView
|
||||
import dev.octoshrimpy.quik.extensions.isAudio
|
||||
import dev.octoshrimpy.quik.model.Message
|
||||
import dev.octoshrimpy.quik.model.MmsPart
|
||||
import dev.octoshrimpy.quik.util.GlideApp
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.mms_audio_preview_list_item.*
|
||||
import kotlinx.android.synthetic.main.mms_audio_preview_list_item.view.playPause
|
||||
import kotlinx.android.synthetic.main.mms_audio_preview_list_item.view.seekBar
|
||||
import kotlinx.android.synthetic.main.mms_image_preview_list_item.thumbnail
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class AudioBinder @Inject constructor(colors: Colors, private val context: Context) :
|
||||
PartBinder() {
|
||||
|
||||
override val partLayout = R.layout.mms_audio_preview_list_item
|
||||
override var theme = colors.theme()
|
||||
private var thisPlayingState = QkMediaPlayer.PlayingState.Stopped
|
||||
|
||||
override fun canBindPart(part: MmsPart) = part.isAudio()
|
||||
|
||||
override fun bindPart(
|
||||
holder: QkViewHolder,
|
||||
part: MmsPart,
|
||||
message: Message,
|
||||
canGroupWithPrevious: Boolean,
|
||||
canGroupWithNext: Boolean
|
||||
) {
|
||||
// play/pause button click handling
|
||||
holder.playPause.setOnClickListener { view ->
|
||||
var seekBarUpdater: Disposable? = null
|
||||
|
||||
when (thisPlayingState) {
|
||||
QkMediaPlayer.PlayingState.Stopped -> {
|
||||
QkMediaPlayer.apply {
|
||||
reset() // make sure reset before trying to (re-)use
|
||||
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) // music, maybe?? could be voice. don't want to use CONTENT_TYPE_UNKNOWN though
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
|
||||
setDataSource(context, part.getUri())
|
||||
|
||||
setOnPreparedListener {
|
||||
// start timer to update seek bar
|
||||
seekBarUpdater = Observable.interval(500, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.single())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map { System.nanoTime() }
|
||||
.doOnNext { holder.seekBar.progress = QkMediaPlayer.currentPosition }
|
||||
.subscribe()
|
||||
|
||||
// set seek bar max value
|
||||
holder.seekBar.max = QkMediaPlayer.duration
|
||||
|
||||
start()
|
||||
|
||||
thisPlayingState = QkMediaPlayer.PlayingState.Playing
|
||||
view.playPause.setImageResource(R.drawable.exo_icon_pause)
|
||||
holder.containerView.seekBar.isEnabled = true
|
||||
holder.metadataTitle.isSelected = true
|
||||
}
|
||||
|
||||
setOnCompletionListener { // also called on error because we don't have an onerrorlistener
|
||||
seekBarUpdater?.dispose() // stop timer
|
||||
|
||||
thisPlayingState = QkMediaPlayer.PlayingState.Stopped
|
||||
holder.containerView.seekBar.seekBar.isEnabled = false
|
||||
holder.containerView.seekBar.seekBar.progress = 0
|
||||
view.playPause.setImageResource(R.drawable.exo_icon_play)
|
||||
holder.metadataTitle.isSelected = false
|
||||
}
|
||||
|
||||
// prepare to play
|
||||
prepareAsync()
|
||||
}
|
||||
}
|
||||
QkMediaPlayer.PlayingState.Playing -> {
|
||||
QkMediaPlayer.pause()
|
||||
view.playPause.setImageResource(R.drawable.exo_icon_play)
|
||||
thisPlayingState = QkMediaPlayer.PlayingState.Paused
|
||||
}
|
||||
QkMediaPlayer.PlayingState.Paused -> {
|
||||
QkMediaPlayer.start()
|
||||
view.playPause.setImageResource(R.drawable.exo_icon_pause)
|
||||
thisPlayingState = QkMediaPlayer.PlayingState.Playing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.thumbnail.bubbleStyle = when {
|
||||
!canGroupWithPrevious && canGroupWithNext ->
|
||||
if (message.isMe()) BubbleImageView.Style.OUT_FIRST else BubbleImageView.Style.IN_FIRST
|
||||
canGroupWithPrevious && canGroupWithNext ->
|
||||
if (message.isMe()) BubbleImageView.Style.OUT_MIDDLE else BubbleImageView.Style.IN_MIDDLE
|
||||
canGroupWithPrevious && !canGroupWithNext ->
|
||||
if (message.isMe()) BubbleImageView.Style.OUT_LAST else BubbleImageView.Style.IN_LAST
|
||||
else -> BubbleImageView.Style.ONLY
|
||||
}
|
||||
|
||||
// tint colours
|
||||
val secondaryColor =
|
||||
if (!message.isMe())
|
||||
theme.theme
|
||||
else
|
||||
holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)
|
||||
val primaryColor =
|
||||
if (!message.isMe())
|
||||
theme.textPrimary
|
||||
else
|
||||
holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary)
|
||||
|
||||
val metaDataRetriever = MediaMetadataRetriever()
|
||||
metaDataRetriever.setDataSource(context, part.getUri())
|
||||
|
||||
// sound wave
|
||||
holder.soundWave.setTint(primaryColor)
|
||||
|
||||
// seek bar
|
||||
holder.seekBar.apply {
|
||||
setTint(secondaryColor)
|
||||
thumbTintList = ColorStateList.valueOf(secondaryColor)
|
||||
max = 0
|
||||
isEnabled = false
|
||||
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if ((thisPlayingState != QkMediaPlayer.PlayingState.Stopped) && fromUser)
|
||||
QkMediaPlayer.seekTo(progress)
|
||||
}
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) { /* nothing */ }
|
||||
override fun onStopTrackingTouch(p0: SeekBar?) { /* nothing */ }
|
||||
})
|
||||
}
|
||||
|
||||
// playPause button
|
||||
holder.playPause. apply {
|
||||
setTint(secondaryColor)
|
||||
setBackgroundTint(primaryColor)
|
||||
}
|
||||
|
||||
holder.metadataTitle.apply {
|
||||
// metadata title
|
||||
text = metaDataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) ?: ""
|
||||
|
||||
if (text == "")
|
||||
visibility = View.GONE
|
||||
else {
|
||||
setTextColor(primaryColor)
|
||||
setBackgroundTint(0xccffffff.toInt() and secondaryColor) // hex value is alpha
|
||||
}
|
||||
}
|
||||
|
||||
// embedded image
|
||||
val embeddedPicture = metaDataRetriever.embeddedPicture
|
||||
if (embeddedPicture == null) {
|
||||
holder.thumbnail.setTint(secondaryColor)
|
||||
holder.frame.layoutParams.height /= 2
|
||||
holder.thumbnail.requestLayout()
|
||||
} else {
|
||||
GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(embeddedPicture)
|
||||
.into(holder.thumbnail)
|
||||
}
|
||||
|
||||
metaDataRetriever.close()
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -29,12 +29,12 @@ import dev.octoshrimpy.quik.extensions.isVideo
|
||||
import dev.octoshrimpy.quik.model.Message
|
||||
import dev.octoshrimpy.quik.model.MmsPart
|
||||
import dev.octoshrimpy.quik.util.GlideApp
|
||||
import kotlinx.android.synthetic.main.mms_preview_list_item.*
|
||||
import kotlinx.android.synthetic.main.mms_image_preview_list_item.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class MediaBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() {
|
||||
class ImageBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() {
|
||||
|
||||
override val partLayout = R.layout.mms_preview_list_item
|
||||
override val partLayout = R.layout.mms_image_preview_list_item
|
||||
override var theme = colors.theme()
|
||||
|
||||
override fun canBindPart(part: MmsPart) = part.isImage() || part.isVideo()
|
||||
@@ -37,11 +37,12 @@ import javax.inject.Inject
|
||||
class PartsAdapter @Inject constructor(
|
||||
colors: Colors,
|
||||
fileBinder: FileBinder,
|
||||
mediaBinder: MediaBinder,
|
||||
imageBinder: ImageBinder,
|
||||
audioBinder: AudioBinder,
|
||||
vCardBinder: VCardBinder
|
||||
) : QkAdapter<MmsPart>() {
|
||||
|
||||
private val partBinders = listOf(mediaBinder, vCardBinder, fileBinder)
|
||||
private val partBinders = listOf(audioBinder, imageBinder, vCardBinder, fileBinder)
|
||||
|
||||
var theme: Colors.Theme = colors.theme()
|
||||
set(value) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -76,7 +76,7 @@
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/timestamp"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/mms_preview_list_item" />
|
||||
tools:listitem="@layout/mms_image_preview_list_item" />
|
||||
|
||||
<dev.octoshrimpy.quik.common.widget.AvatarView
|
||||
android:id="@+id/avatar"
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintTop_toBottomOf="@id/timestamp"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/mms_preview_list_item" />
|
||||
tools:listitem="@layout/mms_image_preview_list_item" />
|
||||
|
||||
<dev.octoshrimpy.quik.common.widget.TightTextView
|
||||
android:id="@+id/body"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
~
|
||||
~ This file is part of QKSMS.
|
||||
~
|
||||
~ QKSMS is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ QKSMS is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<FrameLayout 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:id="@+id/frame"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="280dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:clipChildren="false">
|
||||
|
||||
<dev.octoshrimpy.quik.common.widget.BubbleImageView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="false"
|
||||
android:minHeight="140dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/rectangle" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/soundWave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
app:srcCompat="@drawable/audio_preview_foreground" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekBar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:max="10"
|
||||
android:progress="5"
|
||||
android:progressTint="@color/tools_theme"
|
||||
android:thumbTint="@color/tools_theme" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:clipChildren="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playPause"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginStart="12dp"
|
||||
android:background="@drawable/circle"
|
||||
android:elevation="4dp"
|
||||
android:padding="6dp"
|
||||
app:srcCompat="@drawable/exo_icon_play"
|
||||
app:tint="@color/tools_theme" />
|
||||
|
||||
<dev.octoshrimpy.quik.common.widget.QkTextView
|
||||
android:id="@+id/metadataTitle"
|
||||
style="@style/TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/rounded_rectangle_22dp"
|
||||
android:backgroundTint="@color/tools_theme"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:padding="4dp"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:text="@tools:sample/lorem/random[0]"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
Reference in New Issue
Block a user