-
-
Notifications
You must be signed in to change notification settings - Fork 6k
/
DraftRepository.kt
297 lines (257 loc) · 12.9 KB
/
DraftRepository.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package org.thoughtcrime.securesms.conversation.drafts
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import androidx.annotation.WorkerThread
import com.bumptech.glide.load.engine.DiskCacheStrategy
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.StreamUtil
import org.signal.core.util.concurrent.MaybeCompat
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.location.SignalPlace
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
import org.thoughtcrime.securesms.conversation.MessageStyler
import org.thoughtcrime.securesms.database.DraftTable
import org.thoughtcrime.securesms.database.DraftTable.Drafts
import org.thoughtcrime.securesms.database.MentionUtil
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.adjustBodyRanges
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.QuoteId
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideFactory
import org.thoughtcrime.securesms.mms.StickerSlide
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
import org.thoughtcrime.securesms.util.hasTextSlide
import org.thoughtcrime.securesms.util.requireTextSlide
import java.io.IOException
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class DraftRepository(
private val context: Context = ApplicationDependencies.getApplication(),
private val threadTable: ThreadTable = SignalDatabase.threads,
private val draftTable: DraftTable = SignalDatabase.drafts,
private val saveDraftsExecutor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED),
private val conversationArguments: ConversationIntents.Args? = null
) {
companion object {
val TAG = Log.tag(DraftRepository::class.java)
}
fun getShareOrDraftData(): Maybe<Pair<ShareOrDraftData?, Drafts?>> {
return MaybeCompat.fromCallable { getShareOrDraftDataInternal() }
.observeOn(Schedulers.io())
}
/**
* Loads share data from the intent and draft data from the database and provides a one-spot initial
* load of data.
*
* Note: Voice note drafts are handled differently and via the [DraftViewModel.state]
*/
private fun getShareOrDraftDataInternal(): Pair<ShareOrDraftData?, Drafts?>? {
val shareText = conversationArguments?.draftText
val shareMedia = conversationArguments?.draftMedia
val shareContentType = conversationArguments?.draftContentType
val shareMediaType = conversationArguments?.draftMediaType
val shareMediaList = conversationArguments?.media ?: emptyList()
val stickerLocator = conversationArguments?.stickerLocator
val borderless = conversationArguments?.isBorderless ?: false
if (stickerLocator != null && shareMedia != null) {
val slide = StickerSlide(context, shareMedia, 0, stickerLocator, shareContentType!!)
return ShareOrDraftData.SendSticker(slide) to null
}
if (shareMedia != null && shareContentType != null && borderless) {
val details = getKeyboardImageDetails(GlideApp.with(context), shareMedia)
if (details == null || !details.hasTransparency) {
return ShareOrDraftData.SetMedia(shareMedia, shareMediaType!!, null) to null
}
val slide: Slide? = if (MediaUtil.isGif(shareContentType)) {
GifSlide(context, shareMedia, 0, details.width, details.height, true, null)
} else if (MediaUtil.isImageType(shareContentType)) {
ImageSlide(context, shareMedia, shareContentType, 0, details.width, details.height, true, null, null)
} else {
Log.w(TAG, "Attempting to send unsupported non-image via keyboard share")
null
}
return if (slide != null) ShareOrDraftData.SendKeyboardImage(slide) to null else null
}
if (shareMediaList.isNotEmpty()) {
return ShareOrDraftData.StartSendMedia(shareMediaList, shareText) to null
}
if (shareMedia != null && shareMediaType != null) {
return ShareOrDraftData.SetMedia(shareMedia, shareMediaType, shareText) to null
}
if (shareText != null) {
return ShareOrDraftData.SetText(shareText) to null
}
if (conversationArguments?.canInitializeFromDatabase() == true) {
val (drafts, updatedText) = loadDraftsInternal(conversationArguments.threadId)
val draftText: CharSequence? = drafts.firstOrNull { it.type == DraftTable.Draft.TEXT }?.let { updatedText ?: it.value }
val location: SignalPlace? = drafts.firstOrNull { it.type == DraftTable.Draft.LOCATION }?.let { SignalPlace.deserialize(it.value) }
if (location != null) {
return ShareOrDraftData.SetLocation(location, draftText) to drafts
}
val quote: ConversationMessage? = drafts.firstOrNull { it.type == DraftTable.Draft.QUOTE }?.let { loadDraftQuoteInternal(it.value) }
if (quote != null) {
return ShareOrDraftData.SetQuote(quote, draftText) to drafts
}
val messageEdit: ConversationMessage? = drafts.firstOrNull { it.type == DraftTable.Draft.MESSAGE_EDIT }?.let { loadDraftMessageEditInternal(it.value) }
if (messageEdit != null) {
return ShareOrDraftData.SetEditMessage(messageEdit, draftText) to drafts
}
if (draftText != null) {
return ShareOrDraftData.SetText(draftText) to drafts
}
return null to drafts
}
// no share or draft
return null
}
fun deleteVoiceNoteDraftData(draft: DraftTable.Draft?) {
if (draft != null) {
SignalExecutors.BOUNDED.execute {
BlobProvider.getInstance().delete(context, Uri.parse(draft.value).buildUpon().clearQuery().build())
}
}
}
fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) {
require(threadId != -1L || recipient != null)
saveDraftsExecutor.execute {
if (drafts.isNotEmpty()) {
val actualThreadId = if (threadId == -1L) {
threadTable.getOrCreateThreadIdFor(recipient!!, distributionType)
} else {
threadId
}
draftTable.replaceDrafts(actualThreadId, drafts)
if (drafts.shouldUpdateSnippet()) {
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
} else {
threadTable.update(actualThreadId, unarchive = false, allowDeletion = false)
}
} else if (threadId > 0) {
draftTable.clearDrafts(threadId)
threadTable.update(threadId, unarchive = false, allowDeletion = false)
}
}
}
@Deprecated("Not needed for CFv2")
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
return Single.fromCallable {
loadDraftsInternal(threadId)
}.subscribeOn(Schedulers.io())
}
private fun loadDraftsInternal(threadId: Long): DatabaseDraft {
val drafts: Drafts = draftTable.getDrafts(threadId)
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
val textDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.TEXT)
var updatedText: Spannable? = null
if (textDraft != null && bodyRangesDraft != null) {
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, textDraft.value, mentions)
updatedText = SpannableString(updated.body)
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
MessageStyler.style(id = MessageStyler.DRAFT_ID, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
}
return DatabaseDraft(drafts, updatedText)
}
@Deprecated("Not needed for CFv2")
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) }
}
private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? {
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
if (it is MediaMmsMessageRecord) {
it.withAttachments(context, SignalDatabase.attachments.getAttachmentsForMessage(it.id))
} else {
it
}
} ?: return null
val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
}
@Deprecated("Not needed for CFv2")
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) }
}
private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? {
val messageId = MessageId.deserialize(serialized)
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null
val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId))
if (messageRecord.hasTextSlide()) {
val textSlide = messageRecord.requireTextSlide()
if (textSlide.uri != null) {
try {
PartAuthority.getAttachmentStream(context, textSlide.uri!!).use { stream ->
val body = StreamUtil.readFullyAsString(stream)
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body, threadRecipient)
}
} catch (e: IOException) {
Log.e(TAG, "Failed to load text slide", e)
}
}
}
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
}
@WorkerThread
private fun getKeyboardImageDetails(glideRequests: GlideRequests, uri: Uri): KeyboardImageDetails? {
return try {
val bitmap: Bitmap = glideRequests.asBitmap()
.load(DecryptableUri(uri))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.submit()
.get(1000, TimeUnit.MILLISECONDS)
val topLeft = bitmap.getPixel(0, 0)
KeyboardImageDetails(bitmap.width, bitmap.height, Color.alpha(topLeft) < 255)
} catch (e: InterruptedException) {
null
} catch (e: ExecutionException) {
null
} catch (e: TimeoutException) {
null
}
}
data class DatabaseDraft(val drafts: Drafts, val updatedText: CharSequence?)
sealed interface ShareOrDraftData {
data class SendSticker(val slide: Slide) : ShareOrDraftData
data class SendKeyboardImage(val slide: Slide) : ShareOrDraftData
data class StartSendMedia(val mediaList: List<Media>, val text: CharSequence?) : ShareOrDraftData
data class SetMedia(val media: Uri, val mediaType: SlideFactory.MediaType, val text: CharSequence?) : ShareOrDraftData
data class SetText(val text: CharSequence) : ShareOrDraftData
data class SetLocation(val location: SignalPlace, val draftText: CharSequence?) : ShareOrDraftData
data class SetQuote(val quote: ConversationMessage, val draftText: CharSequence?) : ShareOrDraftData
data class SetEditMessage(val messageEdit: ConversationMessage, val draftText: CharSequence?) : ShareOrDraftData
}
data class KeyboardImageDetails(val width: Int, val height: Int, val hasTransparency: Boolean)
}