Skip to content

Commit 0536bda

Browse files
Merge pull request #1758 from session-foundation/fix/notification-in-conversation
Multi Share
2 parents 9074fd0 + 526e1bf commit 0536bda

File tree

7 files changed

+279
-175
lines changed

7 files changed

+279
-175
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@
218218
<data android:mimeType="text/*" />
219219
<data android:mimeType="*/*" />
220220
</intent-filter>
221+
<intent-filter>
222+
<action android:name="android.intent.action.SEND_MULTIPLE" />
223+
<category android:name="android.intent.category.DEFAULT" />
224+
<data android:mimeType="audio/*" />
225+
<data android:mimeType="image/*" />
226+
<data android:mimeType="text/plain" />
227+
<data android:mimeType="video/*" />
228+
<data android:mimeType="application/*" />
229+
<data android:mimeType="text/*" />
230+
<data android:mimeType="*/*" />
231+
</intent-filter>
221232
<meta-data
222233
android:name="android.service.chooser.chooser_target_service"
223234
android:value=".service.DirectShareService" />

app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,6 @@ class ShareActivity : FullComposeScreenLockActivity() {
7474
}
7575

7676
private fun initializeMedia() {
77-
val streamExtra = intent.getParcelableExtra<Uri?>(Intent.EXTRA_STREAM)
78-
var charSequenceExtra: CharSequence? = null
79-
try {
80-
charSequenceExtra = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
81-
}
82-
catch (e: Exception) {
83-
// It's not necessarily an issue if there's no text extra when sharing files - but we do
84-
// have to catch any failed attempt.
85-
}
86-
87-
viewModel.initialiseMedia(streamExtra, charSequenceExtra, intent)
77+
viewModel.initialiseMedia(intent)
8878
}
8979
}

app/src/main/java/org/thoughtcrime/securesms/ShareViewModel.kt

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ import javax.inject.Inject
4343

4444
@HiltViewModel
4545
class ShareViewModel @Inject constructor(
46-
@param:ApplicationContext private val context: Context,
46+
@ApplicationContext private val context: Context,
4747
private val avatarUtils: AvatarUtils,
4848
private val deprecationManager: LegacyGroupDeprecationManager,
4949
conversationRepository: ConversationRepository,
5050
): ViewModel(){
51+
5152
private val TAG = ShareViewModel::class.java.simpleName
5253

53-
private var resolvedExtra: Uri? = null
54+
private var resolvedExtras: List<Uri> = emptyList()
5455
private var resolvedPlaintext: CharSequence? = null
5556
private var mimeType: String? = null
5657
private var isPassingAlongMedia = false
@@ -64,8 +65,8 @@ class ShareViewModel @Inject constructor(
6465
@OptIn(FlowPreview::class)
6566
val contacts: StateFlow<List<ConversationItem>> = combine(
6667
conversationRepository.observeConversationList(),
67-
mutableSearchQuery.debounce(100L),
68-
::filterContacts
68+
mutableSearchQuery.debounce(100L),
69+
::filterContacts
6970
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
7071

7172
val hasAnyConversations: StateFlow<Boolean?> =
@@ -79,8 +80,6 @@ class ShareViewModel @Inject constructor(
7980
private val _uiState = MutableStateFlow(UIState(false))
8081
val uiState: StateFlow<UIState> get() = _uiState
8182

82-
83-
8483
private fun filterContacts(
8584
threads: List<ThreadRecord>,
8685
query: String,
@@ -114,10 +113,9 @@ class ShareViewModel @Inject constructor(
114113
.thenByDescending { it.lastMessage?.timestamp } // then order by last message time
115114
).map { thread ->
116115
val recipient = thread.recipient
117-
118116
ConversationItem(
119117
name = if(recipient.isSelf) context.getString(R.string.noteToSelf)
120-
else recipient.searchName,
118+
else recipient.searchName,
121119
address = recipient.address,
122120
avatarUIData = avatarUtils.getUIDataFromRecipient(recipient),
123121
showProBadge = recipient.shouldShowProBadge
@@ -130,73 +128,96 @@ class ShareViewModel @Inject constructor(
130128
}
131129

132130
fun onPause(): Boolean{
133-
if (!isPassingAlongMedia && resolvedExtra != null) {
134-
BlobUtils.getInstance().delete(context, resolvedExtra!!)
131+
if (!isPassingAlongMedia && resolvedExtras.isNotEmpty()) {
132+
resolvedExtras.forEach { uri ->
133+
BlobUtils.getInstance().delete(context, uri)
134+
}
135135
return true
136136
}
137-
138137
return false
139138
}
140139

141-
fun initialiseMedia(streamExtra: Uri?, charSequenceExtra: CharSequence?, intent: Intent){
140+
fun initialiseMedia(intent: Intent){
141+
// Reset previous state
142+
resolvedExtras = emptyList()
143+
resolvedPlaintext = null
144+
mimeType = null
142145
isPassingAlongMedia = false
143146

144-
mimeType = getMimeType(streamExtra, intent.type)
147+
val action = intent.action
148+
val type = intent.type
149+
val incomingUris = ArrayList<Uri>()
150+
151+
if (Intent.ACTION_SEND == action) {
152+
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { incomingUris.add(it) }
153+
} else if (Intent.ACTION_SEND_MULTIPLE == action) {
154+
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let { incomingUris.addAll(it) }
155+
}
156+
157+
var charSequenceExtra: CharSequence? = null
158+
try {
159+
charSequenceExtra = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
160+
}
161+
catch (e: Exception) {
162+
// Ignore
163+
}
164+
165+
isPassingAlongMedia = false
166+
mimeType = getMimeType(incomingUris.firstOrNull(), type)
145167

146-
if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
168+
if (incomingUris.isNotEmpty() && incomingUris.all { PartAuthority.isLocalUri(it) }) {
147169
isPassingAlongMedia = true
148-
resolvedExtra = streamExtra
170+
resolvedExtras = incomingUris
149171
handleResolvedMedia(intent)
150-
} else if (charSequenceExtra != null && mimeType != null && mimeType!!.startsWith("text/")) {
172+
} else if (
173+
incomingUris.isEmpty() &&
174+
charSequenceExtra != null &&
175+
(mimeType?.startsWith("text/") == true)
176+
) {
151177
resolvedPlaintext = charSequenceExtra
152178
handleResolvedMedia(intent)
153-
} else {
179+
} else if (incomingUris.isNotEmpty()) {
154180
_uiState.update { it.copy(showLoader = true) }
155-
resolveMedia(intent, streamExtra)
181+
resolveMedia(intent, incomingUris)
182+
} else {
183+
_uiState.update { it.copy(showLoader = false) }
156184
}
157185
}
158186

159187
private fun handleResolvedMedia(intent: Intent) {
160188
val address = IntentCompat.getParcelableExtra(intent, ShareActivity.EXTRA_ADDRESS, Address::class.java)
161-
162189
if (address is Address.Conversable) {
163190
createConversation(address)
164191
} else {
165192
_uiState.update { it.copy(showLoader = false) }
166193
}
167194
}
168195

169-
private fun resolveMedia(intent: Intent, vararg uris: Uri?){
196+
private fun resolveMedia(intent: Intent, uris: List<Uri>){
170197
viewModelScope.launch(Dispatchers.Default){
171-
resolvedExtra = getUri(*uris)
198+
resolvedExtras = uris.mapNotNull { processSingleUri(it) }
172199
handleResolvedMedia(intent)
173200
}
174201
}
175202

176-
private fun getUri(vararg uris: Uri?): Uri? {
203+
private fun processSingleUri(uri: Uri): Uri? {
177204
try {
178-
if (uris.size != 1 || uris[0] == null) {
179-
Log.w(TAG, "Invalid URI passed to ResolveMediaTask - bailing.")
180-
return null
181-
} else {
182-
Log.i(TAG, "Resolved URI: " + uris[0]!!.toString() + " - " + uris[0]!!.path)
183-
}
205+
Log.i(TAG, "Resolving URI: " + uri.toString() + " - " + uri.path)
184206

185-
var inputStream = if ("file" == uris[0]!!.scheme) {
186-
FileInputStream(uris[0]!!.path)
207+
val inputStream = if ("file" == uri.scheme) {
208+
FileInputStream(uri.path)
187209
} else {
188-
context.contentResolver.openInputStream(uris[0]!!)
210+
context.contentResolver.openInputStream(uri)
189211
}
190212

191213
if (inputStream == null) {
192214
Log.w(TAG, "Failed to create input stream during ShareActivity - bailing.")
193215
return null
194216
}
195217

196-
val cursor = context.contentResolver.query(uris[0]!!, arrayOf<String>(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null)
218+
val cursor = context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null)
197219
var fileName: String? = null
198220
var fileSize: Long? = null
199-
200221
try {
201222
if (cursor != null && cursor.moveToFirst()) {
202223
try {
@@ -210,10 +231,12 @@ class ShareViewModel @Inject constructor(
210231
cursor?.close()
211232
}
212233

234+
val specificMime = MediaUtil.getMimeType(context, uri) ?: mimeType ?: "application/octet-stream"
235+
213236
return BlobUtils.getInstance()
214237
.forData(inputStream, if (fileSize == null) 0 else fileSize)
215-
.withMimeType(mimeType!!)
216-
.withFileName(fileName!!)
238+
.withMimeType(specificMime)
239+
.withFileName(fileName ?: "unknown")
217240
.createForMultipleSessionsOnDisk(context, BlobUtils.ErrorListener { e: IOException? -> Log.w(TAG, "Failed to write to disk.", e) })
218241
.get()
219242
} catch (ioe: Exception) {
@@ -236,22 +259,26 @@ class ShareViewModel @Inject constructor(
236259
}
237260
}
238261

239-
240262
private fun createConversation(address: Address.Conversable) {
241263
val intent = ConversationActivityV2.createIntent(
242264
context = context,
243265
address = address,
244266
)
245-
246267
intent.applyBaseShare()
247-
248268
isPassingAlongMedia = true
249269
_uiEvents.tryEmit(ShareUIEvent.GoToScreen(intent))
250270
}
251271

252272
private fun Intent.applyBaseShare() {
253-
if (resolvedExtra != null) {
254-
setDataAndType(resolvedExtra, mimeType)
273+
if (resolvedExtras.isNotEmpty()) {
274+
if (resolvedExtras.size == 1) {
275+
action = Intent.ACTION_SEND
276+
setDataAndType(resolvedExtras.first(), mimeType)
277+
} else {
278+
action = Intent.ACTION_SEND_MULTIPLE
279+
type = mimeType ?: "*/*"
280+
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(resolvedExtras))
281+
}
255282
} else if (resolvedPlaintext != null) {
256283
putExtra(Intent.EXTRA_TEXT, resolvedPlaintext)
257284
setType("text/plain")

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

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -545,12 +545,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
545545

546546
// Check if address is null before proceeding with initialization
547547
if (
548-
IntentCompat.getParcelableExtra(
549-
intent,
550-
ADDRESS,
551-
Address.Conversable::class.java
552-
) == null &&
553-
intent.data?.getQueryParameter(ADDRESS).isNullOrEmpty()
548+
IntentCompat.getParcelableExtra(
549+
intent,
550+
ADDRESS,
551+
Address.Conversable::class.java
552+
) == null &&
553+
intent.data?.getQueryParameter(ADDRESS).isNullOrEmpty()
554554
) {
555555
Log.w(TAG, "ConversationActivityV2 launched without ADDRESS extra - Returning home")
556556
val intent = Intent(this, HomeActivity::class.java).apply {
@@ -909,22 +909,22 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
909909
// called from onCreate
910910
private fun setUpToolBar() {
911911
binding.conversationAppBar.setThemedContent {
912-
val data by viewModel.appBarData.collectAsState()
913-
val query by searchViewModel.searchQuery.collectAsState()
914-
915-
ConversationAppBar(
916-
data = data,
917-
onBackPressed = ::finish,
918-
onCallPressed = ::callRecipient,
919-
searchQuery = query ?: "",
920-
onSearchQueryChanged = ::onSearchQueryUpdated,
921-
onSearchQueryClear = { onSearchQueryUpdated("") },
922-
onSearchCanceled = ::onSearchClosed,
923-
onAvatarPressed = {
924-
val intent = ConversationSettingsActivity.createIntent(this, address)
925-
settingsLauncher.launch(intent)
926-
}
927-
)
912+
val data by viewModel.appBarData.collectAsState()
913+
val query by searchViewModel.searchQuery.collectAsState()
914+
915+
ConversationAppBar(
916+
data = data,
917+
onBackPressed = ::finish,
918+
onCallPressed = ::callRecipient,
919+
searchQuery = query ?: "",
920+
onSearchQueryChanged = ::onSearchQueryUpdated,
921+
onSearchQueryClear = { onSearchQueryUpdated("") },
922+
onSearchCanceled = ::onSearchClosed,
923+
onAvatarPressed = {
924+
val intent = ConversationSettingsActivity.createIntent(this, address)
925+
settingsLauncher.launch(intent)
926+
}
927+
)
928928
}
929929
}
930930

@@ -948,6 +948,30 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
948948

949949
// called from onCreate
950950
private fun restoreDraftIfNeeded() {
951+
// Handle Multiple Streams (ACTION_SEND_MULTIPLE)
952+
if (intent.action == Intent.ACTION_SEND_MULTIPLE && intent.hasExtra(Intent.EXTRA_STREAM)) {
953+
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
954+
if (!uris.isNullOrEmpty()) {
955+
val mediaList = uris.mapNotNull { uri ->
956+
val mime = MediaUtil.getMimeType(this, uri)
957+
if (mime != null) {
958+
val filename = FilenameUtils.getFilenameFromUri(this, uri)
959+
Media(uri, filename, mime, 0, 0, 0, 0, null, null)
960+
} else null
961+
}
962+
963+
if (mediaList.isNotEmpty()) {
964+
startActivityForResult(MediaSendActivity.buildEditorIntent(
965+
this,
966+
mediaList,
967+
viewModel.recipient.address,
968+
getMessageBody()
969+
), PICK_FROM_LIBRARY)
970+
return
971+
}
972+
}
973+
}
974+
951975
val mediaURI = intent.data
952976
val mediaType = AttachmentManager.MediaType.from(intent.type)
953977
val mimeType = MediaUtil.getMimeType(this, mediaURI)
@@ -956,7 +980,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
956980
val filename = FilenameUtils.getFilenameFromUri(this, mediaURI)
957981

958982
if (mimeType != null &&
959-
(AttachmentManager.MediaType.IMAGE == mediaType ||
983+
(AttachmentManager.MediaType.IMAGE == mediaType ||
960984
AttachmentManager.MediaType.GIF == mediaType ||
961985
AttachmentManager.MediaType.VIDEO == mediaType)
962986
) {
@@ -1571,8 +1595,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
15711595
title(R.string.block)
15721596
text(
15731597
Phrase.from(context, R.string.blockDescription)
1574-
.put(NAME_KEY, name)
1575-
.format()
1598+
.put(NAME_KEY, name)
1599+
.format()
15761600
)
15771601
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
15781602
viewModel.block()
@@ -1798,7 +1822,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
17981822
// Send it
17991823
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.toString(), emoji, true)
18001824
if (recipient.address is Address.Community) {
1801-
18021825
// Increment the reaction count locally immediately. This
18031826
// has to apply on all the ReactionRecords with the same messageId/emoji per design.
18041827
reactionDb.updateAllCountFor(messageId, emoji, 1)
@@ -1861,7 +1884,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
18611884

18621885

18631886
val messageServerId = lokiMessageDb.getServerID(originalMessage.messageId) ?:
1864-
return Log.w(TAG, "Failed to find message server ID when removing emoji reaction")
1887+
return Log.w(TAG, "Failed to find message server ID when removing emoji reaction")
18651888

18661889
scope.launch {
18671890
runCatching {
@@ -2876,4 +2899,4 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
28762899
}
28772900
}
28782901
}
2879-
}
2902+
}

0 commit comments

Comments
 (0)