mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 19:18:37 +00:00
Add data seeding playground.
This commit is contained in:
@@ -218,6 +218,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from("Data Seeding Playground"),
|
||||||
|
summary = DSLSettingsText.from("Seed conversations with media files from a folder."),
|
||||||
|
onClick = {
|
||||||
|
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToDataSeedingPlaygroundFragment())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dividerPref()
|
dividerPref()
|
||||||
|
|
||||||
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
|
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.internal.dataseeding
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import org.signal.core.ui.compose.Previews
|
||||||
|
import org.signal.core.ui.compose.Rows
|
||||||
|
import org.signal.core.ui.compose.SignalPreview
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
|
|
||||||
|
class DataSeedingPlaygroundFragment : ComposeFragment() {
|
||||||
|
|
||||||
|
private val viewModel: DataSeedingPlaygroundViewModel by viewModels()
|
||||||
|
private lateinit var selectFolderLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
selectFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
viewModel.selectFolder(uri)
|
||||||
|
} ?: Toast.makeText(requireContext(), "No folder selected", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun FragmentContent() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadThreads()
|
||||||
|
}
|
||||||
|
|
||||||
|
Screen(
|
||||||
|
state = state,
|
||||||
|
onBack = { findNavController().popBackStack() },
|
||||||
|
onSelectFolderClicked = {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
selectFolderLauncher.launch(intent)
|
||||||
|
},
|
||||||
|
onThreadSelectionChanged = { threadId, isSelected ->
|
||||||
|
viewModel.toggleThreadSelection(threadId, isSelected)
|
||||||
|
},
|
||||||
|
onSeedDataClicked = {
|
||||||
|
viewModel.seedData(
|
||||||
|
onComplete = {
|
||||||
|
Toast.makeText(context, "Data seeding completed!", Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
Toast.makeText(context, "Error: $error", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Screen(
|
||||||
|
state: DataSeedingPlaygroundState,
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onSelectFolderClicked: () -> Unit = {},
|
||||||
|
onThreadSelectionChanged: (Long, Boolean) -> Unit = { _, _ -> },
|
||||||
|
onSeedDataClicked: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Data Seeding Playground")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Folder selection section
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Media Folder",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if (state.selectedFolderPath.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Selected: ${state.selectedFolderPath}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Media files found: ${state.mediaFiles.size}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "No folder selected",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Rows.TextRow(
|
||||||
|
text = "Select Media Folder",
|
||||||
|
label = "Choose a folder containing photos and videos to seed into conversations.",
|
||||||
|
onClick = onSelectFolderClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread selection section
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Conversation Threads (${state.selectedThreads.size} selected)",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.height(300.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
items(state.threads) { thread ->
|
||||||
|
ThreadSelectionRow(
|
||||||
|
thread = thread,
|
||||||
|
isSelected = state.selectedThreads.contains(thread.threadId),
|
||||||
|
onSelectionChanged = { isSelected ->
|
||||||
|
onThreadSelectionChanged(thread.threadId, isSelected)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action section
|
||||||
|
if (state.mediaFiles.isNotEmpty() && state.selectedThreads.isNotEmpty()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Ready to Seed Data",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "This will send ${state.mediaFiles.size} media files to ${state.selectedThreads.size} conversations in a round-robin fashion.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Rows.TextRow(
|
||||||
|
text = "Seed Data",
|
||||||
|
label = "Send the selected media files to the selected conversations.",
|
||||||
|
onClick = {
|
||||||
|
showConfirmDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation dialog
|
||||||
|
if (showConfirmDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showConfirmDialog = false },
|
||||||
|
title = { Text("Confirm Data Seeding") },
|
||||||
|
text = {
|
||||||
|
Text("Are you sure you want to send ${state.mediaFiles.size} media files to ${state.selectedThreads.size} conversations? This action cannot be undone.")
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showConfirmDialog = false
|
||||||
|
onSeedDataClicked()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Seed Data")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showConfirmDialog = false }
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ThreadSelectionRow(
|
||||||
|
thread: ThreadRecord,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelectionChanged: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = onSelectionChanged
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = thread.recipient.getDisplayName(LocalContext.current),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
if (thread.body.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = thread.body,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
fun PreviewScreen() {
|
||||||
|
Previews.Preview {
|
||||||
|
Screen(
|
||||||
|
state = DataSeedingPlaygroundState(
|
||||||
|
threads = emptyList(),
|
||||||
|
selectedThreads = emptySet(),
|
||||||
|
mediaFiles = emptyList(),
|
||||||
|
selectedFolderPath = "/storage/emulated/0/Pictures"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
fun PreviewScreenWithData() {
|
||||||
|
Previews.Preview {
|
||||||
|
Screen(
|
||||||
|
state = DataSeedingPlaygroundState(
|
||||||
|
threads = emptyList(),
|
||||||
|
selectedThreads = setOf(1L, 2L),
|
||||||
|
mediaFiles = listOf("photo1.jpg", "video1.mp4", "photo2.jpg"),
|
||||||
|
selectedFolderPath = "/storage/emulated/0/Pictures"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.internal.dataseeding
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
|
||||||
|
class DataSeedingPlaygroundViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(DataSeedingPlaygroundViewModel::class.java)
|
||||||
|
private const val MAX_RECENT_THREADS = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(DataSeedingPlaygroundState())
|
||||||
|
val state: StateFlow<DataSeedingPlaygroundState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun loadThreads() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val threads = mutableListOf<ThreadRecord>()
|
||||||
|
val cursor: Cursor = SignalDatabase.threads.getRecentConversationList(
|
||||||
|
limit = MAX_RECENT_THREADS,
|
||||||
|
includeInactiveGroups = false,
|
||||||
|
hideV1Groups = true
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.use {
|
||||||
|
val reader = SignalDatabase.threads.readerFor(it)
|
||||||
|
var threadRecord = reader.getNext()
|
||||||
|
while (threadRecord != null) {
|
||||||
|
threads.add(threadRecord)
|
||||||
|
threadRecord = reader.getNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.value = _state.value.copy(threads = threads)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to load threads", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFolder(uri: Uri) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val context = getApplication<Application>()
|
||||||
|
val documentFile = DocumentFile.fromTreeUri(context, uri)
|
||||||
|
|
||||||
|
if (documentFile != null && documentFile.isDirectory) {
|
||||||
|
val mediaFiles = findMediaFiles(documentFile)
|
||||||
|
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
selectedFolderPath = documentFile.uri.toString(),
|
||||||
|
mediaFiles = mediaFiles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to select folder", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleThreadSelection(threadId: Long, isSelected: Boolean) {
|
||||||
|
val selectedThreads = _state.value.selectedThreads.toMutableSet()
|
||||||
|
if (isSelected) {
|
||||||
|
selectedThreads.add(threadId)
|
||||||
|
} else {
|
||||||
|
selectedThreads.remove(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.value = _state.value.copy(selectedThreads = selectedThreads)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
fun seedData(onComplete: () -> Unit, onError: (String) -> Unit) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val context = getApplication<Application>()
|
||||||
|
val currentState = _state.value
|
||||||
|
|
||||||
|
if (currentState.mediaFiles.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError("No media files selected")
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState.selectedThreads.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError("No threads selected")
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaFiles = currentState.mediaFiles
|
||||||
|
val threadIds = currentState.selectedThreads.toList()
|
||||||
|
var currentThreadIndex = 0
|
||||||
|
|
||||||
|
for (mediaFile in mediaFiles) {
|
||||||
|
val threadId = threadIds[currentThreadIndex % threadIds.size]
|
||||||
|
sendMediaToThread(context, mediaFile, threadId)
|
||||||
|
currentThreadIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to seed data", e)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findMediaFiles(directory: DocumentFile): List<String> {
|
||||||
|
val mediaFiles = mutableListOf<String>()
|
||||||
|
|
||||||
|
directory.listFiles().forEach { file ->
|
||||||
|
if (file.isFile && file.type != null) {
|
||||||
|
val mimeType = file.type!!
|
||||||
|
if (MediaUtil.isImageType(mimeType) || MediaUtil.isVideoType(mimeType)) {
|
||||||
|
mediaFiles.add(file.name ?: "unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendMediaToThread(context: Context, mediaFileName: String, threadId: Long) {
|
||||||
|
try {
|
||||||
|
// Find the actual file URI
|
||||||
|
val currentState = _state.value
|
||||||
|
val documentFile = DocumentFile.fromTreeUri(context, Uri.parse(currentState.selectedFolderPath))
|
||||||
|
|
||||||
|
if (documentFile != null) {
|
||||||
|
val mediaFile = documentFile.listFiles().find { it.name == mediaFileName }
|
||||||
|
|
||||||
|
if (mediaFile != null && mediaFile.uri != null) {
|
||||||
|
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
|
||||||
|
|
||||||
|
if (recipient != null) {
|
||||||
|
val mimeType = mediaFile.type ?: MediaUtil.getCorrectedMimeType(mediaFileName)
|
||||||
|
val attachment: Attachment = UriAttachment(
|
||||||
|
uri = mediaFile.uri,
|
||||||
|
contentType = mimeType,
|
||||||
|
transferState = AttachmentTable.TRANSFER_PROGRESS_STARTED,
|
||||||
|
size = mediaFile.length(),
|
||||||
|
fileName = mediaFileName,
|
||||||
|
voiceNote = false,
|
||||||
|
borderless = false,
|
||||||
|
videoGif = false,
|
||||||
|
quote = false,
|
||||||
|
quoteTargetContentType = null,
|
||||||
|
caption = null,
|
||||||
|
stickerLocator = null,
|
||||||
|
blurHash = null,
|
||||||
|
audioHash = null,
|
||||||
|
transformProperties = null
|
||||||
|
)
|
||||||
|
|
||||||
|
val message = OutgoingMessage(
|
||||||
|
threadRecipient = recipient,
|
||||||
|
body = "",
|
||||||
|
attachments = listOf(attachment),
|
||||||
|
sentTimeMillis = System.currentTimeMillis(),
|
||||||
|
isSecure = true
|
||||||
|
)
|
||||||
|
|
||||||
|
MessageSender.send(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
threadId,
|
||||||
|
MessageSender.SendType.SIGNAL,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to send media to thread $threadId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DataSeedingPlaygroundState(
|
||||||
|
val threads: List<ThreadRecord> = emptyList(),
|
||||||
|
val selectedThreads: Set<Long> = emptySet(),
|
||||||
|
val mediaFiles: List<String> = emptyList(),
|
||||||
|
val selectedFolderPath: String = ""
|
||||||
|
)
|
||||||
@@ -791,6 +791,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_internalSettingsFragment_to_internalSqlitePlaygroundFragment"
|
android:id="@+id/action_internalSettingsFragment_to_internalSqlitePlaygroundFragment"
|
||||||
app:destination="@id/internalSqlitePlaygroundFragment" />
|
app:destination="@id/internalSqlitePlaygroundFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_internalSettingsFragment_to_dataSeedingPlaygroundFragment"
|
||||||
|
app:destination="@id/dataSeedingPlaygroundFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@@ -853,6 +856,11 @@
|
|||||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.sqlite.InternalSqlitePlaygroundFragment"
|
android:name="org.thoughtcrime.securesms.components.settings.app.internal.sqlite.InternalSqlitePlaygroundFragment"
|
||||||
android:label="internal_sqlite_playground_fragment" />
|
android:label="internal_sqlite_playground_fragment" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/dataSeedingPlaygroundFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.internal.dataseeding.DataSeedingPlaygroundFragment"
|
||||||
|
android:label="data_seeding_playground_fragment" />
|
||||||
|
|
||||||
<!-- endregion -->
|
<!-- endregion -->
|
||||||
|
|
||||||
<!-- App updates -->
|
<!-- App updates -->
|
||||||
|
|||||||
Reference in New Issue
Block a user