Skip to content

Commit c6bd6e4

Browse files
authored
Merge pull request #3598 from vector-im/feature/ons/voice_message
Voice Message
2 parents 8e28872 + 1aa706d commit c6bd6e4

File tree

74 files changed

+2607
-57
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2607
-57
lines changed

build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ allprojects {
4848
// Chat effects
4949
includeGroupByRegex 'com\\.github\\.jetradarmobile'
5050
includeGroupByRegex 'nl\\.dionsegijn'
51+
52+
// Voice RecordView
53+
includeGroupByRegex 'com\\.github\\.Armen101'
5154
}
5255
}
5356
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }

library/ui-styles/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,6 @@ dependencies {
6060
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
6161
// dialpad dimen
6262
implementation 'im.dlg:android-dialer:1.2.5'
63+
// AudioRecordView attr
64+
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
6365
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<solid android:color="#F00" />
5+
6+
<corners android:radius="8dp" />
7+
8+
</shape>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33

4+
<integer name="rtl_x_multiplier">-1</integer>
45
<integer name="rtl_mirror_flip">180</integer>
56

67
</resources>

library/ui-styles/src/main/res/values/colors.xml

+4
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,8 @@
128128
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
129129
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
130130

131+
<attr name="vctr_voice_message_toast_background" format="color" />
132+
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color>
133+
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color>
134+
131135
</resources>

library/ui-styles/src/main/res/values/integers.xml

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
<integer name="default_animation_offset">200</integer>
99

10+
<integer name="rtl_x_multiplier">1</integer>
1011
<integer name="rtl_mirror_flip">0</integer>
1112

1213
<integer name="splash_animation_velocity">750</integer>

library/ui-styles/src/main/res/values/palette.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
<!-- For light themes -->
2525
<color name="palette_gray_25">#F4F6FA</color>
26-
<color name="palette_gray_50">#E6E8F0</color>
26+
<color name="palette_gray_50">#E3E8F0</color>
2727
<color name="palette_gray_100">#C1C6CD</color>
2828
<color name="palette_gray_150">#8D97A5</color>
2929
<color name="palette_gray_200">#737D8C</color>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
4+
<style name="VoicePlaybackWaveform">
5+
<item name="chunkColor">?vctr_content_secondary</item>
6+
<item name="chunkAlignTo">center</item>
7+
<item name="chunkMinHeight">1dp</item>
8+
<item name="chunkRoundedCorners">true</item>
9+
<item name="chunkSoftTransition">true</item>
10+
<item name="chunkSpace">2dp</item>
11+
<item name="chunkWidth">2dp</item>
12+
<item name="direction">rightToLeft</item>
13+
</style>
14+
15+
<style name="Widget.Vector.TextView.Caption.Toast">
16+
<item name="android:paddingTop">8dp</item>
17+
<item name="android:paddingBottom">8dp</item>
18+
<item name="android:paddingStart">12dp</item>
19+
<item name="android:paddingEnd">12dp</item>
20+
<item name="android:background">@drawable/bg_round_corner_8dp</item>
21+
<item name="android:backgroundTint">?vctr_voice_message_toast_background</item>
22+
<item name="android:textColor">@color/palette_white</item>
23+
<item name="android:gravity">center</item>
24+
</style>
25+
26+
</resources>

library/ui-styles/src/main/res/values/theme_dark.xml

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@
135135

136136
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Dark</item>
137137

138+
<!-- Voice Message -->
139+
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
138140
</style>
139141

140142
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

library/ui-styles/src/main/res/values/theme_light.xml

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137

138138
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
139139

140+
<!-- Voice Message -->
141+
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
140142
</style>
141143

142144
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />

matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.reactivex.Single
2323
import kotlinx.coroutines.rx2.rxCompletable
2424
import kotlinx.coroutines.rx2.rxSingle
2525
import org.matrix.android.sdk.api.query.QueryStringValue
26+
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
2627
import org.matrix.android.sdk.api.session.events.model.Event
2728
import org.matrix.android.sdk.api.session.identity.ThreePid
2829
import org.matrix.android.sdk.api.session.room.Room
@@ -146,6 +147,10 @@ class RxRoom(private val room: Room) {
146147
fun deleteAvatar(): Completable = rxCompletable {
147148
room.deleteAvatar()
148149
}
150+
151+
fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>): Completable = rxCompletable {
152+
room.sendMedia(attachment, compressBeforeSending, roomIds)
153+
}
149154
}
150155

151156
fun Room.rx(): RxRoom {

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ data class ContentAttachmentData(
3535
val name: String? = null,
3636
val queryUri: Uri,
3737
val mimeType: String?,
38-
val type: Type
38+
val type: Type,
39+
val waveform: List<Int>? = null
3940
) : Parcelable {
4041

4142
@JsonClass(generateAdapter = false)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ data class AudioInfo(
2424
/**
2525
* The mimetype of the audio e.g. "audio/aac".
2626
*/
27-
@Json(name = "mimetype") val mimeType: String?,
27+
@Json(name = "mimetype") val mimeType: String? = null,
2828

2929
/**
3030
* The size of the audio clip in bytes.
3131
*/
32-
@Json(name = "size") val size: Long = 0,
32+
@Json(name = "size") val size: Long? = null,
3333

3434
/**
3535
* The duration of the audio in milliseconds.
3636
*/
37-
@Json(name = "duration") val duration: Int = 0
37+
@Json(name = "duration") val duration: Int? = null
3838
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2020 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.api.session.room.model.message
18+
19+
import com.squareup.moshi.Json
20+
import com.squareup.moshi.JsonClass
21+
22+
/**
23+
* See https://github.com/matrix-org/matrix-doc/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md
24+
*/
25+
@JsonClass(generateAdapter = true)
26+
data class AudioWaveformInfo(
27+
@Json(name = "duration")
28+
val duration: Int? = null,
29+
30+
/**
31+
* The array should have no less than 30 elements and no more than 120.
32+
* List of integers between zero and 1024, inclusive.
33+
*/
34+
@Json(name = "waveform")
35+
val waveform: List<Int>? = null
36+
)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.squareup.moshi.Json
2020
import com.squareup.moshi.JsonClass
2121
import org.matrix.android.sdk.api.session.events.model.Content
2222
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
23+
import org.matrix.android.sdk.api.util.JsonDict
2324
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
2425

2526
@JsonClass(generateAdapter = true)
@@ -50,7 +51,17 @@ data class MessageAudioContent(
5051
/**
5152
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
5253
*/
53-
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
54+
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null,
55+
56+
/**
57+
* Encapsulates waveform and duration of the audio.
58+
*/
59+
@Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null,
60+
61+
/**
62+
* Indicates that is a voice message.
63+
*/
64+
@Json(name = "org.matrix.msc3245.voice") val voiceMessageIndicator: JsonDict? = null
5465
) : MessageWithAttachmentContent {
5566

5667
override val mimeType: String?

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ object MimeTypes {
3131
const val Jpeg = "image/jpeg"
3232
const val Gif = "image/gif"
3333

34+
const val Ogg = "audio/ogg"
35+
3436
fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
3537

3638
fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt

+12-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith
2525
import kotlinx.coroutines.withContext
2626
import okhttp3.OkHttpClient
2727
import okhttp3.Request
28+
import org.matrix.android.sdk.api.failure.Failure
2829
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
2930
import org.matrix.android.sdk.api.session.file.FileService
3031
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
@@ -124,13 +125,21 @@ internal class DefaultFileService @Inject constructor(
124125
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
125126
.build()
126127

127-
val response = okHttpClient.newCall(request).execute()
128+
val response = try {
129+
okHttpClient.newCall(request).execute()
130+
} catch (failure: Throwable) {
131+
throw if (failure is IOException) {
132+
Failure.NetworkConnection(failure)
133+
} else {
134+
failure
135+
}
136+
}
128137

129138
if (!response.isSuccessful) {
130-
throw IOException()
139+
throw Failure.NetworkConnection(IOException())
131140
}
132141

133-
val source = response.body?.source() ?: throw IOException()
142+
val source = response.body?.source() ?: throw Failure.NetworkConnection(IOException())
134143

135144
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
136145

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ internal class DefaultSendService @AssistedInject constructor(
184184
mimeType = messageContent.mimeType,
185185
name = messageContent.body,
186186
queryUri = Uri.parse(messageContent.url),
187-
type = ContentAttachmentData.Type.AUDIO
187+
type = ContentAttachmentData.Type.AUDIO,
188+
waveform = messageContent.audioWaveformInfo?.waveform
188189
)
189190
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
190191
internalSendMedia(listOf(localEcho.root), attachmentData, true)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
2929
import org.matrix.android.sdk.api.session.events.model.UnsignedData
3030
import org.matrix.android.sdk.api.session.events.model.toContent
3131
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
32+
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
3233
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
3334
import org.matrix.android.sdk.api.session.room.model.message.ImageInfo
3435
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@@ -74,6 +75,7 @@ internal class LocalEchoEventFactory @Inject constructor(
7475
private val markdownParser: MarkdownParser,
7576
private val textPillsUtils: TextPillsUtils,
7677
private val thumbnailExtractor: ThumbnailExtractor,
78+
private val waveformSanitizer: WaveFormSanitizer,
7779
private val localEchoRepository: LocalEchoRepository,
7880
private val permalinkFactory: PermalinkFactory
7981
) {
@@ -289,14 +291,21 @@ internal class LocalEchoEventFactory @Inject constructor(
289291
}
290292

291293
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
294+
val isVoiceMessage = attachment.waveform != null
292295
val content = MessageAudioContent(
293296
msgType = MessageType.MSGTYPE_AUDIO,
294297
body = attachment.name ?: "audio",
295298
audioInfo = AudioInfo(
299+
duration = attachment.duration?.toInt(),
296300
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
297301
size = attachment.size
298302
),
299-
url = attachment.queryUri.toString()
303+
url = attachment.queryUri.toString(),
304+
audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo(
305+
duration = attachment.duration?.toInt(),
306+
waveform = waveformSanitizer.sanitize(attachment.waveform)
307+
),
308+
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
300309
)
301310
return createMessageEvent(roomId, content)
302311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.session.room.send
18+
19+
import timber.log.Timber
20+
import javax.inject.Inject
21+
import kotlin.math.abs
22+
import kotlin.math.ceil
23+
24+
internal class WaveFormSanitizer @Inject constructor() {
25+
private companion object {
26+
const val MIN_NUMBER_OF_VALUES = 30
27+
const val MAX_NUMBER_OF_VALUES = 120
28+
29+
const val MAX_VALUE = 1024
30+
}
31+
32+
/**
33+
* The array should have no less than 30 elements and no more than 120.
34+
* List of integers between zero and 1024, inclusive.
35+
*/
36+
fun sanitize(waveForm: List<Int>?): List<Int>? {
37+
if (waveForm.isNullOrEmpty()) {
38+
return null
39+
}
40+
41+
// Limit the number of items
42+
val sizeInRangeList = mutableListOf<Int>()
43+
when {
44+
waveForm.size < MIN_NUMBER_OF_VALUES -> {
45+
// Repeat the same value to have at least 30 items
46+
val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt()
47+
waveForm.map { value ->
48+
repeat(repeatTimes) {
49+
sizeInRangeList.add(value)
50+
}
51+
}
52+
}
53+
waveForm.size > MAX_NUMBER_OF_VALUES -> {
54+
val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt()
55+
waveForm.mapIndexed { idx, value ->
56+
if (idx % keepOneOf == 0) {
57+
sizeInRangeList.add(value)
58+
}
59+
}
60+
}
61+
else -> {
62+
sizeInRangeList.addAll(waveForm)
63+
}
64+
}
65+
66+
// OK, ensure all items are positive
67+
val positiveList = sizeInRangeList.map {
68+
abs(it)
69+
}
70+
71+
// Ensure max is not above MAX_VALUE
72+
val max = positiveList.maxOrNull() ?: MAX_VALUE
73+
74+
val finalList = if (max > MAX_VALUE) {
75+
// Reduce the values
76+
positiveList.map {
77+
it * MAX_VALUE / max
78+
}
79+
} else {
80+
positiveList
81+
}
82+
83+
Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items. Max value was $max")
84+
return finalList
85+
}
86+
}

0 commit comments

Comments
 (0)