Skip to content

Commit 16bb4d1

Browse files
Fix large message size calculation to use bytes.
1 parent e434cda commit 16bb4d1

File tree

12 files changed

+225
-119
lines changed

12 files changed

+225
-119
lines changed

app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,8 +1052,10 @@ private void setBodyText(@NonNull MessageRecord messageRecord,
10521052

10531053
if (hasExtraText(messageRecord)) {
10541054
bodyText.setOverflowText(getLongMessageSpan(messageRecord));
1055+
bodyText.setMaxLength(messageRecord.getBody().length() - 2);
10551056
} else {
10561057
bodyText.setOverflowText(null);
1058+
bodyText.setMaxLength(messageRecord.getBody().length());
10571059
}
10581060

10591061
if (messageRecord.isOutgoing()) {

app/src/main/java/org/thoughtcrime/securesms/conversation/MessageSendType.kt

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@ package org.thoughtcrime.securesms.conversation
22

33
import android.content.Context
44
import android.os.Parcelable
5-
import androidx.annotation.ColorRes
65
import androidx.annotation.DrawableRes
76
import androidx.annotation.StringRes
87
import kotlinx.parcelize.Parcelize
98
import org.thoughtcrime.securesms.R
10-
import org.thoughtcrime.securesms.util.CharacterCalculator
11-
import org.thoughtcrime.securesms.util.PushCharacterCalculator
12-
import java.lang.IllegalArgumentException
139

1410
/**
1511
* The kinds of messages you can send, e.g. a plain Signal message, an SMS message, etc.
@@ -22,22 +18,14 @@ sealed class MessageSendType(
2218
val composeHintRes: Int,
2319
@DrawableRes
2420
val buttonDrawableRes: Int,
25-
@DrawableRes
26-
val menuDrawableRes: Int,
27-
@ColorRes
28-
val backgroundColorRes: Int,
2921
val transportType: TransportType,
30-
val characterCalculator: CharacterCalculator
22+
val maxBodyByteSize: Int
3123
) : Parcelable {
3224

3325
@get:JvmName("usesSignalTransport")
3426
val usesSignalTransport
3527
get() = transportType == TransportType.SIGNAL
3628

37-
fun calculateCharacters(body: String): CharacterCalculator.CharacterState {
38-
return characterCalculator.calculateCharacters(body)
39-
}
40-
4129
open fun getTitle(context: Context): String {
4230
return context.getString(titleRes)
4331
}
@@ -50,26 +38,12 @@ sealed class MessageSendType(
5038
titleRes = R.string.ConversationActivity_send_message_content_description,
5139
composeHintRes = R.string.conversation_activity__type_message_push,
5240
buttonDrawableRes = R.drawable.ic_send_lock_24,
53-
menuDrawableRes = R.drawable.ic_secure_24,
54-
backgroundColorRes = R.color.core_ultramarine,
5541
transportType = TransportType.SIGNAL,
56-
characterCalculator = PushCharacterCalculator()
42+
maxBodyByteSize = 2048
5743
)
5844

5945
enum class TransportType {
6046
SIGNAL,
6147
SMS
6248
}
63-
64-
companion object {
65-
@JvmStatic
66-
fun getAllAvailable(): List<MessageSendType> {
67-
return listOf(SignalMessageSendType)
68-
}
69-
70-
@JvmStatic
71-
fun getFirstForTransport(transportType: TransportType): MessageSendType {
72-
return getAllAvailable().firstOrNull { it.transportType == transportType } ?: throw IllegalArgumentException("No options available for desired type $transportType!")
73-
}
74-
}
7549
}

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,25 +1281,6 @@ class ConversationFragment :
12811281
}
12821282
}
12831283

1284-
private fun calculateCharactersRemaining() {
1285-
val messageBody: String = binding.conversationInputPanel.embeddedTextEditor.textTrimmed.toString()
1286-
val charactersLeftView: TextView = binding.conversationInputSpaceLeft
1287-
val characterState = MessageSendType.SignalMessageSendType.calculateCharacters(messageBody)
1288-
1289-
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
1290-
charactersLeftView.text = String.format(
1291-
Locale.getDefault(),
1292-
"%d/%d (%d)",
1293-
characterState.charactersRemaining,
1294-
characterState.maxTotalMessageSize,
1295-
characterState.messagesSpent
1296-
)
1297-
charactersLeftView.visibility = View.VISIBLE
1298-
} else {
1299-
charactersLeftView.visibility = View.GONE
1300-
}
1301-
}
1302-
13031284
private fun registerForResults() {
13041285
addToContactsLauncher = registerForActivityResult(AddToContactsContract()) {}
13051286
conversationActivityResultContracts = ConversationActivityResultContracts(this, ActivityResultCallbacks())
@@ -4020,7 +4001,6 @@ class ConversationFragment :
40204001
}
40214002

40224003
override fun afterTextChanged(s: Editable) {
4023-
calculateCharactersRemaining()
40244004
if (composeText.textTrimmed.isEmpty() || beforeLength == 0) {
40254005
composeText.postDelayed({
40264006
if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiStrings
4040
import org.thoughtcrime.securesms.contactshare.Contact
4141
import org.thoughtcrime.securesms.contactshare.ContactUtil
4242
import org.thoughtcrime.securesms.conversation.ConversationMessage
43-
import org.thoughtcrime.securesms.conversation.MessageSendType
4443
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
4544
import org.thoughtcrime.securesms.conversation.v2.RequestReviewState.GroupReviewState
4645
import org.thoughtcrime.securesms.conversation.v2.RequestReviewState.IndividualReviewState
@@ -186,8 +185,7 @@ class ConversationRepository(
186185
val sendCompletable = Completable.create { emitter ->
187186
val splitMessage: MessageUtil.SplitResult = MessageUtil.getSplitMessage(
188187
applicationContext,
189-
body,
190-
MessageSendType.SignalMessageSendType.calculateCharacters(body).maxPrimaryMessageSize
188+
body
191189
)
192190

193191
val outgoingMessageSlideDeck: SlideDeck? = splitMessage.textSlide.map {

app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class MediaSelectionRepository(context: Context) {
169169
)
170170
)
171171
} else {
172-
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, sendType.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
172+
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody)
173173
val splitBody = splitMessage.body
174174

175175
if (splitMessage.textSlide.isPresent) {
@@ -325,7 +325,7 @@ class MediaSelectionRepository(context: Context) {
325325
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.contentType + "'. Skipping.")
326326
}
327327
}
328-
val splitMessage = MessageUtil.getSplitMessage(context, body, sendType.calculateCharacters(body).maxPrimaryMessageSize)
328+
val splitMessage = MessageUtil.getSplitMessage(context, body)
329329
val splitBody = splitMessage.body
330330
if (splitMessage.textSlide.isPresent) {
331331
slideDeck.addSlide(splitMessage.textSlide.get())

app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.thoughtcrime.securesms.util.MessageUtil;
5252
import org.thoughtcrime.securesms.util.Util;
5353

54+
import java.nio.charset.StandardCharsets;
5455
import java.util.ArrayList;
5556
import java.util.Collection;
5657
import java.util.Collections;
@@ -62,6 +63,8 @@
6263
import java.util.concurrent.TimeUnit;
6364
import java.util.stream.Collectors;
6465

66+
import okio.Utf8;
67+
6568
/**
6669
* MultiShareSender encapsulates send logic (stolen from {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
6770
* and provides a means to:
@@ -113,8 +116,7 @@ public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs mu
113116
List<Contact> contacts = multiShareArgs.getSharedContacts();
114117
SlideDeck slideDeck = new SlideDeck(primarySlideDeck);
115118

116-
boolean needsSplit = message != null &&
117-
message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
119+
boolean needsSplit = message != null && Utf8.size(message) > MessageUtil.MAX_MESSAGE_SIZE_BYTES;
118120
boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() ||
119121
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
120122
multiShareArgs.getStickerLocator() != null ||
@@ -196,7 +198,7 @@ private static void sendMediaMessageOrCollectStoryToBatch(@NonNull Context conte
196198
{
197199
String body = multiShareArgs.getDraftText();
198200
if (sendType.usesSignalTransport() && body != null) {
199-
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, sendType.calculateCharacters(body).maxPrimaryMessageSize);
201+
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body);
200202
body = splitMessage.getBody();
201203

202204
if (splitMessage.getTextSlide().isPresent()) {

app/src/main/java/org/thoughtcrime/securesms/util/MessageUtil.java

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.thoughtcrime.securesms.util
2+
3+
import android.content.Context
4+
import org.signal.core.util.splitByByteLength
5+
import org.thoughtcrime.securesms.mms.TextSlide
6+
import org.thoughtcrime.securesms.providers.BlobProvider
7+
import java.text.SimpleDateFormat
8+
import java.util.Date
9+
import java.util.Locale
10+
import java.util.Optional
11+
12+
object MessageUtil {
13+
const val MAX_MESSAGE_SIZE_BYTES: Int = 2000 // Technically 2048, but we'll play it a little safe
14+
15+
/**
16+
* @return If the message is longer than the allowed text size, this will return trimmed text with
17+
* an accompanying TextSlide. Otherwise it'll just return the original text.
18+
*/
19+
@JvmStatic
20+
fun getSplitMessage(context: Context, rawText: String): SplitResult {
21+
val (trimmed, remainder) = rawText.splitByByteLength(MAX_MESSAGE_SIZE_BYTES)
22+
23+
return if (remainder != null) {
24+
val textData = rawText.toByteArray()
25+
val timestamp = SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(Date())
26+
val filename = String.format("signal-%s.txt", timestamp)
27+
val textUri = BlobProvider.getInstance()
28+
.forData(textData)
29+
.withMimeType(MediaUtil.LONG_TEXT)
30+
.withFileName(filename)
31+
.createForSingleSessionInMemory()
32+
33+
val textSlide = Optional.of(TextSlide(context, textUri, filename, textData.size.toLong()))
34+
35+
SplitResult(trimmed, textSlide)
36+
} else {
37+
SplitResult(trimmed, Optional.empty())
38+
}
39+
}
40+
41+
data class SplitResult(
42+
val body: String,
43+
val textSlide: Optional<TextSlide>
44+
)
45+
}

core-util-jvm/src/main/java/org/signal/core/util/StringExtensions.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55

66
package org.signal.core.util
77

8+
import okio.utf8Size
9+
import org.signal.core.util.logging.Log
810
import java.net.URLEncoder
11+
import java.nio.ByteBuffer
12+
import java.nio.CharBuffer
913
import java.nio.charset.StandardCharsets
1014
import kotlin.contracts.ExperimentalContracts
1115
import kotlin.contracts.contract
1216

17+
private const val TAG: String = "StringExtensions"
18+
1319
/**
1420
* Treats the string as a serialized list of tokens and tells you if an item is present in the list.
1521
* In addition to exact matches, this handles wildcards at the end of an item.
@@ -79,3 +85,37 @@ fun CharSequence?.isNotNullOrBlank(): Boolean {
7985
fun String.urlEncode(): String {
8086
return URLEncoder.encode(this, StandardCharsets.UTF_8.name())
8187
}
88+
89+
/**
90+
* Splits a string into two parts, such that the first part will be at most [byteLength] bytes long.
91+
92+
* The first item of the pair will be the shortened string, and the second item will be the remainder.
93+
* Appending the two parts together will give you back the original string.
94+
*
95+
* If the input string is already less than [byteLength] bytes, the second item will be null.
96+
*/
97+
fun String.splitByByteLength(byteLength: Int): Pair<String, String?> {
98+
if (this.utf8Size() <= byteLength) {
99+
return this to null
100+
}
101+
102+
val charBuffer = CharBuffer.wrap(this)
103+
val encoder = Charsets.UTF_8.newEncoder()
104+
val outputBuffer = ByteBuffer.allocate(byteLength)
105+
106+
encoder.encode(charBuffer, outputBuffer, true)
107+
charBuffer.flip()
108+
109+
var firstPart = charBuffer.toString()
110+
111+
// Unfortunately some Android implementations will cause the charBuffer to go a step beyond what it should.
112+
// It's always extremely close (in testing, only ever off by 1), but as a workaround, we chop off characters
113+
// at the end until it fits. Bummer.
114+
while (firstPart.utf8Size() > byteLength) {
115+
Log.w(TAG, "Had to chop off a character to make it fit under the byte limit.")
116+
firstPart = firstPart.substring(0, firstPart.length - 1)
117+
}
118+
119+
val remainder = this.substring(firstPart.length)
120+
return firstPart to remainder
121+
}

0 commit comments

Comments
 (0)