This commit is contained in:
fanchao 2024-05-23 13:48:06 +10:00
parent c7c0519a20
commit 90f0caebbd
6 changed files with 121 additions and 113 deletions

View File

@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (hexEncodedSeed == null) {
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
}
val appContext = applicationContext
val loadFileContents: (String) -> String = { fileName ->
MnemonicUtilities.loadFileContents(this, fileName)
MnemonicUtilities.loadFileContents(appContext, fileName)
}
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
}

View File

@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
this.cursor = cursor;
}
public int getLength() {
return cursor == null ? 0 : cursor.getCount();
}
public ThreadRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;

View File

@ -24,7 +24,9 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -81,7 +83,6 @@ import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.themeState
import java.io.IOException
import java.util.Locale
import javax.inject.Inject
@ -99,7 +100,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private lateinit var binding: ActivityHomeBinding
private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
@ -205,18 +205,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up empty state view
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
IP2Country.configureIfNeeded(this@HomeActivity)
startObservingUpdates()
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
}
// Set up new conversation button
binding.newConversationButton.setOnClickListener { showNewConversation() }
// Observe blocked contacts changed events
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
binding.recyclerView.adapter!!.notifyDataSetChanged()
}
}
this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
// subscribe to outdated config updates, this should be removed after long enough time for device migration
lifecycleScope.launch {
@ -227,6 +223,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
// Subscribe to threads and update the UI
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.threads
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
.collectLatest { threads ->
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) {
manager.findViewByPosition(firstPos)?.let { view ->
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0
} else 0
homeAdapter.data = threads
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState()
}
}
}
lifecycleScope.launchWhenStarted {
launch(Dispatchers.IO) {
// Double check that the long poller is up
@ -385,52 +402,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
}
}
// If the theme hasn't changed then start observing updates again (if it does change then we
// will recreate the activity resulting in it responding to changes multiple times)
if (currentThemeState == textSecurePreferences.themeState() && !homeViewModel.getObservable(this).hasActiveObservers()) {
startObservingUpdates()
}
}
override fun onPause() {
super.onPause()
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
homeViewModel.getObservable(this).removeObservers(this)
}
override fun onDestroy() {
val broadcastReceiver = this.broadcastReceiver
if (broadcastReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
}
super.onDestroy()
EventBus.getDefault().unregister(this)
}
// endregion
// region Updating
private fun startObservingUpdates() {
homeViewModel.getObservable(this).observe(this) { newData ->
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) {
manager.findViewByPosition(firstPos)?.let { view ->
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0
} else 0
homeAdapter.data = newData
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState()
}
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
}
}
private fun updateEmptyState() {
val threadCount = (binding.recyclerView.adapter)!!.itemCount
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible

View File

@ -26,16 +26,15 @@ class HomeAdapter(
var header: View? = null
private var _data: List<ThreadRecord> = emptyList()
var data: List<ThreadRecord>
get() = _data.toList()
var data: List<ThreadRecord> = emptyList()
set(newData) {
val previousData = _data.toList()
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
if (field !== newData) {
val diff = HomeDiffUtil(field, newData, context, configFactory)
val diffResult = DiffUtil.calculateDiff(diff)
_data = newData
field = newData
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
}
}
fun hasHeaderView(): Boolean = header != null
@ -61,7 +60,7 @@ class HomeAdapter(
override fun getItemId(position: Int): Long {
if (hasHeaderView() && position == 0) return NO_ID
val offsetPosition = if (hasHeaderView()) position-1 else position
return _data[offsetPosition].threadId
return data[offsetPosition].threadId
}
lateinit var glide: GlideRequests

View File

@ -1,80 +1,67 @@
package org.thoughtcrime.securesms.home
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.cash.copper.flow.observeQuery
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import java.lang.ref.WeakReference
import org.thoughtcrime.securesms.util.observeChanges
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
private val executor = viewModelScope + SupervisorJob()
private var lastContext: WeakReference<Context>? = null
private val updateJobs: MutableList<Job> = mutableListOf()
private val _conversations = MutableLiveData<List<ThreadRecord>>()
val conversations: LiveData<List<ThreadRecord>> = _conversations
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
override fun onCleared() {
super.onCleared()
for (job in updateJobs) {
job.cancel()
}
updateJobs.clear()
}
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
// If the context has changed (eg. the activity gets recreated) then
// we need to cancel the old executors and recreate them to prevent
// the app from triggering extra updates when data changes
if (context != lastContext?.get()) {
lastContext = WeakReference(context)
updateJobs.forEach { it.cancel() }
updateJobs.clear()
updateJobs.add(
executor.launch(Dispatchers.IO) {
context.contentResolver
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
.onEach { listUpdateChannel.trySend(Unit) }
.collect()
}
class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase,
@ApplicationContext appContext: Context,
) : ViewModel() {
// SharedFlow that emits whenever the user asks us to reload the conversation
private val manualReloadTrigger = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
updateJobs.add(
executor.launch(Dispatchers.IO) {
for (update in listUpdateChannel) {
/**
* A [StateFlow] that emits the list of threads in the conversation list.
*
* This flow will emit whenever the user asks us to reload the conversation list or
* whenever the conversation list changes.
*/
@Suppress("OPT_IN_USAGE")
val threads: StateFlow<List<ThreadRecord>?> = merge(
manualReloadTrigger,
appContext.contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI))
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) }
.mapLatest { _ ->
withContext(Dispatchers.IO) {
threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor)
val threads = mutableListOf<ThreadRecord>()
buildList(reader.length) {
while (true) {
threads += reader.next ?: break
}
withContext(Dispatchers.Main) {
_conversations.value = threads
add(reader.next ?: break)
}
}
}
}
)
}
return conversations
}
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
fun tryUpdateChannel() = manualReloadTrigger.tryEmit(Unit)
companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
}
}

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.util
import android.content.ContentResolver
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.annotation.CheckResult
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
/**
* Observe changes to a content URI. This function will emit the URI whenever the content or
* its descendants change, according to the parameter [notifyForDescendants].
*/
@CheckResult
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
return callbackFlow {
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
trySend(uri)
}
}
registerContentObserver(uri, notifyForDescendants, observer)
awaitClose {
unregisterContentObserver(observer)
}
}
}