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) { if (hexEncodedSeed == null) {
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
} }
val appContext = applicationContext
val loadFileContents: (String) -> String = { fileName -> val loadFileContents: (String) -> String = { fileName ->
MnemonicUtilities.loadFileContents(this, fileName) MnemonicUtilities.loadFileContents(appContext, fileName)
} }
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
} }

View File

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

View File

@ -24,7 +24,9 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.themeState
import java.io.IOException import java.io.IOException
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -99,7 +100,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
@Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
@ -205,18 +205,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up empty state view // Set up empty state view
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() } binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
IP2Country.configureIfNeeded(this@HomeActivity) IP2Country.configureIfNeeded(this@HomeActivity)
startObservingUpdates()
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
}
// Set up new conversation button // Set up new conversation button
binding.newConversationButton.setOnClickListener { showNewConversation() } binding.newConversationButton.setOnClickListener { showNewConversation() }
// Observe blocked contacts changed events // 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 // subscribe to outdated config updates, this should be removed after long enough time for device migration
lifecycleScope.launch { 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 { lifecycleScope.launchWhenStarted {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
// Double check that the long poller is up // Double check that the long poller is up
@ -385,52 +402,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) 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() { override fun onPause() {
super.onPause() super.onPause()
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false) ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
homeViewModel.getObservable(this).removeObservers(this)
} }
override fun onDestroy() { override fun onDestroy() {
val broadcastReceiver = this.broadcastReceiver
if (broadcastReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
}
super.onDestroy() super.onDestroy()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
} }
// endregion // endregion
// region Updating // 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() { private fun updateEmptyState() {
val threadCount = (binding.recyclerView.adapter)!!.itemCount val threadCount = (binding.recyclerView.adapter)!!.itemCount
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible

View File

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

View File

@ -1,80 +1,67 @@
package org.thoughtcrime.securesms.home package org.thoughtcrime.securesms.home
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.cash.copper.flow.observeQuery
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.onEach 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.DatabaseContentProviders
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import java.lang.ref.WeakReference import org.thoughtcrime.securesms.util.observeChanges
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() { class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase,
private val executor = viewModelScope + SupervisorJob() @ApplicationContext appContext: Context,
private var lastContext: WeakReference<Context>? = null ) : ViewModel() {
private val updateJobs: MutableList<Job> = mutableListOf() // SharedFlow that emits whenever the user asks us to reload the conversation
private val manualReloadTrigger = MutableSharedFlow<Unit>(
private val _conversations = MutableLiveData<List<ThreadRecord>>() extraBufferCapacity = 1,
val conversations: LiveData<List<ThreadRecord>> = _conversations onBufferOverflow = BufferOverflow.DROP_OLDEST
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()
}
) )
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 -> threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor) val reader = threadDb.readerFor(openCursor)
val threads = mutableListOf<ThreadRecord>() buildList(reader.length) {
while (true) { while (true) {
threads += reader.next ?: break add(reader.next ?: break)
}
withContext(Dispatchers.Main) {
_conversations.value = threads
} }
} }
} }
} }
)
}
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)
}
}
}